第 7 章 · 03 exit-code 协议与踩坑经验 本节摘要:钩子与宿主之间靠退出码 + JSON 通信:exit 0 表示成功并解析 stdout JSON,exit 2 表示阻断(原因写 stderr),其他退出码是非阻断错误。JSON 输出里藏着全部控制面:PreToolUse 的 (allow/deny/ask/defer,冲突时 deny > defer > ask > allow)、PostToolUse 的 与 、各事件的 。本节最后汇总调试方法与全部踩坑经验——引号、路径、stderr 静默、Stop 死循环、stdin 占用,一个都别踩。 学习目标 阅读完本节,你应当能够: 背出 exit-code 三态行为,并说出各自对应 stderr 的处理方式。
本节摘要:钩子与宿主之间靠退出码 + JSON 通信:exit 0 表示成功并解析 stdout JSON,exit 2 表示阻断(原因写 stderr),其他退出码是非阻断错误。JSON 输出里藏着全部控制面:PreToolUse 的
permissionDecision(allow/deny/ask/defer,冲突时 deny > defer > ask > allow)、PostToolUse 的block与continueOnBlock、各事件的additionalContext。本节最后汇总调试方法与全部踩坑经验——引号、路径、stderr 静默、Stop 死循环、stdin 占用,一个都别踩。
阅读完本节,你应当能够:
continueOnBlock 的适用场景。钩子进程与宿主的全部默契浓缩在三组退出码:
| 退出码 | 含义 | 行为 |
|---|---|---|
| 0 | 成功 | 继续,解析 stdout 的 JSON |
| 2 | 阻断错误 | 阻断当前操作,stderr 作为原因展示给用户 |
| 其他 | 非阻断错误 | 继续,stderr 仅在 verbose 模式显示 |
两个细节务必记住:exit 2 时 stderr 是用户唯一能看到的原因,不写就等于"莫名其妙被拦";exit 0 但 stderr 有内容时,那是静默丢弃的——想让人看到,要么 exit 2,要么走 JSON 输出的 systemMessage 字段。hook 输入一律经 stdin(JSON),不是命令行参数——这是新手最容易踩的坑。
每个钩子都会收到一份 JSON 载荷,公共字段:
{ "session_id": "abc123", "transcript_path": "/path/to/transcript.jsonl", "cwd": "/current/working/directory", "permission_mode": "default", "hook_event_name": "PreToolUse", "tool_name": "Write", "tool_input": { "file_path": "/path/to/file.js", "content": "..." }, "tool_use_id": "toolu_01ABC123...", "agent_id": "agent-abc123", "agent_type": "main", "worktree": "/path/to/worktree", "effort": { "level": "medium" } }
常用字段:session_id(会话标识,跨钩子关联状态)、transcript_path(会话记录文件,context-tracker 靠它算 token)、cwd(当前目录)、hook_event_name(钩子自己触发的哪个事件)、tool_name/tool_input(工具名与参数)、agent_type(main 或子代理类型)。Stop/SubagentStop 额外带 last_assistant_message(最后一条回复,做完成度判断用);PostToolUse/PostToolUseFailure 额外带 duration_ms(工具执行耗时,不含权限询问与钩子自身)。
{ "continue": true, "stopReason": "Optional message if stopping", "suppressOutput": false, "systemMessage": "Optional warning message", "hookSpecificOutput": { "hookEventName": "PreToolUse", "...": "..." } }
hookSpecificOutput 按事件装载专用字段,是主要的控制面。
| 值 | 行为 |
|---|---|
allow |
跳过权限询问(需交互的工具与组织设为 ask 的连接器除外) |
deny |
阻止工具调用 |
ask |
弹窗让用户确认 |
defer |
优雅退出,工具可稍后恢复;该值下其余字段全部忽略 |
附 permissionDecisionReason:allow/ask 时给用户看,deny 时给 Claude 看。多个钩子冲突时优先级:deny > defer > ask > allow——安全永远压过便利,而且 deny/ask 规则无论如何都会被继续评估,钩子不能绕过现有权限体系。还能用 updatedInput 改写工具参数(比如把文件路径重定向到允许目录)。
PostToolUse 返回 "decision": "block" 默认终止当前回合;给钩子加 "continueOnBlock": true 后,拒绝会被当作 tool_result 反馈给 Claude——模型读到原因后可以自己重试或调整。适用判断:原因里含有 Claude 能行动的信息(如"该文件只读,请写别处")就用 continueOnBlock;必须整体停下的场景保持默认。v2.1.121+ 起,hookSpecificOutput.updatedToolOutput 对所有工具生效——PostToolUse 钩子可以改写 Bash/Edit/Read 的输出(脱敏密钥、清理 ANSI 转义、过滤噪音),不再限于 MCP 工具。
UserPromptSubmit 用 decision: "block" + reason 阻断;PermissionRequest 用 hookSpecificOutput.decision.behavior(allow/deny)带自定义消息;SessionStart 可返回 reloadSkills: true 触发技能重扫,或 hookSpecificOutput.sessionTitle 设置会话标题;任意钩子可带 terminalSequence 向宿主终端写原始 OSC 转义(OSC 9 桌面通知、OSC 0 改窗口标题——依赖终端支持,Kitty/iTerm2/Windows Terminal 认 OSC 9)。
echo '{"tool_name": "Bash", "tool_input": {"command": "ls -la"}}' | python3 .claude/hooks/validate-bash.py echo $? # 检查退出码
claude --debug:启动时加 debug 标志,看完整钩子执行日志;会话内按 Ctrl+O 开 verbose 模式看实时进度。read 必须 </dev/tty,否则读到的是 JSON 而不是用户输入。/Users/xxx/... 会在别的机器上炸,一律用 $CLAUDE_PROJECT_DIR 锚定项目根,并给脚本加 chmod +x。FOO=bar somecommand 这种"内联变量赋值+非白名单命令"曾能被仅含 FOO=bar 的白名单规则隐式放行;升级后此类命令会回到权限询问。想放行就写覆盖完整命令的 Bash(...) 规则。CLAUDE_CODE_STOP_HOOK_BLOCK_CAP),写 Stop 钩子时务必想好终止条件。command 与 args 两种命令钩子写法互斥,同设会被配置加载拒绝;需要管道/重定向/&& 链用 command,单二进制+参数用 args(execve 直启,无 shell 解析,免引号免注入)。| Do | Don't |
|---|---|
| 校验并净化所有输入 | 盲目信任输入数据 |
变量加引号:"$VAR" | 裸用 $VAR |
|
拦截路径穿越(..) |
放行任意路径 |
用 $CLAUDE_PROJECT_DIR 绝对路径 |
硬编码路径 |
跳过敏感文件(.env、.git/、密钥) |
处理所有文件 |
| 先在隔离环境测试 | 直接部署未测试钩子 |
HTTP 钩子显式 allowedEnvVars |
把全部环境变量暴露给 webhook |
总原则写在官方文档首页:钩子执行任意 Shell 命令,风险自负——只配置你信任的命令,测试先于生产,插件/远程仓库引入的钩子配置要审查后再放行。
钩子协议三句话:exit 0 放行并解析 stdout JSON,exit 2 阻断且 stderr 就是理由,其他退出码静默继续。JSON 输出是控制面——PreToolUse 的四态权限按 deny > defer > ask > allow 仲裁,PostToolUse 的 block 可配 continueOnBlock 转反馈,additionalContext 把提醒喂回给 Claude。调试先独立喂 JSON,再查退出码与 stderr。五个坑(stdio 占用、路径硬编码、隐式放行、Stop 死循环、配置互斥)与七条安全纪律背熟,你的钩子就稳了。
下一节预告:钩子是单点能力,插件是把命令/代理/钩子/MCP 打包分发——第 8 章插件 Plugins。