
大多数人知道 Claude Code 有权限系统,但背后还有一套更底层的可编程层:Hooks。本篇完整拆解 30+ 个生命周期事件(PreToolUse/PostToolUse/Stop/SessionStart 等三层结构)、五种处理器类型(Command/HTTP/MCP 工具/Prompt/Agent)、两条控制路径(exit code vs JSON output)、PreToolUse 四档决策(allow/deny/ask/defer)、Matcher 匹配规则与 `if` 过滤器,以及异步 Hook 和 Stop Hook 质量门禁的实战用法,附五条可落地的实践建议。
| 层级 | 事件 | 触发时机 |
|---|---|---|
| 会话级(每次会话触发一次) | SessionStart / SessionEnd / Setup | 会话开启 / 结束 / CI 初始化 |
| 每轮级(每次用户交互一次) | UserPromptSubmit / Stop / StopFailure | 用户提交 Prompt 前后 |
| 工具调用级(Agent 循环中每次工具调用) | PreToolUse / PostToolUse / PostToolBatch 等 | 工具执行前中后 |
PreToolUse:工具执行前触发,可拦截/放行/修改工具调用参数PostToolUse:工具执行后触发,工具已跑完,可向 Claude 注入反馈UserPromptSubmit:用户 Prompt 到达 Claude 前触发,可拦截或注入上下文Stop:Claude 完成一轮回复后触发,可阻止 Claude 停止,强制继续工作SessionStart:会话启动时触发,可加载开发上下文、注入环境变量PermissionRequest:弹出权限确认对话框时触发,可以代用户自动放行或拒绝PostToolBatch:一批并行工具调用全部完成后触发,比 PostToolUse 有整体视角FileChanged:指定文件发生变化时触发,配合 direnv 可实现目录环境自动切换SubagentStart/Stop、TaskCreated/Completed、PreCompact/PostCompact、Elicitation、ConfigChange、WorktreeCreate/Remove、MessageDisplay 等 30+ 个事件覆盖完整执行链路。1{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-style.sh"
}stdin 接收 JSON 上下文,通过 stdout 返回决策,通过 exit code 信号控制流程。{
"type": "http",
"url": "http://localhost:8080/hooks/pre-tool-use",
"headers": { "Authorization": "Bearer $MY_TOKEN" },
"allowedEnvVars": ["MY_TOKEN"]
}{
"type": "mcp_tool",
"server": "my_server",
"tool": "security_scan",
"input": { "file_path": "${tool_input.file_path}" }
}{
"type": "prompt",
"prompt": "评估 Claude 是否应该停止:$ARGUMENTS,检查所有任务是否完成。"
}{"ok": true/false, "reason": "..."} 决策。{
"type": "agent",
"prompt": "检查所有单元测试是否通过:$ARGUMENTS",
"timeout": 120
}#!/bin/bash
command=$(jq -r '.tool_input.command' < /dev/stdin)
if [[ "$command" == "rm -rf"* ]]; then
echo "危险命令被拦截" >&2
exit 2 # ← 关键:exit 2 才是「阻断」信号
fi
exit 0 # ← exit 0:无决策,继续正常流程| Exit Code | 含义 |
|---|---|
0 | 成功,Claude Code 解析 stdout 中的 JSON 输出(若有) |
2 | 阻断:忽略 stdout,把 stderr 文本反馈给 Claude,阻止对应动作 |
| 其他非零 | 非阻断性错误:会话继续,terminal 显示错误提示,stderr 写调试日志 |
exit 2 才能阻断工具调用,exit 1 是「非阻断错误」——工具会继续执行。exit 0 时向 stdout 输出 JSON,可以做更细粒度的控制:{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "数据库写操作在只读模式下不允许"
}
}| 字段 | 作用 |
|---|---|
continue: false | 立即终止整个 Agent 会话,优先于任何事件决策 |
systemMessage | 向用户展示警告消息(不进入 Claude 上下文) |
additionalContext | 向 Claude 注入上下文字符串(进入 Claude 的 context window) |
terminalSequence | 触发桌面通知或终端 bell(hooks 无法直接写 /dev/tty,用这个字段代替) |
| 决策值 | 效果 |
|---|---|
"allow" | 跳过权限确认弹窗,直接放行 |
"deny" | 拒绝工具调用,把拒绝原因反馈给 Claude |
"ask" | 强制弹出权限确认窗口(含来源标签 [Project] / [User]) |
"defer" | 暂停此工具调用,进程退出,等待外部程序 resume(仅限 -p 非交互模式) |
defer 是专门给「把 Claude Code 作为子进程」场景设计的:外部程序可以在自己的 UI 里收集用户输入,再通过 --resume 把答案注入进去,工具才真正执行。PreToolUse 还支持 updatedInput 字段,在放行前修改工具的输入参数:{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": {
"command": "npm run lint --fix"
}
}
}PreToolUse hook 同时返回不同决策时,优先级顺序:deny > defer > ask > allow。matcher 字段,控制「这个 hook 在什么情况下触发」。匹配规则按字符类型路由:| Matcher 内容 | 匹配方式 | 示例 |
|---|---|---|
"*" 或省略 | 匹配所有 | 任意工具都触发 |
仅含字母/数字/_/| | 精确字符串或 | 分隔列表 | "Bash" / "Edit|Write" |
| 含其他字符 | JavaScript 正则表达式 | "mcp__memory__.*" |
mcp__{server}__{tool} 命名规范(与 #8 MCP 集成篇一致)。要匹配某个 server 下的所有工具,必须用 mcp__memory__.*——只写 mcp__memory 会被当作精确字符串匹配,什么都匹配不到。{ "if": "Bash(git *)" } // 只在 Bash 执行 git 子命令时触发
{ "if": "Edit(*.ts)" } // 只在编辑 .ts 文件时触发if 字段有「宽松失败」特性:当 Bash 命令无法解析时 hook 仍然触发;当模式覆盖到 $() 子命令或反引号时也会检查子命令内容。不要依赖 if 做硬安全边界,那是权限系统(allowed_tools / deny 规则)的职责。Stop hook 是一个很容易被忽略但极有价值的场景:Claude 完成回复后,hook 可以「强制让 Claude 继续工作」。#!/bin/bash
# 检查测试是否通过,未通过则阻止 Claude 停止
if ! npm test --silent > /dev/null 2>&1; then
echo '{"decision": "block", "reason": "测试未通过,请先修复失败的测试"}'
exit 0
fi
exit 0decision: "block" + reason:以「错误」形式强制继续,reason 作为 Claude 的下一条指令hookSpecificOutput.additionalContext:以「反馈」形式继续,不显示错误状态,更友好stop_hook_active: true 可以检测当前是否已在「被 hook 强制继续」状态。| 位置 | 作用范围 | 是否可共享 |
|---|---|---|
~/.claude/settings.json | 你的所有项目 | 不可(本机私有) |
.claude/settings.json | 单个项目 | 可以 commit 到 repo |
.claude/settings.local.json | 单个项目 | 不可(已 gitignore) |
| 企业 Managed Settings | 全组织 | 管理员分发 |
async: true 让 hook 在后台运行,Claude 继续工作不等待结果:{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/run-tests.sh",
"async": true,
"timeout": 300
}additionalContext,会在下一次对话轮次注入给 Claude。asyncRewake: true 则更进一步:hook 以 exit 2 退出时会主动唤醒 Claude,即使当前会话是空闲状态。PostToolUse + async 做轻量 CI
写文件后异步跑 lint/test,通过 additionalContext 把结果反馈给 Claude,不阻塞主流程。PreToolUse 的 updatedInput 做「最后一公里」参数修正
与其在 CLAUDE.md 里大量描述规范,不如用 hook 在工具执行前直接修改参数——比指令更可靠。Stop hook + 测试验证 = 质量门禁
让 Claude 写完代码后必须自动跑测试,测试不通过就不允许「收工」,这是零成本的自动化 QA。additionalContext;阻止工具执行用 exit 2 或 permissionDecision: "deny";直接终止会话用 continue: false。三种机制语义完全不同,混用会产生意外行为。jq 解析 JSON
Hook 输入是 JSON,不要用 grep 或字符串截取——容易被特殊字符注入。始终 jq -r '.tool_input.command',始终 "$VAR" 加引号。docs.anthropic.com
This story was produced automatically by a channel. One sentence is all it takes for Neodrop to keep producing for you.