让 Claude 自己管自己:用 Hooks 拦截 rm -rf 的那一次救场,TaoToken 统一 Key 通道实测 1. 提示词拦不住 rm -rf我差点把项目删没了先说结论Claude Code 的 Hooks 里PreToolUse 是唯一能在命令真正落到 shell 之前把它拦下来的机制。你可以在 CLAUDE.md 里写一百遍“禁止 rm -rf”模型也会乖乖回你“好的我会注意”但它在生成 token 的那一刻并不保证遵守。确定性的事必须交给确定性机制。我踩过的坑很典型。当时让 Claude 清理 /tmp 下的测试产物它把rm -rf /tmp/*顺手扩成了rm -rf /*。项目从 git 历史里捞了三天才恢复。事后我搜了两个小时 issue 才想明白提示词是自然语言约束本质是“拜托模型别这么做”而 Hooks 是 Harness 能力发生在工具真正执行之前由 Claude Code 运行时触发模型根本碰不到这一层。这篇就还原那次救场用 PreToolUse Hook 拦截危险命令给出可复制的 settings.json 配置片段和触发验证命令再说明怎么通过 TaoToken 统一 Key/API 通道管理多工具调用最后用一次模拟删除动作验证拦截是否生效。适合已经在用 Claude Code、想让 Agent 跑命令但心里没底的人。Hooks 挂的事件分两类。会话级管生命周期SessionStart、SessionEnd、Stop。工具调用级管每一次工具执行PreToolUse 在执行前拦截能 allow/deny、PostToolUse 在执行后回调、SubagentStart/SubagentStop 管子智能体。我这次救场用的就是 PreToolUse——在 Bash 命令真正敲进 shell 之前先过一遍我的脚本。配置分散在六个层级企业级策略 → 用户级~/.claude/settings.json→ 项目级.claude/settings.json→ 本地级.claude/settings.local.json→ Skill 的 frontmatter → 命令行参数。越具体优先级越高。我把危险命令拦截放在项目级团队共享把本地敏感路径放在 local不上库。处理器类型有三档。command 跑 shell 脚本规则硬编码最快最确定拦 rm -rf 就用它。prompt 是单次 LLM 评估灵活但有延迟、有不确定性适合“这段代码有没有安全漏洞”这种模糊判断。agent 起子智能体多轮验证最强但最慢。我的原则是能用 command 解决的绝不请 LLM你拦危险命令总不希望拦截器自己抽风吧。2. TaoToken 前置统一 Key 通道多工具一套凭证在写 Hook 脚本之前先把 Key 通道理顺。我同时用 Claude Code、Cline、Codex 几个工具早期每个工具配一套 Key改一次要翻四五个配置文件还容易把生产 Key 和测试 Key 搞混。后来统一走 TaoToken 的 API 通道一个 Key 管所有工具调用配置只改一处。TaoToken 在这里的角色是统一入口它提供兼容的 Base URL 和 KeyClaude Code、Cline、Codex 这些工具都指向同一个地址。官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 这个不加 UTM。注意它是正常的 API 通道服务不是什么灰色中转配置方式就是标准的 Base URL Key Model ID 三件套。为什么 Hook 场景下要强调统一 Key因为 Hook 脚本里经常要调模型做二次判断比如 prompt 类型的处理器如果每个脚本各配一套凭证密钥就散落在各个 .sh 文件里既难轮换也容易泄漏。统一到 TaoToken 之后脚本只读环境变量Key 轮换时改一处即可。具体操作先去控制台创建 Key地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。创建完把 Key 写进环境变量别硬编码进脚本。我习惯在~/.zshrc或~/.bashrc里加一行export TAOTOKEN_API_KEYsk-你的key export ANTHROPIC_BASE_URLhttps://taotoken.net/api export ANTHROPIC_API_KEY$TAOTOKEN_API_KEYClaude Code 读的是ANTHROPIC_BASE_URL和ANTHROPIC_API_KEY这两个环境变量。设好之后Claude Code 的所有请求都走 TaoToken 通道。如果你用 Cline 或 Codex也是同样的 Base URL只是配置文件的字段名不同。Codex 的auth.json里对应的是OPENAI_API_KEY和OPENAI_BASE_URLCline 的 MCP 配置里则是baseUrl和apiKey。模型 ID 这块Claude Code 默认用claude-sonnet-4-5这类标识你在 TaoToken 的模型列表里选对应的即可。想先验证通道通不通可以直接用模型对话页面发一条测试消息地址是 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。能正常返回就说明 Key 和 Base URL 没问题再往下配 Hook。如果你打算长期跑编码 Agent、频繁调用可以考虑 Coding Plan地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 比按次计费更适合高频场景。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各工具的完整配置示例。环境变量设好之后验证一下echo $ANTHROPIC_BASE_URL # 应输出 https://taotoken.net/api echo $ANTHROPIC_API_KEY | head -c 8 # 应输出 sk- 开头的前几位这一步别跳过。我见过有人环境变量写在.bash_profile里但用的是 zsh结果 Claude Code 读不到报 401 还以为是 Key 失效。确认 shell 和配置文件对应上再继续。3. 可复制配置settings.json 挂上 PreToolUse 拦截现在进入正题。先建目录结构在项目根下mkdir -p .claude/hooks .claude/logs touch .claude/hooks/block-dangerous.sh chmod x .claude/hooks/block-dangerous.sh拦截脚本block-dangerous.sh的完整内容#!/bin/bash # .claude/hooks/block-dangerous.sh set -e INPUT$(cat) # 提取命令调试信息输出到 stderr COMMAND$(echo $INPUT | jq -r .tool_input.command // ) echo DEBUG: Checking command: $COMMAND 2 DANGEROUS_PATTERNS( rm -rf / rm -rf ~ rm -rf \$HOME /dev/sd mkfs. :(){:|:};: chmod -R 777 / git push --force origin main git push --force origin master git reset --hard origin DROP DATABASE DROP TABLE TRUNCATE curl.*| sh curl.*| bash ) for pattern in ${DANGEROUS_PATTERNS[]}; do if [[ $COMMAND *$pattern* ]]; then echo BLOCKED: $pattern 2 cat EOF { hookSpecificOutput: { hookEventName: PreToolUse, permissionDecision: deny, permissionDecisionReason: 拦截危险命令模式: $pattern } } EOF exit 2 fi done echo {hookSpecificOutput:{hookEventName:PreToolUse,permissionDecision:allow}} exit 0几个关键点。第一调试信息必须走 stderr2别走 stdout。stdout 是 Hook 的返回值通道混进 debug 日志会导致 JSON 解析失败整条命令静默放行——这个坑我躺过半天明明拦了却没生效排查半天才发现是日志污染了 stdout。第二exit 2表示阻断Claude Code 看到这个码就知道要拦exit 0是放行。第三fork bomb 那个:(){:|:};:别在终端手贱试写进模式列表就行。接下来是settings.json。项目级配置放在.claude/settings.json{ hooks: { PreToolUse: [ { matcher: Bash, hooks: [ { type: command, command: ./.claude/hooks/block-dangerous.sh } ] }, { matcher: Write|Edit, hooks: [ { type: command, command: ./.claude/hooks/protect-files.sh } ] } ], PostToolUse: [ { matcher: *, hooks: [ { type: command, command: ./.claude/hooks/audit-log.sh } ] } ] } }matcher用Bash只匹配命令执行Write|Edit匹配文件写入*是通配。注意路径是相对项目根的Claude Code 会在项目目录下执行。如果你把脚本放在别处用绝对路径或$CLAUDE_PROJECT_DIR变量。PostToolUse 的审计脚本audit-log.sh#!/bin/bash # .claude/hooks/audit-log.sh INPUT$(cat) LOG_DIR${CLAUDE_PROJECT_DIR:-.}/.claude/logs mkdir -p $LOG_DIR LOG_FILE$LOG_DIR/audit-$(date %Y-%m-%d).log TIMESTAMP$(date -Iseconds) TOOL_NAME$(echo $INPUT | jq -r .tool_name // unknown) TOOL_INPUT$(echo $INPUT | jq -c .tool_input // {}) echo [$TIMESTAMP] $TOOL_NAME: $TOOL_INPUT $LOG_FILE echo {} exit 0CLAUDE_PROJECT_DIR是 Claude Code 注入的环境变量指向项目根。日志按天切分出事了一 grep 就能定位。记得把.claude/logs/加进.gitignore不然每天 diff 全是日志噪音。Stop Hook 做测试质量门控run-tests.sh#!/bin/bash # .claude/hooks/run-tests.sh INPUT$(cat) STOP_ACTIVE$(echo $INPUT | jq -r .stop_hook_active // false) if [ $STOP_ACTIVE true ]; then exit 0 fi if [ -n $CLAUDE_PROJECT_DIR ]; then cd $CLAUDE_PROJECT_DIR fi TEST_PASSEDtrue TEST_RESULT if [ -f package.json ] grep -q test package.json; then TEST_RESULT$(npm test 21) || TEST_PASSEDfalse elif [ -f pyproject.toml ] || [ -f pytest.ini ]; then TEST_RESULT$(pytest 21) || TEST_PASSEDfalse elif [ -f go.mod ]; then TEST_RESULT$(go test ./... 21) || TEST_PASSEDfalse else echo {hookSpecificOutput:{additionalContext:未检测到测试框架}} exit 0 fi if [ $TEST_PASSED true ]; then echo {hookSpecificOutput:{additionalContext:所有测试通过}} else TEST_ESCAPED$(echo $TEST_RESULT | head -50 | jq -Rs .) cat EOF { decision: block, reason: 测试失败请修复后再停止, hookSpecificOutput: { additionalContext: $TEST_ESCAPED } } EOF fi exit 0stop_hook_active检查是必须的。Stop Hook block 之后 Claude 会继续干干完又触发 Stop又 block死循环。Claude Code 在第二次触发时带stop_hook_active: true这时必须放行。这个防死循环逻辑漏了我机器风扇转了二十分钟才发现CPU 满载跑测试套件一轮又一轮。4. 验证请求模拟删除动作看拦截是否生效配置写完得验证。别直接拿真项目试先建个沙箱目录mkdir -p /tmp/hook-test cd /tmp/hook-test git init echo test important.txt然后启动 Claude Code让它执行一条危险命令。你可以直接在对话里说“帮我清理一下 /tmp 下的临时文件用 rm -rf”。Claude 会生成类似rm -rf /tmp/*的命令PreToolUse Hook 应该拦下来。预期结果Claude Code 显示命令被拒绝理由是“拦截危险命令模式: rm -rf /”。同时你的终端 stderr 会打印DEBUG: Checking command: rm -rf /tmp/*和BLOCKED: rm -rf /。如果想不启动 Claude Code 直接测脚本可以手动喂 JSONecho {tool_name:Bash,tool_input:{command:rm -rf /}} | ./.claude/hooks/block-dangerous.sh echo exit code: $?预期输出DEBUG: Checking command: rm -rf / BLOCKED: rm -rf / { hookSpecificOutput: { hookEventName: PreToolUse, permissionDecision: deny, permissionDecisionReason: 拦截危险命令模式: rm -rf / } } exit code: 2再测一条安全命令echo {tool_name:Bash,tool_input:{command:ls -la}} | ./.claude/hooks/block-dangerous.sh echo exit code: $?预期输出{hookSpecificOutput:{hookEventName:PreToolUse,permissionDecision:allow}}exit code 0。验证 PostToolUse 审计日志随便让 Claude 跑一条命令然后看.claude/logs/audit-$(date %Y-%m-%d).log应该有一行记录。验证 Stop Hook故意让测试失败看 Claude 是否被 block 回去继续修。这里有个细节Hook 脚本的路径。如果你在settings.json里写./.claude/hooks/block-dangerous.shClaude Code 是从项目根解析的。但如果你在子目录启动 Claude Code路径可能不对。稳妥做法是用$CLAUDE_PROJECT_DIR{ type: command, command: $CLAUDE_PROJECT_DIR/.claude/hooks/block-dangerous.sh }验证通过后把配置提交到 git团队共享。本地敏感路径的拦截放.claude/settings.local.json这个文件加进.gitignore。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth配 Hook 和 TaoToken 通道时我踩过的报错基本集中在这几类。逐个说。401 Unauthorized。最常见。先查环境变量echo $ANTHROPIC_API_KEY echo $ANTHROPIC_BASE_URL如果 Key 是空的说明 shell 配置文件没生效。zsh 用户检查~/.zshrcbash 用户检查~/.bashrc改完记得source或重开终端。如果 Key 有值但还是 401去 TaoToken 控制台确认 Key 没过期、没被删。还有一种情况Key 复制时带了空格或换行echo $ANTHROPIC_API_KEY | wc -c看长度对不对。local proxy failed。这个报错通常出现在你本地起了代理但没配对或者 Base URL 写成了http://localhost:xxxx但本地服务没启动。检查ANTHROPIC_BASE_URL是不是https://taotoken.net/api别写成别的。如果你之前配过本地代理工具先把相关环境变量清掉unset HTTP_PROXY HTTPS_PROXY http_proxy https_proxy然后重开终端再试。reading choices 相关报错。这个一般出现在流式响应解析时返回体格式不对。原因可能是 Base URL 少了/api后缀或者模型 ID 写错了。确认ANTHROPIC_BASE_URLhttps://taotoken.net/api模型 ID 用 TaoToken 文档里列出的。如果还报错去模型对话页面发一条消息看能不能正常返回能返回说明通道没问题问题在 Claude Code 的配置。OAuth 相关报错。Claude Code 某些版本会尝试 OAuth 登录如果你用的是 API Key 模式需要确保没走 OAuth 流程。检查~/.claude.json或项目下的.claude/settings.json看有没有残留的 OAuth 配置。有的话删掉只保留 API Key 相关字段。Codex 的auth.json里如果混了 OAuth token 和 API Key也会冲突清空重写{ OPENAI_API_KEY: sk-你的key, OPENAI_BASE_URL: https://taotoken.net/api }Hook 不生效。先确认脚本有执行权限ls -l .claude/hooks/block-dangerous.sh没有x就chmod x。再确认settings.json的 JSON 格式没错用jq . .claude/settings.json验证。然后手动喂 JSON 测脚本看 exit code 对不对。最后确认 Claude Code 是从项目根启动的路径解析才对。Hook 拦了但 Claude 还是执行了。检查 stdout 有没有被 debug 日志污染。所有echo调试信息必须走2。另外确认exit 2在正确的位置别被set -e提前中断了。Stop Hook 死循环。检查stop_hook_active逻辑第二次触发必须exit 0。如果还是循环看run-tests.sh里有没有别的exit语句提前返回了非零码。排查顺序建议先验证 TaoToken 通道模型对话页面发消息再验证 Hook 脚本手动喂 JSON最后验证 Claude Code 集成实际对话触发。分层排查别一上来就怀疑最复杂的部分。6. 把确定性红线交给确定性机制写到这里回头看提示词和 Hooks 不是二选一是分工。提示词管“该做什么”意图层Hooks 管“不能做什么”执行层。把确定性的红线交给确定性的机制模型再聪明也越不过去——这才是 Harness 该干的事。我现在的习惯是任何让 Agent 跑命令的项目先配 PreToolUse 拦截再配 PostToolUse 审计Stop Hook 做质量门控。三件套配齐心里才踏实。TaoToken 统一 Key 通道之后多工具切换不用改配置Key 轮换也只改一处。如果你还没配 TaoToken 通道先去 API Keys 页面创建一个https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有 Claude Code、Cline、Codex 的完整配置示例。长期跑编码 Agent 的话Coding Plan 更划算https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。最后留个问题当你的 Hook 脚本本身写错了谁来拦 Hook我的做法是 Hook 脚本也进 git改之前先手动喂 JSON 测一遍别直接上生产。脚本里的模式列表定期 review新踩的坑补进去。这套机制跑顺之后让 Claude 自己管自己你只需要管好 Hook。