Claude Code hook 是 Claude Code 在生命周期固定点运行的命令,所以无论模型是否选择,通知、格式化或拦截都会发生。文档中的通知是 ~/.claude/settings.json 里的 Notification 命令:macOS 用 osascript,Linux 用 notify-send,Windows 用 PowerShell MessageBox。在只读的 /hooks 菜单里确认。该文件只属于这台机器,云端会话不会读取它。共享的 Prettier hook 放在项目的 .claude/settings.json,事件是 PostToolUse,matcher 为 Edit|Write。成功时聊天里什么都不显示,它也看不到 Bash 命令改写的文件。要拦住一次编辑,Anthropic 的 protect-files.sh 会在 Edit 或 Write 之前把原因写到 stderr,并以退出码 2 退出。要拦住破坏性的 shell 命令,block-rm.sh 以退出码 0 退出,并输出 permissionDecision deny。退出码 2 即使 JSON 写的是 allow,也会拦截 PreToolUse。单独的退出码 0 不会批准这次调用。PermissionRequest 和 Notification 忽略退出码 2。无法启动的 command hook,或在 PreToolUse 上超时的 command、HTTP、MCP tool hook,会把工具调用留在平常的权限流程里。命令本身以你的用户账户的文件权限运行。

无论模型是否选择,hook 都会运行

Claude Code hook 是挂在 Claude Code 生命周期某个时刻上的 shell 命令。那个时刻一到,Claude Code 就会运行这条命令,所以无论模型是否选择运行,通知、格式化或拦截都会发生。Anthropic 的 hook 指南是配方。hook 参考收录事件 schema、退出码表和 JSON 字段。

处理程序有五种:command、http、mcp_tool、prompt 和 agent。下面三个配方使用 command。决定需要判断时,prompt hook 会把 hook 输入发给一个 Claude 模型。指南里的默认模型是 Haiku。agent hook 是实验性的,可能会变,指南说生产工作流应优先使用 command hook。

Anthropic 还把 GitHub 上的 Bash 命令校验器作为完整参考实现链接出来。

用 hook 自动化操作

hook 参考

Bash 命令校验示例

从文档中的桌面通知开始

打开 ~/.claude/settings.json,加入一个 Notification hook。文件不存在就创建。Anthropic 的第一个例子是 macOS 上的 osascript 桌面通知,matcher 为空,“type” 为 “command”。

如果文件里已经有 hooks 键,就把 Notification 加在其他事件键旁边。每个事件名都是这一个 hooks 对象里的一个键。替换整个对象会删掉该文件里已经配置的全部 hook。

输入 /hooks 确认注册。菜单列出每个事件和数量。选中 hook 会显示 matcher、type、源文件和命令。菜单是只读的。添加、修改或删除 hook 需要编辑 JSON,或让 Claude 来改。

要试一次,按 Esc,再按 Shift+Tab,直到状态栏显示 manual mode on。让它做一件需要权限的事,然后离开终端。在 macOS 上,osascript 经过 Script Editor。如果 Script Editor 没有通知权限,命令会静默失败,macOS 也不会提示你授权。先运行一次 osascript -e ‘display notification “test”’,再在 System Settings 里允许 Script Editor 的通知。

~/.claude/settings.json

{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

Linux 和 Windows 使用同一个 Notification 事件

Linux 和 Windows 的 ~/.claude/settings.json 里也放入同一个 Notification hook、空 matcher 和 command 类型。变的只有命令字符串。Anthropic 的 Linux 命令是 notify-send。notify-send 需要桌面通知守护进程,无头服务器、SSH 会话和大多数容器没有它。用 notify-send ‘Claude Code’ ‘test’ 测试。在 Debian 或 Ubuntu 上如果找不到命令,软件包是 libnotify-bin。

Anthropic 的 Windows 命令会打开一个 PowerShell MessageBox。那是对话框,所以可能被终端挡住。先在 PowerShell 里测试这条命令。在 WSL 里,powershell.exe 必须通过 Windows 互操作出现在 PATH 上。Notification hook 不能拦截:它的退出码和 stderr 会被忽略。

Anthropic 为各平台提供的 Notification 命令。把命令放进与 macOS 示例相同的 command 字段。

| 平台 | 命令 |

| — | — |

| macOS | osascript -e ‘display notification “Claude Code needs your attention” with title “Claude Code”’ |

| Linux | notify-send ‘Claude Code’ ‘Claude Code needs your attention’ |

| Windows | powershell.exe -Command “[System.Reflection.Assembly]::LoadWithPartialName(‘System.Windows.Forms’); [System.Windows.Forms.MessageBox]::Show(‘Claude Code needs your attention’, ‘Claude Code’)” |

只匹配你真正想要的通知

空 matcher 会对每一种通知类型触发。permission_prompt 在 Claude 需要你批准工具,或批准沙箱命令的网络请求时触发。idle_prompt 在 Claude 完成回复约 60 秒后、你还没有输入时触发。elicitation 对话框使用与 permission_prompt 相同的约 6 秒门槛。

在终端里,permission_prompt 的计时从提示出现时开始,等待约 6 秒。每次按键都会推迟它。在 Claude Desktop、VS Code 扩展,以及其他通过 Agent SDK 回答权限请求的宿主里,permission_prompt 发生在询问之后约 6 秒,输入不会推迟它。如果你更早回答,或 PermissionRequest hook 更早回答,那个 Notification hook 就不会运行。权限时刻本身使用 PermissionRequest。

agent_needs_input 在 agent view 打开、后台会话开始等待你的输入时触发,或在当前会话询问队友的终端设置问题、而你约 6 秒没有输入时触发。agent_completed 在后台会话完成或失败时触发,并且只在 agent view 打开时触发。quota_auto_resume_fired 在 claude.ai 用量上限暂停任务之后、Claude Code 继续该任务时触发。时间点是上限重置时,或者等待期间你做的某件事让用量重新可用,那时会更早。quota_auto_resume_stale 在电脑休眠超过约 30 分钟、期间该上限已经重置时触发。Claude Code 会等待 Enter。更短的休眠之后它会继续,并改为触发 quota_auto_resume_fired。quota_auto_resume_disabled 在 Claude Code 结束等待且不继续任务时触发。按 Esc 或 Ctrl+C,或选择 Don’t continue automatically 时,它不会触发。

agent_needs_input 和 agent_completed 需要 Claude Code v2.1.198 或更高版本。agent_needs_input 里队友终端设置的情形需要 v2.1.248 或更高版本。三个 quota_auto_resume matcher 需要 v2.1.234 或更高版本。在终端里,针对沙箱命令网络请求的 permission_prompt 需要 v2.1.246 或更高版本。

个人提醒放在用户文件,共享 hook 放在项目文件

~/.claude/settings.json 适用于这台机器上的每个项目,不能共享。.claude/settings.json 适用于一个项目,可以提交。.claude/settings.local.json 适用于一个项目,Claude Code 把设置保存到那里时会被 gitignore。hook 条目会在用户、项目和本地设置之间合并:每个文件加上自己的 hook。项目里的格式化程序不会去掉用户的通知 hook。同一个处理程序写在多个设置文件里也只运行一次。插件或 skill 里的那份副本仍然分开。在一个文件里替换 hooks 对象,仍会丢掉该文件中配置的其他事件。

云端会话不会读取本地的 ~/.claude/settings.json,所以该文件里的个人通知 hook 留在这台机器上。

command hook 以你的用户账户权限运行。该账户能访问的任何文件,它们都可以修改、删除或读取。把命令提交进共享项目文件之前,先审查并测试。在交互会话里,Claude Code 会保留所有设置文件中的 hook,包括 ~/.claude/settings.json,直到工作区信任对话框被接受。在 claude -p 或 SDK 会话里,Claude Code 把该文件夹视为已信任,因此提交在 .claude/settings.json 里的 hook 会运行。

Anthropic 所说的 hook 设置适用范围。

| 文件 | 范围 | 可否共享 |

| — | — | — |

| ~/.claude/settings.json | 这台机器上的每个项目 | 否 |

| .claude/settings.json | 一个项目 | 是,可以提交 |

| .claude/settings.local.json | 一个项目 | 否。Claude Code 把设置保存到该文件时会被 gitignore |

在 Edit 或 Write 之后格式化文件

Anthropic 的编辑后格式化配方是项目 .claude/settings.json 里的 PostToolUse hook,matcher 为 Edit|Write。命令用 jq 读取 tool_input.file_path,再传给 npx prettier –write。把 PostToolUse 加在该文件里已有的事件键旁边。

hook 指南里的 Bash 示例使用 jq。macOS 用 brew install jq 安装,Debian 和 Ubuntu 用 apt-get install jq。hook 成功时,Claude Code 在对话里什么都不显示。Anthropic 的检查方法是让 Claude 往一个 JavaScript 文件加一行单引号字符串,然后打开文件:在 Prettier 默认设置下,hook 会把它们改成双引号。

PostToolUse 在工具成功之后运行,所以不能撤销这次编辑。Edit|Write 也会漏掉被 Bash 命令改写的文件。无论是谁写入,只要某个特定文件都要重新格式化,Anthropic 指向 FileChanged hook。像 .envrc|.env 这样的 FileChanged matcher 是按 | 拆开的字面文件名列表,FileChanged 不能拦截写入。对于一个回合里的每次更改,指南指向一次 Stop 扫描,或一个 Bash|PowerShell matcher,其脚本用 git status –porcelain 列出已修改和未跟踪的文件。在 Windows 上,PowerShell 工具是主 shell 时,只匹配 Bash 的 hook 不会触发。

.claude/settings.json

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}

用退出码 2 拦截编辑

Anthropic 的 protect-files 示例是挂在 Edit|Write 上的 PreToolUse hook。脚本读取 tool_input.file_path,把 .env、package-lock.json 和 .git/ 当作受保护的子串,把一条 Blocked 消息写到 stderr,并以退出码 2 退出。Claude Code 随后拦截这次 Edit 或 Write,并把该消息传给 Claude。脚本末尾的退出码 0 表示没有决定,平常的权限流程仍然适用。它不会批准这次编辑。

在 Windows 上,Write、Edit 和 Read 用反斜杠传递 file_path。脚本的替换会在模式检查之前把它们变成正斜杠。用正斜杠去比较反斜杠路径,编辑就会通过。

把脚本存为 .claude/hooks/protect-files.sh。在 macOS 和 Linux 上运行 chmod +x .claude/hooks/protect-files.sh。这条命令是 shell form,所以从项目的 .claude/settings.json 注册时要给路径加引号。Anthropic 的测试是让 Claude 给 .env 加一条注释。编辑会在运行前被拦截。

在 PreToolUse 上,即使脚本同时输出了写着 allow 的 JSON,退出码 2 仍然拦截。使用 stderr 加退出码 2,或退出码 0 加 JSON,每个 hook 选一种。

.claude/hooks/protect-files.sh

#!/bin/bash
# protect-files.sh

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

# Normalize Windows backslash separators so the patterns below match
FILE_PATH="${FILE_PATH//\\//}"

PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")

for pattern in "${PROTECTED_PATTERNS[@]}"; do
  if [[ "$FILE_PATH" == *"$pattern"* ]]; then
    echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
    exit 2
  fi
done

exit 0

在 Edit 或 Write 之前注册文件防护

把这个 PreToolUse 组加到项目的 .claude/settings.json,放在已有事件键旁边。$CLAUDE_PROJECT_DIR 是会话开始时的项目根目录。在 shell form 里引号很重要,带空格的路径才能保留下来。

无法启动的 command hook,也就是文件缺失或脚本不可执行,不会拦截编辑。shell 会以 127 这样的代码退出,记录里会出现一条不拦截的 hook 错误,工具调用继续。在 PreToolUse 上到达超时的 command、HTTP 或 MCP tool hook,也会把调用留在平常的权限流程里。这些 hook 的默认时间是 600 秒。启动失败或超时的守卫会把门留开,所以硬拦截仍然属于权限系统。参考说明的例外是:在 PreToolUse 上超过超时的 Agent SDK 回调 hook。该回调会拦截工具调用。

.claude/settings.json

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
          }
        ]
      }
    ]
  }
}

用 JSON 拒绝破坏性的 shell 命令

要在命令运行前停下,对 Bash 使用 PreToolUse。Anthropic 的参考示例把 block-rm.sh 保存在项目里,用 chmod +x 让它可执行,并用 if 规则 Bash(rm *) 注册。命令包含 rm -rf 时,脚本输出 permissionDecision deny。否则它以退出码 0 退出,平常的权限流程保持不变,也不会批准该命令。

if 字段只放一条权限规则,匹配是 best-effort。当 Claude Code 无法判断一条 Bash 输入会运行哪些命令时,它会不论模式如何都运行这个 hook。参考说,硬性的 allow 或 deny 应使用权限系统,而不是 hook。hook 实际返回的 permissionDecision deny,在 bypassPermissions 模式和 –dangerously-skip-permissions 下仍然拦截工具。allow 决定不会放宽设置里的 deny 规则。

在 PreToolUse 上,deny 取消调用,并把 permissionDecisionReason 发给 Claude;ask 显示平常的提示;allow 跳过交互提示,deny 和 ask 规则仍然适用。defer 用于非交互的 claude -p 运行。一个 PreToolUse hook 返回 deny 之后,同级 hook 仍会运行。合并决定的顺序是 deny、defer、ask、allow。

这个注册把 args 设为空数组,这是 exec form:Claude Code 直接启动脚本,不经过 shell。命令需要管道或 && 时省略 args,Prettier hook 就是这样。在 Windows 上,PowerShell 是主 shell 时,也要匹配 Bash|PowerShell。只匹配 Bash 的 hook 在那里不会触发。参考里还有一个单独的 PowerShell 处理程序,用于 Remove-Item。

.claude/settings.json

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(rm *)",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh",
            "args": []
          }
        ]
      }
    ]
  }
}

输出 deny 的脚本

把它存为 .claude/hooks/block-rm.sh,并在 macOS 和 Linux 上 chmod +x。它使用 jq,jq 必须在 PATH 上。对于 rm -rf,它输出 JSON 并以退出码 0 退出,所以拒绝调用的是 JSON。对于 rm file.txt 这样更安全的命令,它以退出码 0 退出且没有输出。这种写法保持退出码 0。加上退出码 2 的话,即使后来把 JSON 改成 allow,也会拦截。

PreToolUse 也会跳过 EndConversation,并且看不到你用 @ 引用的文件。Claude Code 在构建提示时插入这些内容,所以针对它们的 Read hook 不会触发。

.claude/hooks/block-rm.sh

#!/bin/bash
# .claude/hooks/block-rm.sh
COMMAND=$(jq -r '.tool_input.command')

if echo "$COMMAND" | grep -q 'rm -rf'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Destructive command blocked by hook"
    }
  }'
else
  exit 0
fi

退出码的作用因事件而异

退出码 0 表示 hook 没有通过退出码提出异议。在 PreToolUse 上,平常的权限流程保持不变。hook 输出 JSON 决定时,也使用退出码 0。退出码 2 拦截可以拦截的事件。原因是:若该 JSON 决定本身在拦截,就用决定里的 reason,否则用 stderr。PermissionRequest 忽略退出码 2;deny 必须来自 hookSpecificOutput.decision.behavior。Notification hook 忽略自己的退出码和 stderr。PostToolUse 不能撤销已经运行的工具;那里的退出码 2 是在事后把 stderr 显示给 Claude。

对大多数事件,能单靠退出码拦截的只有 2。没有有效的决定 JSON 时,退出码 1 是不拦截的错误,动作继续。WorktreeCreate 是例外:任何非零退出都会让工作树创建失败。在可以拦截的事件上把退出码 2 和 JSON 混用时,退出码 2 仍然拦截,permissionDecision 为 allow 也不能覆盖它。在 Claude Code v2.1.214 之前,退出码 2 与未通过 schema 校验的 stdout JSON 合在一起时不会拦截。当前参考说这种组合仍然拦截,Claude Code 用 stderr 作为原因。

如果 shell 配置在 JSON 之前打印了文字,stdout 就不再以 { 开头,Claude Code 会把它当作纯文本,在退出码 0 上忽略该决定。指南的修法是只在交互式 shell 里 echo。无法启动的 command hook,以及在 PreToolUse 上超时的 command、HTTP 或 MCP tool hook,会把工具调用留在平常的权限流程里。

  • PreToolUse 上的退出码 0 让平常的权限流程保持不变。
  • 退出码 0 加 JSON 是结构化控制:PreToolUse 用 permissionDecision,PermissionRequest 用 decision.behavior,Stop 和 PostToolUse 等事件用顶层 decision block。
  • 即使 stdout JSON 写着 allow,退出码 2 也会拦截 PreToolUse。
  • PostToolUse 上的退出码 2 在工具已经运行之后把 stderr 显示给 Claude。
  • PermissionRequest 和 Notification 忽略退出码 2。
  • 没有有效决定 JSON 的退出码 1 不会拦截。

对这些配方中的事件,Anthropic 的 hook 参考说明退出码 2 和 JSON 会做什么。

| 事件 | 退出码 2 的作用 | 做出决定的 JSON 字段 |

| — | — | — |

| PreToolUse | 拦截工具调用 | hookSpecificOutput.permissionDecision:allow、deny、ask 或 defer |

| PostToolUse | 把 stderr 显示给 Claude;工具已经运行 | 顶层 decision 设为 block |

| PermissionRequest | 被忽略;权限流程继续 | hookSpecificOutput.decision.behavior:allow 或 deny |

| Notification | 被忽略 | 无。此事件没有决定控制 |

| Stop | 让 Claude 继续工作,而不是结束这一回合 | 顶层 decision 设为 block |

三种节奏覆盖会话的大部分

hook 参考把常见事件分成三种节奏。SessionStart 和 SessionEnd 按会话运行。UserPromptSubmit、Stop 和 StopFailure 按回合运行。PreToolUse 和 PostToolUse 在代理循环内的工具调用上运行,EndConversation 两者都跳过。格式化放在 PostToolUse。拦截放在 PreToolUse。桌面提醒放在 Notification。Notification 在 Claude Code 发送通知时触发,包括上面所说延迟的 permission_prompt。

同一事件上匹配的 hook 会并行运行到结束。一个 deny 不会停掉旁边的日志 hook。多个 PreToolUse hook 为同一个工具返回 updatedInput 时,最后完成的那个生效,这个顺序不是确定的。

把 Anthropic 的 Notification 命令加入 ~/.claude/settings.json,并在 /hooks 中确认。Linux 使用 notify-send,Windows 使用 PowerShell MessageBox 命令。安装 jq,再把 PostToolUse、matcher 为 Edit|Write 的 Prettier 命令加入项目的 .claude/settings.json。保存 protect-files.sh,在 macOS 或 Linux 上 chmod +x,并用带引号的 $CLAUDE_PROJECT_DIR 路径注册到 PreToolUse。用同样方式保存 block-rm.sh,把 if 设为 Bash(rm *),把 args 设为空数组,让它输出 permissionDecision deny。权限系统里也要保留硬 deny,因为守卫脚本启动失败或超时并不会拦截工具调用。

来源