Claude Code Hooks在工具调用前确定性拦截受保护文件方法详解(退出码与反馈路径详解)

PreToolUse Hook 通过”读 stdin JSON + 退出码 + 可选 stdout JSON”三件套,实现对工具调用前的确定性拦截,stderr 文本与 permissionDecisionReason 共同构成反馈通道,让 Claude 看见阻断原因并修正行为。这套机制不依赖模型”听话”,因此在团队安全策略里属于”必须绕开它,否则工具根本跑不起来”的硬性护栏。

一、为什么 PreToolUse 是”确定性拦截”的位置

Claude Code 是按事件驱动的代理主循环:用户发消息 → 模型决定要调用哪个工具 → 主循环把工具调用交给对应执行器。Hooks 是一组在主循环固定位置触发的”外部回调”,每个事件点都有明确语义。

事件点 触发位置 hook 能决定什么
PreToolUse 工具执行前 阻断、改写、放行
PostToolUse 工具执行成功后 注入上下文、报错让 Claude 重试
Stop / SubagentStop 代理完成一轮 阻止回合结束
Notification 通知发送时 仅显示
SessionStart / SessionEnd 会话生命周期 注入环境/清理

把 Hook 想成 Git 里的 pre-commit 钩子:在提交真正落地之前执行你的脚本,脚本返回错误就拦下。PreToolUse 的位置相当于”工具提交到执行器前”——只在这一个时点你拥有 100% 的话语权。

二、PreToolUse 拦截的标准流水线

完整的 PreToolUse 拦截流程是五步:

  1. 模型发出”调用工具 X、参数 Y”的请求;
  2. 主循环把请求序列化为 JSON,通过 stdin 喂给 hook 进程;
  3. hook 进程解析 JSON、判断是否允许、决定退出码;
  4. 主循环读取退出码:0 走常规权限流程、2 阻断、其他视为非阻塞错误;
  5. 阻断时,stderr 文本与 JSON 中的 permissionDecisionReason 一起作为反馈注入到模型上下文。
{
  "session_id": "abc123",
  "cwd": "/Users/sarah/myproject",
  "hook_event_name": "PreToolUse",
  "tool_name": "Edit",
  "tool_input": {"file_path": "/repo/.env", "old_string": "X", "new_string": "Y"}
}

hook 脚本能用 jq -r '.tool_input.file_path' 拿到绝对路径(tool_input.file_path 永远是绝对路径),再用 jq -r '.tool_input.command' 拿到 Bash 工具的命令字符串。

三、退出码语义:0 / 2 / 其他

退出码是 hook 表达”我要不要拦”的最快通道,三类退出码语义完全不同。

退出码 含义 适用事件 反馈路径
0 成功 / 无意见 所有事件 走常规权限流程;stdout JSON 被解析
2 阻断 仅支持阻断的事件 stderr 文本送给 Claude
其他非 0 非阻塞错误 所有事件 transcript 显示 hook error,工具照常执行

注意退出码 2 只能”阻断”工具,不能自动放行——如果想让 hook 跳过权限弹窗直接放行,必须用退出码 0 + stdout JSON 的 permissionDecision: "allow" 形式。

四、用退出码 2 阻止编辑受保护文件

下面这段配置实现一个常见需求:阻止 Claude 编辑 .env、secrets/、.git/ 等敏感路径。它走”退出码 2 + stderr 解释”的极简通道。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "python3 -c \"import json,sys; d=json.load(sys.stdin); p=d.get('tool_input',{}).get('file_path',''); bad=any(x in p for x in ['/.env','/secrets/','/.git/']); sys.stderr.write('BLOCKED: protected path '+p+'\\n' if bad else ''); sys.exit(2 if bad else 0)\""
          }
        ]
      }
    ]
  }
}

hook 进程内的工作分四步:

  1. 读 stdin 拿到整个事件 JSON;
  2. 用 jq 或 Python 解析 tool_input.file_path;
  3. 命中黑名单时往 stderr 写明原因,方便模型理解;
  4. exit 2 退出,主循环把工具调用拦下、把 stderr 喂给 Claude。

效果:Claude 看到”BLOCKED: protected path /repo/.env”这样的反馈,会改用其他路径或换工具;如果它继续尝试同一条命令,hook 会再次拦下,循环往复直到模型放弃或重写路径。

五、用 JSON 输出做精细放行与改写

退出码只能表达”放行/阻断”两种态度,要更细的控制(自动放行、改写命令、追加上下文),必须用退出码 0 + stdout JSON。PreToolUse 的结构化输出字段如下。

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "use rg instead of grep for better performance"
  }
}

permissionDecision 四个值在 PreToolUse 上的语义:

  • allow:跳过交互式权限弹窗,但仍受 deny 规则约束;
  • deny:取消工具调用,把 permissionDecisionReason 送给 Claude;
  • ask:走正常人工审批弹窗;
  • defer:在 -p 非交互模式下保留调用,由外层 Agent SDK 决定。

如果想在阻断的同时把命令改写成安全版本,可以同时塞 updatedInput:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "rewrote rm to a safer variant",
    "updatedInput": {"command": "rm -i risky-file"}
  }
}

注意两条硬性规则:退出码 2 与 stdout JSON 不能混用(退出 2 时 Claude Code 会忽略 JSON);deny 始终胜出,即使 hook 返回 allow 也会被团队级 deny 规则压过。

六、PostToolUse 的”伪阻断”——错误反馈让 Claude 重试

很多人误以为 PostToolUse 也能阻断工具,其实它在 Claude Code 主循环里”晚了一步”:工具已经执行完成,hook 拿到的 tool_response 是结果。但它仍能通过 decision: "block" 顶层字段,把”hook 拒绝接受这个结果”告诉 Claude,让模型在下一轮里重做或修正。

{
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "decision": {"behavior": "block", "reason": "test output contains errors, please rerun"}
  }
}

把 PostToolUse 当成”事后把关人”:工具跑完了,但它跑得不对、不安全、留下垃圾,可以在这里喊停。PreToolUse 才是”事前把关人”。两者配合,构成本地闭环。

七、落地受保护文件拦截的工程实践

把上面的零散片段组装成可维护的策略,建议遵循四步法:

  1. 黑白名单分文件:把”绝对不能动”放进 PreToolUse 硬阻断,把”可以动但要二次确认”放进 PermissionRequest;
  2. 退出码 + JSON 双通道:默认用退出码 2 + stderr 应对简单场景,遇到需要改写参数或加上下文时切到 JSON 通道;
  3. matcher 收紧命中范围:用 Edit|Write|MultiEdit 限定到文件编辑类工具,避免误命中 Read;
  4. 测试 + 日志:先用 claude --debug 跑一遍,观察 hook 触发是否符合预期,再用 /hooks 命令做交互式配置。
# 用 debug 模式验证 hook 行为
claude --debug
# 在 transcript 里查看 "hook error" 之外是否还有 "hook stderr"
# 全量 stderr 写入 debug log

最容易踩的坑是 hook 自身写错(路径匹配失败、JSON 解析报错)而没有 fail loudly——多 matcher 并发执行时,其中一个 hook 失败不会阻断其他 hook,所以一定要在本地逐条跑通再进 settings.json。

八、阻断机制的边界

最后回到边界问题:PreToolUse 阻断很硬,但仍有几处绕不开的限制。

  • hook 自身有 600 秒默认超时,长任务会按非阻塞错误处理;
  • hook 跑在本地用户权限下,恶意 hook 可以泄露数据——务必审查 settings.json 提交记录;
  • hook 阻断只对注册过的事件点生效,模型如果绕开工具直接生成文件内容,hook 无法拦;
  • 退出 2 在 Notification / SessionStart 等事件上只会显示 stderr,工具照常推进。

把 PreToolUse 当作”硬墙”、PostToolUse 当作”软补刀”、PermissionRequest 当作”人工兜底”,三层叠加才是完整的确定性拦截体系。

常见问题(FAQ)

Q1:退出码 2 和 JSON permissionDecision 哪个更稳?

退出码 2 更轻量、依赖少;JSON 更灵活、可改写参数。两者不能混用,混用会被忽略。

Q2:PreToolUse 拦下后 Claude 会看到原因吗?

会。stderr 文本与 permissionDecisionReason 字段都会作为反馈送进模型上下文。

Q3:怎么防止 Claude 绕过 hook?

把受保护文件拦截写在 .claude/settings.json(项目级)和 ~/.claude/settings.json(用户级),并禁止随意修改;同时配合团队 managed settings。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
命令行手艺人的头像命令行手艺人普通用户

相关推荐

返回顶部