1. 为什么要把 Claude Code 工具调用日志统一收口
Claude Code 是 Anthropic 推出的命令行 AI 编码代理,它能在你的终端里自主读写文件、跑 bash、调工具,把多步骤编码任务串起来。Hooks 是它从 2025 年 GA 版本开始提供的扩展机制:在工具调用前后插入你自己写的 shell 脚本,做拦截、审计、通知。问题也随之而来——当团队里多个人、多台机器、多个项目都在跑 Claude Code,工具调用日志散落在各自的~/.claude/logs/和临时文件里,没人能回答"昨天到底有哪些命令被 AI 执行过"。
我试过最原始的做法:每个项目配一个 PostToolUse hook,把命令追加到本地 jsonl。结果三台开发机、两个 CI runner,日志格式还不一样,排查一次线上误删事件翻了半小时。核心矛盾在于:Hooks 本身只负责"触发脚本",它不负责"把日志送到一个统一的地方"。你需要一个稳定的、带鉴权的、可观测的出口,让所有 hook 脚本把结构化日志发到同一个通道。
TaoToken 在这里扮演的角色就是那个统一出口。它提供兼容 OpenAI 风格与 Anthropic 风格的 API 端点,你可以把 Claude Code 的模型请求和 hook 产生的观测数据都指向同一个 Base URL,用同一把 Key 做鉴权。这样做的直接好处是:请求经统一通道发出,日志完整可查,不用在每台机器上维护不同的上报地址和密钥。
适合谁看:已经在用 Claude Code CLI、需要给 AI 代理行为加审计或合规层的开发者;团队里有多人共用 Claude Code、想把工具调用日志集中观测的工程负责人;以及想搞清楚 Hooks 的 stdin/stdout/exit code 到底怎么落地的人。下面从配置到验证一步步来,命令都能直接复制。
2. TaoToken 前置准备与 Claude Code 接入配置
在写 hook 之前,先把 Claude Code 的模型请求通道切到 TaoToken,否则你的工具调用日志和模型请求会走两条不同的路,观测就断了。这一步的核心是三件套:Base URL、API Key、Model ID。
先拿 Key。打开 https://taotoken.net/api-keys ,登录后创建一个 API Key,复制出来形如sk-xxxxxxxx。这个 Key 同时用于模型请求和后续 hook 上报的鉴权头。注意不要把它硬编码进会提交 git 的文件,用环境变量。
接着确认 Base URL。TaoToken 的 API 入口是https://taotoken.net/api,注意这里不带任何查询参数。Claude Code 走 Anthropic 兼容协议时,需要把ANTHROPIC_BASE_URL指向它。你可以先在 shell 里验证连通性:
export TAOTOKEN_API_KEY="sk-你的key" export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_API_KEY="$TAOTOKEN_API_KEY" curl -s "$ANTHROPIC_BASE_URL/v1/messages" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-sonnet-4-20250514", "max_tokens": 64, "messages": [{"role":"user","content":"reply with ok"}] }'如果返回里带content字段,说明通道通了。Model ID 要写你账号下可用的具体模型名,别照抄,去 https://taotoken.net/doc 查当前支持的列表。
然后配置 Claude Code 本身。Claude Code 读取~/.claude/settings.json作为用户级配置,项目级在<project>/.claude/settings.json,本地覆盖在<project>/.claude/settings.local.json,优先级从低到高。把模型通道和 hooks 一起写进去:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的key" }, "hooks": [ { "event": "PostToolUse", "matcher": { "tool_name": "Bash" }, "command": "/usr/local/bin/taotoken-audit.sh", "timeout": 5000 } ] }这里env块让 Claude Code 启动时自动带上 TaoToken 的地址和 Key,hooks块先放一个 PostToolUse 审计脚本占位。注意event字段大小写敏感,写错会静默失效。matcher.tool_name只对 Bash 触发,避免所有工具调用都跑一遍脚本拖慢速度。
如果你用的是 Claude Code 的 coding plan 模式或想长期跑 Agent 任务,建议单独开一个 Coding Plan 配额,把观测流量和日常对话流量分开,方便按项目核算。入口在 https://taotoken.net/coding-plan 。
配置写完先别急着跑,用jq . ~/.claude/settings.json验证 JSON 语法,语法错会导致整个配置被忽略,这是最常见的"配了没反应"原因。
3. 可复制的 Hooks 配置与 endpoint 改写片段
这一节给你能直接落地的完整片段。核心思路:PreToolUse 做拦截和鉴权头注入,PostToolUse 把工具调用结果上报到 TaoToken 统一通道,Stop 做任务完成通知。三个 hook 脚本都通过环境变量读取 TaoToken 的地址和 Key,不硬编码。
先看 PreToolUse 拦截脚本,它负责在危险命令执行前拦下来,同时把这次调用的元信息带上鉴权头预登记:
#!/bin/bash # /usr/local/bin/taotoken-pre-guard.sh # 运行环境:bash 5.x,jq 1.6+ set -uo pipefail INPUT=$(cat) COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""') SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"') DANGEROUS=("rm -rf /" "dd if=/dev/zero" "> /etc/passwd" "chmod -R 777 /") for p in "${DANGEROUS[@]}"; do if echo "$COMMAND" | grep -qF "$p"; then echo "{\"decision\":\"block\",\"reason\":\"blocked pattern: $p\"}" exit 0 fi done # 预登记到 TaoToken 统一通道(异步,不阻塞) curl -s --max-time 3 -X POST "$TAOTOKEN_API_BASE/v1/audit/pre" \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "content-type: application/json" \ -d "{\"session\":\"$SESSION_ID\",\"phase\":\"pre\",\"cmd\":$(echo "$COMMAND" | jq -R .)}" \ >/dev/null 2>&1 & echo "{\"decision\":\"approve\"}" exit 0PostToolUse 上报脚本,把工具名、输入、输出、时间戳打成一条结构化日志发到统一通道:
#!/bin/bash # /usr/local/bin/taotoken-audit.sh set -uo pipefail INPUT=$(cat) SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"') TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "unknown"') TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}') TOOL_OUTPUT=$(echo "$INPUT" | jq -c '.tool_output // {}') TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ") PAYLOAD=$(jq -n \ --arg ts "$TS" \ --arg sid "$SESSION_ID" \ --arg tool "$TOOL_NAME" \ --argjson input "$TOOL_INPUT" \ --argjson output "$TOOL_OUTPUT" \ '{ts:$ts, session:$sid, tool:$tool, input:$input, output:$output}') curl -s --max-time 3 -X POST "$TAOTOKEN_API_BASE/v1/audit/post" \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "content-type: application/json" \ -d "$PAYLOAD" >/dev/null 2>&1 & exit 0对应的 settings.json 完整片段,注意 endpoint 已经改写成 TaoToken 统一通道:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的key", "TAOTOKEN_API_BASE": "https://taotoken.net/api", "TAOTOKEN_API_KEY": "sk-你的key" }, "hooks": [ { "event": "PreToolUse", "matcher": { "tool_name": "Bash" }, "command": "/usr/local/bin/taotoken-pre-guard.sh", "timeout": 5000 }, { "event": "PostToolUse", "matcher": { "tool_name": "Bash" }, "command": "/usr/local/bin/taotoken-audit.sh", "timeout": 5000 }, { "event": "Stop", "command": "/usr/local/bin/taotoken-stop-notify.sh", "timeout": 3000 } ] }Stop 通知脚本可以简单到只发一条完成事件:
#!/bin/bash # /usr/local/bin/taotoken-stop-notify.sh set -uo pipefail INPUT=$(cat) SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"') curl -s --max-time 3 -X POST "$TAOTOKEN_API_BASE/v1/audit/stop" \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "content-type: application/json" \ -d "{\"session\":\"$SESSION_ID\",\"phase\":\"stop\"}" >/dev/null 2>&1 & exit 0三个脚本都要chmod +x。注意TAOTOKEN_API_BASE和ANTHROPIC_BASE_URL都指向https://taotoken.net/api,这样模型请求和审计上报走同一个域名,鉴权头格式统一,排查时只看一个出口。如果你在 Cline 或 CC Switch 里也配了 Claude Code,记得把这三件套(Base URL、Key、Model ID)同步过去,否则不同客户端会走不同通道,观测又断了。
4. 验证一次工具调用日志是否经统一通道发出
配置写完必须验证,否则你不知道 hook 到底有没有触发、请求有没有真的到 TaoToken。分三步:单元测试脚本、干跑一次真实工具调用、查统一通道的日志。
第一步,直接 pipe 一个构造的 HookInput 给脚本,看 exit code 和 stdout:
echo '{"session_id":"test-001","tool_name":"Bash","tool_input":{"command":"ls -la"},"tool_output":{"stdout":"total 0"}}' \ | /usr/local/bin/taotoken-audit.sh echo "exit code: $?"期望输出是exit code: 0,且没有报错。如果脚本里 curl 的地址或 Key 没配好,这里不会报错(因为异步且吞了输出),所以第二步要抓真实请求。
第二步,在测试项目里启动 Claude Code,让它执行一条无害命令:
cd /tmp/hook-test claude "run ls -la and tell me what you see"Claude Code 会调用 Bash 工具,触发 PreToolUse 和 PostToolUse。观察终端有没有 hook 报错。同时开另一个窗口 tail 你的调试日志:
tail -f /tmp/hook-debug.log在脚本里加一行echo "[$(date -u +%T)] $TOOL_NAME triggered" >> /tmp/hook-debug.log就能看到触发记录。
第三步,去 TaoToken 控制台查统一通道的日志。打开 https://taotoken.net/console ,在请求日志里筛选刚才的时间段,你应该能看到两类记录:一类是 Claude Code 的模型请求(走/v1/messages),一类是 hook 上报的审计事件(走/v1/audit/post)。如果只看到模型请求、没有审计事件,说明 hook 脚本没触发或上报失败。
验证成功的标志:控制台里同一个 session_id 下,模型请求和工具调用审计事件时间线对齐,工具名、命令、输出都能对上。这时候你就确认了请求经 TaoToken 统一通道发出且日志完整可查。
想快速验证模型通道本身是否正常,可以用模型对话页面发一条测试消息,看返回是否正常,入口在 https://taotoken.net/models 。这一步和 hook 验证分开做,避免两个问题混在一起排查。
5. 本篇常见报错与排查对照
配 Hooks 最容易踩的坑集中在鉴权、代理、JSON 解析和 OAuth 四类。下面按真实报错对照排查。
401 Unauthorized。现象是模型请求或审计上报返回 401。原因通常是 Key 没带上、带错、或环境变量没生效。排查:echo $TAOTOKEN_API_KEY确认非空;检查 settings.json 的env块里 Key 拼写;确认请求头是Authorization: Bearer还是x-api-key,Anthropic 兼容端点用x-api-key,审计端点用Authorization。改完重启 Claude Code,环境变量在启动时读取。
local proxy failed / connection refused。现象是 Claude Code 报连不上本地代理。这通常是因为你之前配过HTTP_PROXY或HTTPS_PROXY指向了本地端口,而那个服务没开。排查:env | grep -i proxy看有没有残留代理变量,有就unset掉。注意 TaoToken 的地址是直连的,不需要任何本地转发层,把代理变量清干净反而更稳。
reading choices: unexpected end of JSON input。现象是 hook 脚本报 JSON 解析失败。原因是 stdin 读到的不是合法 JSON,或者jq没装。排查:which jq确认存在;在脚本开头加INPUT=$(cat)后先echo "$INPUT" >> /tmp/raw-input.log看原始内容;如果 Claude Code 传的是空 stdin,jq会报错,用jq -r '.tool_name // "unknown"'加默认值兜底。
OAuth token expired / authentication failed。现象是 Claude Code 提示 OAuth 相关错误。这通常是你之前用 Anthropic 官方账号登录过,本地缓存了 OAuth token,和现在的 API Key 模式冲突。排查:检查~/.claude/下有没有旧的凭据缓存文件,清掉后重新用 API Key 模式启动。确认 settings.json 里没有残留的 OAuth 配置项。
Hook 静默失效,什么都不发生。现象是配了 hook 但脚本从没被调用。三个原因:settings.json JSON 语法错(用jq .验证);event字段大小写错(必须是PreToolUse/PostToolUse/Stop);脚本路径不存在或无执行权限(chmod +x)。逐个排除。
Hook 超时导致所有工具调用被阻断。现象是每次 Bash 都失败报 timeout。原因是脚本里有同步网络请求或等待输入。排查:把 curl 改成异步(末尾加&),或加--max-time 3;适当调大timeout但别超过 30000ms。记住 Hooks 是 fail-closed 的,超时默认阻断,所以脚本必须快速返回。
排查时如果拿不准是模型通道问题还是 hook 问题,先单独用 curl 测模型端点,再单独 pipe 测 hook 脚本,把两个变量分开。接入文档在 https://taotoken.net/doc 有完整的端点和鉴权说明,遇到 401 先对一遍请求头格式。
6. 把观测通道固定下来,后续才好扩展
走到这里,你已经有了一个能跑的闭环:Claude Code 的模型请求走 TaoToken,工具调用日志通过 Hooks 上报到同一个通道,控制台里能看到完整时间线。接下来真正省事的地方在于扩展——当你想加新的拦截规则,只需要改 PreToolUse 脚本里的模式列表;想加新的观测维度,改 PostToolUse 的 payload 字段;想换上报目标,改一个环境变量。
一个实用技巧:把危险模式列表和上报地址都抽到单独的配置文件里,脚本只读配置,这样运维更新规则不用动脚本逻辑,也不用重新chmod。另一个技巧是给 hook 脚本加一个DRY_RUN环境变量,测试时只打印不上报,避免污染生产日志。
如果你团队里多人共用,建议每人用自己的 API Key,在 TaoToken 控制台按 Key 维度看用量和日志,这样能区分是谁的会话触发了什么工具调用。长期跑 Agent 任务的话,Coding Plan 的配额和观测是分开的,按项目分配更清晰。
最后提醒一句:Hooks 脚本本身是受信任的扩展点,它在你的用户权限下运行,能读密钥、能发网络请求。企业部署时对脚本内容做代码审查,用最小权限原则跑,别把 Key 硬编码进会提交的文件。观测通道固定下来之后,这些治理动作才有统一的落点。