[说人话系列] Claude code 的 Hooks 钩子怎么用?以及个人推荐的2个Hooks钩子

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设置的时候为了灵活度多了一些东西:

  1. 这里多了 -Path "D:\XXX项目\XXX工程"" 可以灵活变更文本树开始生成的根目录
  2. 可以在脚本中的 ExcludeFolders 灵活添加屏蔽的文件夹,添加后可以递归屏蔽所有的子文件夹和子文件的显示
  3. 可以在脚本中的 ExcludeFiles 灵活添加屏蔽的文件类型,支持通配符

    半夜码字累死了,希望佬友们多多点赞和关注,后续会有更多实用的经验分享
44 个赞

感谢分享,明早试试

感谢佬分享。

1 个赞

好文,感谢分享。另外佬友对subagents有什么心得吗

2 个赞

我也想知道

非常感谢分享,拓宽了思路

感谢大佬!

谢谢佬友 等使用到claude code再去看看怎么使用

晚点有时间会开贴分享哈

1 个赞

我试了下生成整个项目的文件树,项目不算大,600多个java文件,生成出来的 poject_tree.txt 有69k,1300多行,这真的能贴给AI吗,只有gemini能这么搞吧

个人不是很建议用这个,确实非常消耗token。赞同楼主的hooks场景分类,我认为在"往LLM上下文中塞"的场景里一定要避免频繁调用hooks的情况下,往里塞过长的内容。一般一句话或500tokens以内为佳。

1 个赞

对,得加约束才行

你可以给hooks严格约束的过滤器来调用这个文件树hooks,这样就不会有上下文占用的问题了

受益良多,请教下,如何能在当前cc cli 终端输入 继续 让他继续干活,有没有内置命令直接写入 cli,目前的方法是通过查找终端窗口,前置,然后输入,属于模拟的,容易出问题

找到方法了

#Requires -Version 5.1


# 输出 JSON,使用 UTF-8 编码
$json = $output | ConvertTo-Json -Compress
$json = '{"decision": "block", "reason": "继续"}'

[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
Write-Output $json

钩子支持的输出可以不阻碍claude运行同时可以把信息注入他的上下文

感谢佬分享