☰
让 Claude 自己管自己:用 Hooks 拦截 rm -rf 的那一次救场,TaoToken 统一 Key 通道实测
2026/10/9 2:15:57 网站建设 项目流程

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 和 Key,Claude Code、Cline、Codex 这些工具都指向同一个地址。官网在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_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_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 。创建完把 Key 写进环境变量,别硬编码进脚本。我习惯在~/.zshrc或~/.bashrc里加一行:

export TAOTOKEN_API_KEY="sk-你的key" export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_API_KEY="$TAOTOKEN_API_KEY"

Claude Code 读的是ANTHROPIC_BASE_URL和ANTHROPIC_API_KEY这两个环境变量。设好之后,Claude Code 的所有请求都走 TaoToken 通道。如果你用 Cline 或 Codex,也是同样的 Base URL,只是配置文件的字段名不同。Codex 的auth.json里对应的是OPENAI_API_KEY和OPENAI_BASE_URL,Cline 的 MCP 配置里则是baseUrl和apiKey。

模型 ID 这块,Claude Code 默认用claude-sonnet-4-5这类标识,你在 TaoToken 的模型列表里选对应的即可。想先验证通道通不通,可以直接用模型对话页面发一条测试消息,地址是 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。能正常返回就说明 Key 和 Base URL 没问题,再往下配 Hook。

如果你打算长期跑编码 Agent、频繁调用,可以考虑 Coding Plan,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,比按次计费更适合高频场景。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有各工具的完整配置示例。

环境变量设好之后,验证一下:

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

几个关键点。第一,调试信息必须走 stderr(>&2),别走 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 0

CLAUDE_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_PASSED=true TEST_RESULT="" if [ -f "package.json" ] && grep -q '"test"' package.json; then TEST_RESULT=$(npm test 2>&1) || TEST_PASSED=false elif [ -f "pyproject.toml" ] || [ -f "pytest.ini" ]; then TEST_RESULT=$(pytest 2>&1) || TEST_PASSED=false elif [ -f "go.mod" ]; then TEST_RESULT=$(go test ./... 2>&1) || TEST_PASSED=false 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 0

stop_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 直接测脚本,可以手动喂 JSON:

echo '{"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.sh,Claude 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 用户检查~/.zshrc,bash 用户检查~/.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_URL=https://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_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有 Claude Code、Cline、Codex 的完整配置示例。长期跑编码 Agent 的话,Coding Plan 更划算:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。

最后留个问题:当你的 Hook 脚本本身写错了,谁来拦 Hook?我的做法是 Hook 脚本也进 git,改之前先手动喂 JSON 测一遍,别直接上生产。脚本里的模式列表定期 review,新踩的坑补进去。这套机制跑顺之后,让 Claude 自己管自己,你只需要管好 Hook。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询