第 7 章 · 03 exit-code 协议与踩坑经验


文档摘要

第 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 的处理方式。

第 7 章 · 03 exit-code 协议与踩坑经验

本节摘要:钩子与宿主之间靠退出码 + JSON 通信:exit 0 表示成功并解析 stdout JSON,exit 2 表示阻断(原因写 stderr),其他退出码是非阻断错误。JSON 输出里藏着全部控制面:PreToolUse 的 permissionDecision(allow/deny/ask/defer,冲突时 deny > defer > ask > allow)、PostToolUse 的 blockcontinueOnBlock、各事件的 additionalContext。本节最后汇总调试方法与全部踩坑经验——引号、路径、stderr 静默、Stop 死循环、stdin 占用,一个都别踩。

学习目标

阅读完本节,你应当能够:

  1. 背出 exit-code 三态行为,并说出各自对应 stderr 的处理方式。
  2. 说出 JSON 输出的核心字段与 PreToolUse 权限决策的优先级。
  3. 说出 PostToolUse 的 block 语义与 continueOnBlock 的适用场景。
  4. 按"独立喂 JSON → 检查退出码 → 查 stderr"的顺序调试钩子。
  5. 背出安全最佳实践清单与五个常见坑。

一、exit-code 协议:三态行为

钩子进程与宿主的全部默契浓缩在三组退出码:

退出码 含义 行为
0 成功 继续,解析 stdout 的 JSON
2 阻断错误 阻断当前操作,stderr 作为原因展示给用户
其他 非阻断错误 继续,stderr 仅在 verbose 模式显示

两个细节务必记住:exit 2 时 stderr 是用户唯一能看到的原因,不写就等于"莫名其妙被拦";exit 0 但 stderr 有内容时,那是静默丢弃的——想让人看到,要么 exit 2,要么走 JSON 输出的 systemMessage 字段。hook 输入一律经 stdin(JSON),不是命令行参数——这是新手最容易踩的坑。

二、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(工具执行耗时,不含权限询问与钩子自身)。

三、JSON 输出:钩子怎么说话

通用外壳

{ "continue": true, "stopReason": "Optional message if stopping", "suppressOutput": false, "systemMessage": "Optional warning message", "hookSpecificOutput": { "hookEventName": "PreToolUse", "...": "..." } }

hookSpecificOutput 按事件装载专用字段,是主要的控制面。

PreToolUse:权限四态

行为
allow 跳过权限询问(需交互的工具与组织设为 ask 的连接器除外)
deny 阻止工具调用
ask 弹窗让用户确认
defer 优雅退出,工具可稍后恢复;该值下其余字段全部忽略

permissionDecisionReason:allow/ask 时给用户看,deny 时给 Claude 看。多个钩子冲突时优先级:deny > defer > ask > allow——安全永远压过便利,而且 deny/ask 规则无论如何都会被继续评估,钩子不能绕过现有权限体系。还能用 updatedInput 改写工具参数(比如把文件路径重定向到允许目录)。

PostToolUse:block 与 continueOnBlock

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)。

四、调试三板斧

  1. 独立喂 JSON:把钩子从 Claude Code 里拆出来单独跑,可控且快:
echo '{"tool_name": "Bash", "tool_input": {"command": "ls -la"}}' | python3 .claude/hooks/validate-bash.py echo $? # 检查退出码
  1. claude --debug:启动时加 debug 标志,看完整钩子执行日志;会话内按 Ctrl+O 开 verbose 模式看实时进度。
  2. 按退出码对号入座:非 0 非 2 → 脚本本身崩了(查语法/依赖);2 → 主动阻断(查 stderr 理由);0 但没效果 → JSON 字段写错(校验 hookSpecificOutput.hookEventName 是否与事件一致)。

五、踩坑经验与安全清单

五个高频坑

  1. stdin 被占用:钩子的 stdin 是 JSON 载荷,交互式 read 必须 </dev/tty,否则读到的是 JSON 而不是用户输入。
  2. 路径硬编码:脚本里写 /Users/xxx/... 会在别的机器上炸,一律用 $CLAUDE_PROJECT_DIR 锚定项目根,并给脚本加 chmod +x
  3. bash 自动批准漏洞(v2.1.145 已封):FOO=bar somecommand 这种"内联变量赋值+非白名单命令"曾能被仅含 FOO=bar 的白名单规则隐式放行;升级后此类命令会回到权限询问。想放行就写覆盖完整命令的 Bash(...) 规则。
  4. Stop 死循环:有 bug 的 Stop 钩子连续 block 会卡死会话——默认 8 次熔断(可调 CLAUDE_CODE_STOP_HOOK_BLOCK_CAP),写 Stop 钩子时务必想好终止条件。
  5. 配置互斥:commandargs 两种命令钩子写法互斥,同设会被配置加载拒绝;需要管道/重定向/&& 链用 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。


发布者: 作者: 灏天文库 转发
评论区 (0)
U