
1. 先搞清楚 Claude Code Hooks 到底在拦什么Claude Code Hooks 是嵌在 Claude Code 生命周期里的自动化脚本或逻辑它能在会话开始、工具调用前后、任务结束这些节点上介入对当前操作做校验、改写或拦截。说白了它就像给 Claude Code 装了一排关卡模型想执行 Bash、想写文件、想结束回答都得先过这道关卡关卡放行才继续关卡拦下就中止。适合谁用本地做自动化开发的、想让 AI 改代码前先跑 lint 的、想禁止危险命令的、想给每次工具调用记审计日志的都能用上。很多人第一次接触会误以为模型知道有 hooks其实不是。Hook 完全跑在 Claude Code 运行时框架层模型本身感知不到。用户发一条消息框架先拦截触发 UserPromptSubmit hook执行完 hook 逻辑后模型收到的已经是处理过的上下文。模型从头到尾不知道 hooks 存在它只是被动接收框架喂过来的内容。理解这一点很关键否则你会一直纠结为什么我配了 hook 模型没反应——因为反应本来就不该由模型给而是框架给的。触发链路大致是这样事件发生比如 Claude 决定跑Bash: rm -rf /tmp→ 框架检查配置里有没有匹配的 hook → 匹配上就把事件上下文序列化成 JSON → 命令型 hook 通过 stdin 把 JSON 喂给脚本HTTP 型 hook 把 JSON 当 POST body 发出去 → hook 执行完返回结果 → 框架根据退出码或返回的 JSON 字段决定放行、阻止还是改写输入。这里有个容易踩的坑匹配机制靠 matcher。PreToolUse 这类事件支持 matcher它拿工具名Bash、Write、Read去匹配正则。matcher 不匹配hook 直接跳过脚本根本不会跑。我见过有人 matcher 写成bash小写结果死活不触发改成Bash立刻就好了。大小写敏感这点务必记住。控制流的三种结果也要分清退出码 0 且没有阻止指令就是放行退出码 2或者 stdout 返回{permissionDecision: deny}就是阻止Claude 会取消操作并报错返回带updatedInput的 JSON就是改写参数后用新参数执行。这三种结果决定了 hook 是看门狗还是改写器。2. 接入前的准备TaoToken 配置与模型 ID 对齐在写 hook 之前得先保证 Claude Code 本身能正常跑起来不然你连触发都验证不了。这里用 TaoToken 做接入层它提供兼容 Anthropic 的接口Claude Code 直接指过去就行。官网入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 注意 API 这个地址不带 UTM 参数配置里填干净的就行。先拿 Key。打开 https://taotoken.net/api-keys 登录后创建一个 API Key复制出来。这个 Key 就是后面配置里的ANTHROPIC_AUTH_TOKEN。别把它写进会提交到 git 的文件里用环境变量或者本地 settings 更稳妥。模型 ID 这块要对齐。Claude Code 默认会请求 Anthropic 的模型名走 TaoToken 时你需要确认它支持的模型 ID 和你配置里写的一致。常见的做法是在 settings 里显式指定模型避免默认值对不上导致 404。如果你不确定用哪个模型可以先去 https://taotoken.net/models 看一眼当前可用的模型列表再回来填。环境变量配置我一般这样写放在 shell 的 profile 里export ANTHROPIC_BASE_URLhttps://taotoken.net/api export ANTHROPIC_AUTH_TOKENsk-你的TaoToken密钥 export ANTHROPIC_MODELclaude-sonnet-4-5三件套齐了Base URL 指向 TaoToken 的 APIKey 用刚创建的Model ID 填你确认可用的。配完source ~/.zshrc或重开终端然后跑claude进交互模式随便问一句看能不能正常回。能回说明接入层通了接下来才有资格谈 hook 触发。如果你用的是 Claude Code 的配置文件方式而不是环境变量可以在~/.claude/settings.json里写env字段把上面三个变量塞进去。两种方式选一种别混着来混着来容易一个覆盖另一个排查起来很烦。顺带说一句Coding Plan 适合长期编码和 Agent 场景如果你打算把 hook 用在持续集成式的自动化里可以了解下 https://taotoken.net/coding-plan 按需选。但这一步不影响 hook 本身先把基础接入跑通再说。3. 可复制的 Hooks 配置settings.json 与脚本落地配置分两层一层是settings.json里声明什么事件、匹配什么工具、跑哪个脚本另一层是脚本本身实现判断逻辑。先看配置。项目级配置放.claude/settings.json用户全局配置放~/.claude/settings.json。项目级只对当前项目生效全局对所有项目生效。我建议先在项目级试验证通过再考虑提到全局。下面是一个拦截危险 Bash 命令的完整配置{ hooks: { PreToolUse: [ { matcher: Bash, hooks: [ { type: command, command: .claude/hooks/block-rm.sh } ] } ] } }逐字段说PreToolUse是事件类型表示工具执行前触发matcher是Bash只对 Bash 工具生效其他工具调用不会触发这个 hooktype是command表示跑本地 shell 脚本另外还支持http、prompt、agentcommand是脚本路径相对项目根目录。注意用户级配置的格式略有不同它不需要外层hooks包裹直接在顶层写事件{ PreToolUse: [ { matcher: Bash, hooks: [ { type: command, command: ~/.claude/hooks/block-rm.sh } ] } ] }这个差异是很多人配置不生效的原因把项目级的{hooks: {...}}结构直接抄到用户级结果框架找不到事件hook 静默失效。记住项目级要包hooks用户级不包。然后是脚本。创建.claude/hooks/block-rm.sh内容如下#!/bin/bash INPUT$(cat) COMMAND$(echo $INPUT | jq -r .tool_input.command) if echo $COMMAND | grep -q rm -rf; then echo Blocked: Destructive command detected! 2 exit 2 fi exit 0脚本逻辑三步从 stdin 读 JSON用 jq 提取tool_input.command判断是否含rm -rf。命中就打印错误到 stderr 并exit 2框架收到退出码 2 就阻止操作没命中就exit 0放行。写完记得加执行权限chmod x .claude/hooks/block-rm.sh如果你想要更灵活的控制比如自定义阻止理由可以用 JSON 输出代替退出码jq -n { hookSpecificOutput: { hookEventName: PreToolUse, permissionDecision: deny, permissionDecisionReason: Destructive command blocked by hook } } exit 0这种方式的好处是理由能透传给用户比单纯exit 2信息量大。两种方式二选一别同时用同时用行为可能不符合预期。脚本里还能用环境变量。$CLAUDE_PROJECT_DIR是项目根目录$CLAUDE_PLUGIN_ROOT是插件目录。用它们而不是硬编码绝对路径脚本才能在不同机器上复用。比如bash $CLAUDE_PLUGIN_ROOT/scripts/validate.sh就比写死/Users/xxx/.claude/plugins/...强得多。4. 触发验证从请求到成功结果的全过程配置写完怎么确认 hook 真的在跑最直接的办法是让脚本输出点东西然后观察。先把脚本改成带日志的版本#!/bin/bash INPUT$(cat) echo [hook] $(date) triggered /tmp/hook-debug.log echo [hook] input: $INPUT /tmp/hook-debug.log COMMAND$(echo $INPUT | jq -r .tool_input.command) if echo $COMMAND | grep -q rm -rf; then echo Blocked: Destructive command detected! 2 exit 2 fi exit 0然后重启 Claude Codehook 在会话启动时加载改完必须重启才生效进交互模式让它执行一条危险命令比如直接说帮我删掉 /tmp 下的测试目录用 rm -rf。正常情况你会看到 Claude 尝试调用 Bash然后被 hook 拦下报出 Blocked: Destructive command detected!。同时去看/tmp/hook-debug.log应该能看到触发记录和完整的输入 JSON。输入 JSON 长这样{ tool_name: Bash, tool_input: { command: rm -rf /tmp/test } }看到这个说明整条链路通了事件触发 → matcher 匹配 Bash → JSON 通过 stdin 传给脚本 → 脚本判断命中 → exit 2 → 框架阻止。再试一条安全命令比如ls -la应该正常执行日志里也有记录但脚本走的是exit 0分支。验证 PostToolUse 也类似把事件换成PostToolUse脚本里读tool_response字段。PostToolUse 在工具成功执行后触发适合做 lint、格式化、审计。比如每次 Write 之后自动跑一次 prettier{ hooks: { PostToolUse: [ { matcher: Write, hooks: [ { type: command, command: .claude/hooks/format.sh } ] } ] } }format.sh里从 JSON 拿文件路径跑格式化命令exit 0即可。PostToolUse 一般不阻止操作操作已经完成了它的价值在于事后处理。验证时如果发现 hook 没触发按这个顺序查配置结构对不对项目级有没有hooks包裹→ matcher 大小写对不对 → 脚本有没有执行权限 → 改完有没有重启 Claude Code。这四步能解决八成问题。5. 常见报错排查401、local proxy failed 与 OAuth配 hook 的过程中报错往往不在 hook 本身而在接入层。下面几个是我实际遇到过的。401 Unauthorized。这个基本是 Key 的问题。检查ANTHROPIC_AUTH_TOKEN有没有填对有没有多余空格Key 是不是被撤销了。还有一种情况是 Base URL 写错比如写成了带路径的https://taotoken.net/api/v1而实际应该用https://taotoken.net/api。地址不对请求打到错误端点也会返回 401 或 404。确认三件套Base URL、Key、Model ID 三者都对得上。local proxy failed / connection refused。这个通常是你本地配了某个代理端口但代理没起来或者环境变量里残留了HTTP_PROXY、HTTPS_PROXY指向一个不存在的地址。检查env | grep -i proxy把不该有的清掉。Claude Code 走 TaoToken 是直连 API不需要额外代理层残留的代理配置反而会捣乱。reading choices 相关报错。这类多半是响应格式和预期不符常见于 Model ID 填错服务端返回了非预期结构。回到 https://taotoken.net/models 核对模型 ID确保和你配置里写的一字不差。模型名大小写、版本号后缀都要对。OAuth 相关报错。如果你之前用官方账号登录过 Claude Code本地可能残留了 OAuth 凭证和现在的 Token 方式冲突。清理~/.claude下的凭证缓存或者显式用环境变量覆盖让 Claude Code 走 Token 而不是 OAuth。环境变量的优先级通常高于缓存凭证配好ANTHROPIC_AUTH_TOKEN后重启一般能解决。排查时有个通用技巧把 Claude Code 的日志级别调高或者在 hook 脚本里把输入输出都打到日志文件先确认请求到底有没有发出去、发出去了返回什么。很多hook 不生效其实是接入层就没通hook 根本没机会跑。如果你在排查接入问题时需要看更细的文档接入文档在 https://taotoken.net/doc 里面有各语言的调用示例和字段说明。对照着核对你配置里的字段名能省不少时间。6. 把 Hooks 用起来从验证到长期自动化基础跑通后可以往实用方向走。几个我常用的场景。审计日志在 PreToolUse 和 PostToolUse 都挂一个脚本把每次工具调用的时间、工具名、参数追加到日志文件。这样你能回溯 Claude 到底干了什么出问题时有据可查。自动 lintPostToolUse 匹配 Write 和 Edit脚本里对改动的文件跑 eslint 或 ruff发现问题就输出到 stderr。虽然 PostToolUse 不阻止操作但错误信息会显示出来你能及时看到。环境准备SessionStart 事件触发时跑一个脚本检查依赖、拉取最新配置、设置环境变量。$CLAUDE_ENV_FILES这个变量就是 SessionStart 专用的用来持久化环境变量。异步执行耗时任务比如跑完整测试套件可以在配置里加async: true让 hook 在后台跑不阻塞 Claude 的主流程。适合那种跑完通知我而不是必须等结果的场景。基于 LLM 的判断type: prompt让模型自己判断某个操作是否合规比如检查这个文件修改是否符合代码规范。这比写死规则灵活但会多一次模型调用成本和延迟都要考虑。写 hook 有个原则脚本要幂等、要快、要能独立测试。别在 hook 里做重活它跑在关键路径上慢了会拖累整个交互。先在命令行手动喂 JSON 测脚本确认逻辑对了再挂到配置里。最后提醒一句hook 改完一定要重启 Claude Code它是会话启动时加载的热改不生效。这个坑我踩过不止一次改了半天以为逻辑错了其实是没重启。养成改配置 → 重启 → 验证的习惯能省很多无效排查。