Claude code 想必各位佬友也深度体验了,但是论坛中几乎没看到讨论Hooks钩子功能的,所以单开一帖来谈论个人对于这个cc新功能的理解以及自己搓的两个常用的钩子引用
注意:场景仅适合于直接在windows上跑的claude code,下面推荐的脚本都是 .ps1 的powershell脚本,本帖仅作为抛砖引玉的作用
Claude code钩子是什么?
官网有介绍 开始使用 Claude Code 钩子 - Anthropic ,官方的说法是【 Claude Code 钩子是用户定义的 shell 命令,在 Claude Code 生命周期的各个点执行。钩子提供对 Claude Code 行为的确定性控制,确保某些操作总是发生,而不是依赖 LLM 选择运行它们】 但是更直白的说法就是让你决定在 claude code 在 哪个时间点 执行 什么脚本。
举几个例子Claude Code 提供了几个在工作流程不同点运行的钩子事件:
- PreToolUse:在工具调用之前运行(可以阻止它们)
- PostToolUse:在工具调用完成后运行
- Notification:当 Claude Code 发送通知时运行
- Stop:当 Claude Code 完成响应时运行
- Subagent Stop:当子代理任务完成时运行
Claude code钩子怎么用
根据上面初步的印象就能开始考虑这个钩子的具体实用场景了,个人把实用场景分为了3类:“往LLM上下文中塞的”,“提取LLM输出的”,“等待时机完成固定动作的”。当然这是很简单的分类,以这个Hooks为基础可以发展出很复杂的嵌套应用,这里只是我个人的一个初步理解。
往LLM上下文中塞的
比如对于“往LLM上下文中塞的”:可以通过脚本固定把单个或者多个脚本固定塞进CC的上下文中,但是这样似乎和直接写提示词没什么区别,为了体现Hooks实用脚本的特点,我推荐的第一个也是最广泛的用法就是“注入实时时间”(脚本下面会贴出),因为CC除非你可以提醒执行BASH指令,否则不会主动读取实施时间,而Hooks完美的弥补了这一点,有了实时时间注入,在使用git commit,subagent创建维护文档时生成的时间就不会乱写了。
提取LLM输出的
在 Claude Code 里,钩子脚本会收到一整段 JSON 数据(通过 stdin 管道传进来)――里面包含了当前事件的名字、工具参数、会话 ID 等字段, 要让 Bash 或任何命令行脚本读懂这些结构化信息,就得先把 JSON 解析出来,而 jq 就是干这件事的小工具 ,官方文档中的示例就是展示并实现了 自动记录所有 Bash 命令 的功能
等待时机完成固定动作的
这个也是官方文档中提到的,比如在agent执行完成后自动构建docker之类的,各位佬友有兴趣可以试下。
Claude code推荐的Hooks脚本:自动注入时间
cn-time-injector.ps1
#Requires -Version 5.1
$ErrorActionPreference = 'Stop'
try {
$resp = Invoke-RestMethod -Uri 'https://worldtimeapi.org/api/timezone/Asia/Shanghai' -TimeoutSec 2
$iso = $resp.datetime
} catch {
# 网络失败时用本机时间转换为 UTC+8
$iso = (Get-Date).ToUniversalTime().AddHours(8).ToString("yyyy-MM-ddTHH:mm:ss.fffzzz")
}
# 使用 Here-String 避免编码问题,并确保变量正确展开
$contextMessage = "Current China Standard Time: $iso"
$output = @{
hookSpecificOutput = @{
hookEventName = 'UserPromptSubmit'
additionalContext = $contextMessage
}
}
# 输出 JSON,使用 UTF-8 编码
$json = $output | ConvertTo-Json -Compress
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
Write-Output $json
使用方法
这个建议放到全局 ~/.claude/settings.json 中进行钩子的设置
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "pwsh.exe -NoProfile -ExecutionPolicy Bypass -File \"%USERPROFILE%\\.claude\\hooks\\cn-time-injector.ps1\""
}
]
}
]
},
这里注意几点,首先是 -NoProfile -ExecutionPolicy Bypass 这几个参数我写进去是因为我这个脚本是放在c盘CC根目录也就是 ~/.claude/hooks 中的,不加的话似乎有权限的问题,当然了
"command": "powershell.exe -NoProfile -ExecutionPolicy Bypass -File \"%USERPROFILE%\\.claude\\hooks\\cn-time-injector.ps1\""
这种写法也是可以的,我是跟着哈雷佬的 《 Claude Code 终极版FAQ指南 》 - 文档共建 - LINUX DO 这个教程安装的 PowerShell 7 ,但是原装的powershell也能用。下图是实际的执行效果。
另外一点就是注意文件路径的转义符不要写错了。
Claude code推荐的Hooks脚本:实时更新文件树
generate_tree.ps1
<#--------------------------------------------------------------------
generate_tree.ps1
生成并彩色显示项目文件树,支持:
• 排除文件夹(递归)
• 排除文件(* / ? 通配符)
• 统计文件夹数、文件数、总体积
--------------------------------------------------------------------#>
param(
[string] $Path = ".", # 目标路径(默认当前目录)
[string[]]$ExcludeFolders = @(
".git","node_modules","dist",".claude_code","__pycache__",
".claude","build",".vscode",".eide","output"
), # 要排除的文件夹
[string[]]$ExcludeFiles = @("*.o","*.d","*.crf"), # 要排除的文件(通配符)
[switch] $NoExclude = $false, # 不排除任何条目
[switch] $SaveToFile = $false, # 是否保存到文件
[string] $OutputFile = "project_tree.txt", # 输出文件名
[switch] $ShowStats = $true, # 是否显示统计信息
[int] $Depth = 0 # 树深度,0 为无限制
)
#------------------------------ 参数检查 ------------------------------#
if (-not (Test-Path $Path)) {
Write-Host "错误: 路径 '$Path' 不存在!" -ForegroundColor Red
exit 1
}
#------------------------------ 环境准备 ------------------------------#
$TargetPath = Resolve-Path $Path
$OriginalLocation = Get-Location
Set-Location $TargetPath
#--------------------------- 生成排除正则 -----------------------------#
$folderRegex = ($ExcludeFolders | ForEach-Object { [regex]::Escape($_) }) -join '|'
$fileRegex = ($ExcludeFiles | ForEach-Object {
$_.Replace('.', '\.').Replace('*', '.*').Replace('?', '.')
}) -join '|'
$excludeRegex = $folderRegex
if ($fileRegex) { $excludeRegex = "($folderRegex)|($fileRegex)" }
#--------------------------- 生成 tree 输出 ---------------------------#
Write-Host "`n正在生成文件树..." -ForegroundColor Green
Write-Host "目标路径: $TargetPath" -ForegroundColor Yellow
Write-Host ("=" * 60) -ForegroundColor DarkGray
$treeCmd = "tree /F /A"
if ($Depth -gt 0) { $treeCmd += " /L $Depth" }
$treeOutput = Invoke-Expression $treeCmd
#----------------------------- 过滤输出 -------------------------------#
if ($NoExclude -or ($ExcludeFolders.Count -eq 0 -and $ExcludeFiles.Count -eq 0)) {
Write-Host "显示完整文件树(不排除任何项目)" -ForegroundColor DarkYellow
} else {
Write-Host "排除文件夹: $($ExcludeFolders -join ', ')" -ForegroundColor DarkYellow
Write-Host "排除文件 : $($ExcludeFiles -join ', ')" -ForegroundColor DarkYellow
$filteredOutput = @()
$skipDepth = -1 # 当前需要跳过的层级
$currentDepth = 0
foreach ($line in $treeOutput) {
# 计算行深度:一个“│”或一个 4 空格缩进 = 1 层
$currentDepth = ([regex]::Matches($line, '((│|\|)\s{3}|\s{4})')).Count
# 若仍在被排除目录下且更深层,直接跳过
if ($skipDepth -ge 0 -and $currentDepth -gt $skipDepth) { continue }
else { $skipDepth = -1 }
# 若本行命中排除规则,记录跳过层级
if ($line -match $excludeRegex) {
$skipDepth = $currentDepth
continue
}
$filteredOutput += $line
}
$treeOutput = $filteredOutput
}
#--------------------------- 彩色打印输出 -----------------------------#
Write-Host "`n文件树结构:" -ForegroundColor Cyan
Write-Host ("=" * 60) -ForegroundColor DarkGray
foreach ($line in $treeOutput) {
if ($line -match '^\s*$') { Write-Host $line } # 空行
elseif ($line -match '^[A-Z]:' -or $line -match '^卷.*PATH' -or $line -match '^卷序列号') {
Write-Host $line -ForegroundColor Gray # 头部信息
}
elseif ($line -match '([├└]───|\+---)\s*(.+)$') {
$name = $matches[2].Trim()
if ($name -match '\.\w+$') { Write-Host $line -ForegroundColor White } # 文件
else { Write-Host $line -ForegroundColor Yellow } # 文件夹
}
elseif ($line -match '^\s*[│|]') { Write-Host $line -ForegroundColor DarkGray } # 连接线
elseif ($line -match '\.\w+$') { Write-Host $line -ForegroundColor White } # 文件(无树符号)
else { Write-Host $line -ForegroundColor Green } # 其他
}
Write-Host ("=" * 60) -ForegroundColor DarkGray
#--------------------------- 保存到文件 -------------------------------#
if ($SaveToFile) {
$treeOutput | Out-File -FilePath $OutputFile -Encoding UTF8
Write-Host "`n文件树已保存到: $OutputFile" -ForegroundColor Green
}
#--------------------------- 统计信息 -------------------------------#
if ($ShowStats) {
Write-Host "`n正在计算统计信息..." -ForegroundColor DarkCyan
$allItems = Get-ChildItem -Path $TargetPath -Recurse -ErrorAction SilentlyContinue
$files = $allItems | Where-Object { -not $_.PSIsContainer } # ← 使用 PSIsContainer
$folders = $allItems | Where-Object { $_.PSIsContainer }
if (-not $NoExclude -and ($ExcludeFolders.Count -gt 0 -or $ExcludeFiles.Count -gt 0)) {
$files = $files | Where-Object { $_.FullName -notmatch $excludeRegex }
$folders = $folders | Where-Object { $_.FullName -notmatch $excludeRegex }
}
Write-Host "`n📊 统计信息:" -ForegroundColor Cyan
Write-Host " 📁 文件夹数: $($folders.Count)" -ForegroundColor White
Write-Host " 📄 文件数: $($files.Count)" -ForegroundColor White
$totalSize = ($files | Measure-Object -Property Length -Sum).Sum
if ($null -eq $totalSize) { $totalSize = 0 }
$sizeStr = switch ($totalSize) {
{ $_ -gt 1GB } { "{0:N2} GB" -f ($totalSize / 1GB) }
{ $_ -gt 1MB } { "{0:N2} MB" -f ($totalSize / 1MB) }
{ $_ -gt 1KB } { "{0:N2} KB" -f ($totalSize / 1KB) }
default { "$totalSize Bytes" }
}
Write-Host " 💾 总大小: $sizeStr" -ForegroundColor White
}
#------------------------------ 收尾 ------------------------------#
Set-Location $OriginalLocation
Write-Host "`n✅ 完成!" -ForegroundColor Green
这个我建议和上面的Hooks都放在 ~/.claude/hooks 里面方便管理。因为项目内部的 settings.local.json或者settings.json调用的时候可以直接指定地址,比如我在自己工程的settings.local.json中是这么写的
"hooks": {
"PreToolUse": [
{
"matcher": "Grep|Glob|Read|WebSearch|WebFetch",
"hooks": [
{
"type": "command",
"command": "pwsh.exe -NoProfile -ExecutionPolicy Bypass -File \"%USERPROFILE%\\.claude\\hooks\\generate_tree.ps1\" -Path \"D:\\XXX项目\\XXX工程\""
}
]
}
]
},
这个文件树的脚本来源于我的另一个需求,就是当CC频繁的增删文件时会经常错读文件位置,所以我需要注入当前工程实时的文件树,这样做可以让CC一直能够了解最新的项目文件关系和实时文件存在的情况。这里可以看到多了个matcher,这个说白了就是个过滤器,因为这个和实时注入时间不一样,文本很少,这个当处于大型项目的时候可能上百行的文本输出,所以我当前选择的是加入一个过滤器,只在 CC调用工具进行代码查询之前 输出一个实时文本树情况,这样既可以规避文本树文本过大占用Token,又可以让CC更懂项目结构。
这里的要注意
实时时钟注入的hooks设置是写在系统级的settings.json中,为了全局通用,而这个文件树并不是所有情况都适用的,所以只建议写在项目级的settings.json里面。
而且这里在填写hooks设置的时候为了灵活度多了一些东西:
- 这里多了 -Path "D:\XXX项目\XXX工程"" 可以灵活变更文本树开始生成的根目录
- 可以在脚本中的 ExcludeFolders 灵活添加屏蔽的文件夹,添加后可以递归屏蔽所有的子文件夹和子文件的显示
- 可以在脚本中的 ExcludeFiles 灵活添加屏蔽的文件类型,支持通配符
半夜码字累死了,希望佬友们多多点赞和关注,后续会有更多实用的经验分享

