1. 为什么你的 Claude Code 需要一个“刹车片”
上周我让 Claude Code 帮我重构一个模块,它很贴心地执行了rm -rf dist/来清理构建产物。问题是,那个目录里还有一份我手动调试时放进去的配置文件——没有提交到 Git,直接没了。这不是 Claude 的错,它按照正常的工程流程执行了清理操作。但作为人类,我希望有一种机制能在危险操作执行之前就拦住它,就像 Git 的 pre-commit hook 一样,让自动化流程在关键节点停下来,先检查再执行。
Claude Code Hooks 就是干这个的。它允许你在 Agent 的生命周期中插入自定义的 Shell 脚本、HTTP 请求甚至 LLM 判断,实现从“信任 Agent”到“信任但验证”的转变。如果你写过 Spring 的 AOP 或者用过 Git Hooks,这个概念你一秒就懂:在 Agent 执行特定操作的前后,自动触发你定义的逻辑。整个生命周期里,最核心的事件包括SessionStart(对话开始/恢复)、PreToolUse(工具执行前拦截)、PostToolUse(工具执行后处理)、PermissionRequest(权限弹窗时)、Stop(Agent 结束响应)以及UserPromptSubmit(用户提交 prompt)。其中PreToolUse是最常用的,90% 的“护栏”需求都在这里实现。
这篇文章不讲概念,直接给 6 个生产可用的 Hook 场景,每个都附完整脚本和配置。同时,我会演示如何把 endpoint 改到 TaoToken 统一 Key/API 通道,让你在团队协作中用一个 Key 管理所有 Claude Code 实例的调用。看完直接能用。
2. TaoToken 前置:统一 Key 与 API 通道接入
在开始写 Hook 脚本之前,我们需要先解决一个基础设施问题:Claude Code 的 API 调用通道。默认情况下,Claude Code 会直连 Anthropic 的官方端点。但在团队协作或生产环境中,你可能希望统一管理 Key、统一计费、统一审计。TaoToken 提供了兼容 Anthropic API 的通道,你可以把 Claude Code 的 endpoint 指向 TaoToken,用一个 Key 管理所有实例。
首先,你需要获取一个 TaoToken API Key。访问 TaoToken 控制台 创建一个 Key。然后,在 Claude Code 的配置中设置环境变量。Claude Code 支持通过ANTHROPIC_BASE_URL和ANTHROPIC_API_KEY来覆盖默认端点。
# 在 ~/.bashrc 或 ~/.zshrc 中添加 export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_API_KEY="sk-your-taotoken-key"如果你使用的是 Claude Code 的 settings 文件,也可以在~/.claude/settings.json中配置:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-your-taotoken-key" } }这样配置后,Claude Code 的所有 API 请求都会经过 TaoToken 通道。你可以在 TaoToken 模型对话 页面验证 Key 是否生效,或者直接在 Claude Code 里发一条消息测试。
对于长期编码和 Agent 场景,建议使用 Coding Plan,它提供了更稳定的配额和更低的延迟。如果你需要查看详细的接入文档,可以参考 TaoToken 文档。
配置完成后,你可以用以下命令验证通道是否通畅:
curl -s https://taotoken.net/api/v1/models \ -H "Authorization: Bearer $ANTHROPIC_API_KEY" | jq '.data[].id' | head -5如果返回了模型列表,说明通道正常。接下来,我们就可以在这个基础上配置 Hooks 了。
3. 可复制配置:6 个生产场景的 Shell 脚本与 settings 片段
Hooks 的配置是一个 JSON 对象,放在 Claude Code 的 settings 文件里。根据你的需求,有三个位置可选:~/.claude/settings.json(全局,所有项目生效)、.claude/settings.json(项目级,随代码提交,团队共享)、.claude/settings.local.json(本地,不提交,只对自己生效)。推荐做法是:通用的安全策略放全局,项目特定的放.claude/settings.json提交到仓库。
配置的基本结构是三层嵌套:事件名 → 匹配器 → 处理器数组。matcher用正则匹配工具名,比如Bash只拦截命令行操作,Edit|Write拦截文件修改,mcp__.*拦截所有 MCP 工具调用。
3.1 场景一:拦截危险 Shell 命令
这是最高频的需求。创建一个脚本,拦截rm -rf、DROP TABLE、git push --force等危险操作。
#!/bin/bash # .claude/hooks/block-dangerous-commands.sh INPUT=$(cat) COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') DANGEROUS_PATTERNS=( 'rm\s+-rf\s+/' 'git\s+push\s+.*--force' 'DROP\s+TABLE' 'DROP\s+DATABASE' 'git\s+reset\s+--hard' '>\s*/dev/sd' ) for pattern in "${DANGEROUS_PATTERNS[@]}"; do if echo "$COMMAND" | grep -iEq "$pattern"; then echo "BLOCKED: 命令匹配危险模式 [$pattern]" >&2 echo "原始命令: $COMMAND" >&2 exit 2 fi done exit 0配置片段:
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous-commands.sh", "timeout": 5 } ] } ] } }踩坑记录:退出码的含义和你想象的不一样。退出码 1 是非阻塞错误(Claude 会忽略并继续执行),退出码 2 才是阻塞错误(Claude 收到拒绝,停止操作)。我第一次写的时候用了exit 1,结果发现 Claude 完全无视了我的拦截逻辑。
3.2 场景二:敏感文件保护
防止 Claude 修改.env、密钥文件、锁文件等你不想被动的文件。
#!/bin/bash # .claude/hooks/protect-sensitive-files.sh INPUT=$(cat) FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') [ -z "$FILE_PATH" ] && exit 0 PROTECTED_PATTERNS=( '\.env$' '\.env\.' 'credentials' 'secret' '\.pem$' '\.key$' 'package-lock\.json$' 'pnpm-lock\.yaml$' 'yarn\.lock$' 'go\.sum$' ) for pattern in "${PROTECTED_PATTERNS[@]}"; do if echo "$FILE_PATH" | grep -iEq "$pattern"; then echo "BLOCKED: 不允许修改敏感文件 $FILE_PATH" >&2 exit 2 fi done exit 0配置片段(注意 matcher 匹配 Edit 和 Write 两个工具):
{ "hooks": { "PreToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-sensitive-files.sh", "timeout": 5 } ] } ] } }3.3 场景三:代码修改后自动 Lint
每次 Claude 编辑完文件,自动跑一遍 linter,把结果反馈给它。这样 Claude 可以在同一轮对话里自动修复格式问题。
#!/bin/bash # .claude/hooks/auto-lint.sh INPUT=$(cat) FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') [ -z "$FILE_PATH" ] && exit 0 case "$FILE_PATH" in *.js|*.ts|*.jsx|*.tsx) RESULT=$(npx eslint --fix "$FILE_PATH" 2>&1) || true ;; *.py) RESULT=$(python -m ruff check --fix "$FILE_PATH" 2>&1) || true ;; *.go) RESULT=$(gofmt -w "$FILE_PATH" 2>&1) || true ;; *.java) RESULT=$(mvn checkstyle:check -pl "$(dirname "$FILE_PATH")" 2>&1 | tail -5) || true ;; *) exit 0 ;; esac if [ -n "$RESULT" ]; then jq -n --arg result "$RESULT" --arg file "$FILE_PATH" '{ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: "Lint 结果 [\($file)]:\n\($result)\n如果有问题请修复。" } }' fi exit 0配置片段(注意这是 PostToolUse,在编辑完成之后触发):
{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/auto-lint.sh", "timeout": 30 } ] } ] } }3.4 场景四:SessionStart 自动注入项目上下文
每次对话启动时,自动加载 Git 状态、最近 issue、当前分支等信息,让 Claude 一进来就有上下文。
#!/bin/bash # .claude/hooks/inject-context.sh BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown") RECENT_COMMITS=$(git log --oneline -5 2>/dev/null || echo "no commits") DIRTY_FILES=$(git diff --name-only 2>/dev/null | head -10) ISSUES=$(gh issue list -L 3 --json title,number --jq '.[] | "#\(.number) \(.title)"' 2>/dev/null || echo "GitHub CLI 不可用") CONTEXT=" 当前分支: $BRANCH 最近 5 次提交: $RECENT_COMMITS 未提交的修改: ${DIRTY_FILES:-无} 最近的 Issues: ${ISSUES:-无} " jq -n --arg ctx "$CONTEXT" '{ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: $ctx } }' exit 0配置片段:
{ "hooks": { "SessionStart": [ { "matcher": "startup", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/inject-context.sh", "timeout": 15 } ] } ] } }这个 Hook 有个细节:SessionStart 的 matcher 可以区分startup(新对话)、resume(恢复对话)和compact(context 压缩后)。只在startup时加载完整上下文,避免 resume 时重复注入。
3.5 场景五:异步审计日志
记录 Claude 执行的所有操作,不阻塞正常流程。关键是"async": true——异步执行,不影响 Agent 速度。
{ "hooks": { "PostToolUse": [ { "hooks": [ { "type": "command", "async": true, "command": "echo \"$(date +%Y-%m-%dT%H:%M:%S) | $(jq -r '.tool_name') | $(jq -r '.tool_input | tostring' | head -c 200)\" >> \"$CLAUDE_PROJECT_DIR\"/.claude/audit.log" } ] } ] } }没有 matcher 意味着所有工具调用都会被记录。输出像这样:
2026-04-08T14:23:01 | Edit | {"file_path":"/src/main/java/Service.java","old_string":"... 2026-04-08T14:23:05 | Bash | {"command":"mvn compile -pl dlm-framework/dlm-rule"} 2026-04-08T14:23:12 | Read | {"file_path":"/src/test/java/ServiceTest.java"}在出问题的时候,这个日志能帮你回溯 Claude 的每一步操作。
3.6 场景六:HTTP Hook 对接外部合规系统
如果你的团队有合规审核系统(比如安全扫描服务),可以用 HTTP Hook 在执行前请求外部 API:
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "http", "url": "http://localhost:8080/api/validate-command", "headers": { "Authorization": "Bearer $COMPLIANCE_TOKEN" }, "allowedEnvVars": ["COMPLIANCE_TOKEN"], "timeout": 10 } ] } ] } }HTTP Hook 会把工具调用的完整 JSON 作为 POST body 发送给你的服务。你的服务返回{"decision": "block", "reason": "..."}就能拦截操作。这在企业环境里特别有用——安全团队可以维护一个中心化的策略服务,所有开发者的 Claude Code 实例都通过 HTTP Hook 对接。
4. 验证请求:一次触发确认 Hook 生效
配置写完了,怎么确认 Hook 真的生效了?最直接的方法是在终端单独测试脚本,然后在 Claude Code 里触发一次真实操作。
首先,测试危险命令拦截脚本:
echo '{"tool_input":{"command":"rm -rf /"}}' | bash .claude/hooks/block-dangerous-commands.sh echo "退出码: $?"如果输出BLOCKED: 命令匹配危险模式 [rm\s+-rf\s+/]并且退出码是 2,说明脚本逻辑正确。
然后,在 Claude Code 里输入/hooks命令,查看所有已加载的 Hook 配置。你应该能看到刚才配置的PreToolUse、PostToolUse等事件。
接下来,让 Claude Code 执行一个危险操作来验证拦截是否生效。比如输入:“帮我清理一下 dist 目录,用 rm -rf”。如果 Hook 生效,Claude 会收到拒绝通知,并告诉你操作被阻止了。
对于自动 Lint 的 Hook,你可以让 Claude 修改一个文件,然后观察它是否自动运行了 linter 并反馈了结果。如果一切正常,Claude 会在修改后自动修复格式问题。
对于审计日志,检查.claude/audit.log文件是否在每次工具调用后追加了新行:
tail -f .claude/audit.log如果日志在实时更新,说明异步 Hook 正常工作。
最后,验证 TaoToken 通道是否在 Hook 执行期间保持稳定。你可以在 TaoToken 模型对话 页面查看请求日志,确认所有 API 调用都经过了统一通道。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
在配置 Hooks 和 TaoToken 通道的过程中,你可能会遇到一些典型错误。这里整理了几个高频问题及其排查方法。
错误一:401 Unauthorized
如果你在 Claude Code 里看到 401 错误,通常是因为 API Key 没有正确设置。检查ANTHROPIC_API_KEY环境变量是否指向了 TaoToken 的 Key,而不是 Anthropic 的官方 Key。你可以在 TaoToken API Keys 页面重新生成一个 Key,然后更新环境变量。
echo $ANTHROPIC_API_KEY # 应该输出 sk- 开头的 TaoToken Key错误二:local proxy failed
这个错误通常出现在你使用了本地代理但配置不正确的情况下。如果你没有使用代理,检查ANTHROPIC_BASE_URL是否被错误地设置为了http://localhost:xxxx。正确的 TaoToken 地址是https://taotoken.net/api。
echo $ANTHROPIC_BASE_URL # 应该输出 https://taotoken.net/api错误三:reading choices 报错
这个错误通常与 HTTP Hook 的响应格式有关。如果你的合规服务返回的 JSON 格式不正确,Claude Code 会报reading choices错误。确保你的服务返回的是:
{ "decision": "block", "reason": "命令被合规策略拦截" }或者:
{ "decision": "allow" }错误四:OAuth 相关错误
Claude Code 在某些版本中会尝试 OAuth 认证。如果你看到 OAuth 错误,检查是否在 settings 中同时配置了ANTHROPIC_API_KEY和 OAuth 相关的字段。建议只保留 API Key 配置,删除 OAuth 相关字段。
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-your-taotoken-key" } }错误五:Hook 脚本没有执行
如果 Hook 脚本没有按预期执行,首先检查脚本是否有可执行权限:
chmod +x .claude/hooks/*.sh然后检查settings.json中的路径是否正确。$CLAUDE_PROJECT_DIR是 Claude Code 注入的环境变量,指向项目根目录。如果你在全局 settings 中配置,确保路径是绝对路径。
错误六:退出码不生效
再次强调:退出码 2 才是阻塞错误,退出码 1 是非阻塞错误。如果你用了exit 1,Claude 会忽略你的拦截逻辑。确保所有拦截脚本都使用exit 2。
6. 语义一致 CTA:从 Hook 到统一 Key 的完整闭环
Hooks 本质上是给 AI Agent 加 middleware。和 Web 开发里的中间件一样,最好的 Hook 是你写完就忘了它存在——它在背后默默工作,只在真正危险的时候跳出来拦你一下。
我个人的最小化配置是三个 Hook:PreToolUse/Bash拦截rm -rf、--force、DROP等危险模式;PreToolUse/Edit|Write保护.env和锁文件;PostToolUse/Edit|Write(async)自动 lint 加审计日志。这三个覆盖了 95% 的“AI 编程事故”场景。剩下的 5% 靠 Git。
而 TaoToken 的统一 Key 通道,则是让这套 Hook 体系在团队中可复制、可管理的基础设施。你不需要在每个开发者的机器上单独配置 Anthropic Key,只需要在 TaoToken 控制台 生成一个 Key,然后通过环境变量或 settings 文件分发。对于长期编码和 Agent 场景,Coding Plan 提供了更稳定的配额和更低的延迟。如果你需要查看详细的接入文档,可以参考 TaoToken 文档。
现在,你可以从最简单的危险命令拦截开始,逐步添加 Lint、审计日志和合规检查。每加一个 Hook,就多一层保护。等你写完第六个 Hook,你会发现 Claude Code 已经从一个“信任的助手”变成了一个“可信但可验证的生产力工具”。