最近在重度使用 Claude Code 写项目时,最让人头疼的不是需求拆解,也不是代码报错,而是“用着用着额度突然没了”。一个长时间任务跑到一半,模型调用直接中断,会话上下文丢失,账单还可能在最后时刻给你一个惊喜。这种体验经历几次之后,我开始思考能不能在真正触顶之前,主动拦住它。于是就有了这篇文章要讲的 AgentObs。
AgentObs 本质上是一个基于 Claude Code hook 机制的本地“用量哨兵”。它不修改模型行为,也不尝试绕过任何平台限制,而是在 Claude Code 执行工具调用之前,先检查本地累计的 token 消耗。一旦发现用量已经接近或超过你设置的阈值,就立刻返回 block 决策,阻止后续操作继续消耗额度。简单说,它把“被服务端强制中断”变成了“在本地主动拦截”。
这篇文章会从 Claude Code 的 hook 机制讲起,解释 AgentObs 的定位和设计思路,再带你把一个最小可用的 AgentObs 完整实现出来。内容包括环境准备、配置文件、Node.js 脚本、settings.json 注册方式、常见报错排查和工程化建议。无论你是刚接触 Claude Code 的新手,还是已经在用 hook 做自动化控制的开发者,都可以按文章步骤直接上手。
1. 背景与核心概念
1.1 为什么需要“额度耗尽前的主动拦截”
Claude Code 是面向终端场景的 AI 编程助手,适合在真实项目里完成代码生成、重构、调试、批量文件处理等任务。它的计费通常与 token 消耗挂钩,订阅用户也有自己的额度上限。日常使用中容易遇到两种情况:
第一种是长时间任务中的“硬中断”。比如你让 Claude Code 连续处理几十个文件,跑到第 30 个文件时额度耗尽,会话直接停住。此时你已经投入了大量上下文和时间,重新恢复的成本很高。
第二种是成本失控。命令行工具使用起来非常顺手,一行命令就触发大量 token 消耗,等你想起来去查看用量时,费用已经积累到一个不理想的数值。
AgentObs 的价值就在这里。它通过本地统计和 hook 拦截,在额度到达硬性上限之前留出缓冲。你可以在 80% 用量时收到预警,在 90% 用量时强制阻止新的工具调用。这样既保护任务不被打断,也避免超出预期成本。
1.2 Claude Code 的 hook 机制是什么
Claude Code 提供了一种叫 hook 的扩展机制。简单理解,它允许你在 Claude Code 运行生命周期的某些节点上,挂载一段自己的脚本。脚本执行完以后,可以把执行结果返回给 Claude Code,Claude Code 会根据返回值决定是继续执行、跳过还是停止。
常见的 hook 事件包括:
| 事件名称 | 触发时机 | 典型用途 |
|---|---|---|
| PreToolUse | 模型准备调用某个工具之前 | 权限校验、参数过滤、用量检查 |
| PostToolUse | 工具执行完成之后 | 记录结果、汇总成本、清理临时文件 |
| Notification | Claude Code 需要向用户显示通知时 | 弹窗提醒、转发消息、触发外部通知 |
| Stop | 一次助手响应结束之后 | 更新本地统计、写审计日志 |
| PreCompact | 对话上下文即将被压缩时 | 保存摘要、备份关键信息 |
每个 hook 事件都会由一个 JSON 对象作为输入,内容包含事件名称、当前工作目录、会话标识、工具名称等。脚本从 stdin 读取这段 JSON,处理后把决策写到 stdout,Claude Code 再读取 stdout 来完成后续动作。
对 AgentObs 来说,最关键的事件是 PreToolUse。因为只要我们能在这个节点检查用量并返回 block,就能在模型真正调用工具之前“刹住车”。
1.3 AgentObs 的定位:观察者 + 守卫
从名字可以看出,AgentObs 的核心是两个词:Observe(观察)和 Block(拦截)。它的工作流程可以拆成四步:
- 统计用量:定期从 Claude Code 的会话日志中提取 token 消耗,累加到本地统计文件。
- 判断阈值:读取配置文件中的软性阈值和硬性阈值。
- 提前预警:当用量超过软性阈值时,在日志中输出预警,提醒你该注意了。
- 主动拦截:当用量超过硬性阈值时,在 PreToolUse 阶段返回 block,阻止后续调用继续消耗额度。
这里必须强调一点:AgentObs 不是“破解限制”的工具,也不是绕过平台计费的方案。它做的事情完全基于 Claude Code 开放的能力边界,只是把额度的控制权从“服务端强制”转移到了“本地主动管理”。这种思路在成本敏感的项目里尤其有用。
2. 环境准备与版本说明
2.1 安装 Claude Code CLI
AgentObs 依赖 Claude Code 的 hook 能力,所以第一步是确保你已经安装了 Claude Code CLI。安装完成后,可以在终端执行:
claude --version正常情况下会输出当前 CLI 的版本号。由于 Claude Code 更新比较频繁,hook 配置项、事件名称、HookInput 的字段结构都可能在不同版本之间有细微差别。本文的示例以较为常见的 npm 安装方式为例,具体版本请以你本机实际安装结果为准。
如果你还没有安装命令行工具,可以先参考官方文档完成安装和登录。安装过程中会要求登录账号并完成授权,这是正常流程,按照指引操作即可。
2.2 准备 Node.js 运行环境
AgentObs 的示例脚本使用 Node.js 编写,这样不需要额外安装第三方依赖,只要系统里有 Node.js 就能直接运行。建议使用 18 或以上版本,方便使用较新的 API。在终端执行:
node -v如果返回版本号,说明 Node.js 环境正常。如果你更熟悉 Python 或其他语言,完全可以把示例改写为对应的实现,核心逻辑是一样的。
2.3 了解 Claude Code 的配置文件
Claude Code 的全局配置位于用户主目录下的.claude文件夹中,其中settings.json是 hook 注册的入口。这个文件的常见位置是:
~/.claude/settings.json在编写 Hook 配置之前,建议先备份原文件。如果你之前没有配置过,这个文件可能不存在,直接新建即可。
同时,AgentObs 自己的脚本和状态文件会放在一个独立的目录中,建议统一放在:
~/.agentobs/这样可以把“工具自身文件”和“Claude Code 配置”解耦,后续升级或回滚都更方便。
3. 核心原理拆解
3.1 Hook 的输入输出协议
要写 AgentObs,就必须先理解 Claude Code hook 的输入输出协议。
当某个 hook 事件触发时,Claude Code 会执行你在 settings.json 里配置的 command,并通过 stdin 把一段 JSON 传给这个命令。这个 JSON 通常包含下面这些字段:
| 字段名 | 含义 |
|---|---|
| session_id | 当前会话的 ID |
| transcript_path | 当前会话日志文件的路径 |
| cwd | 当前工作目录 |
| hook_event_name | 触发的事件名称,比如 PreToolUse |
| tool_name | 本次准备调用的工具名称 |
| tool_input | 工具调用参数 |
脚本处理完以后,需要向 stdout 输出执行结果。如果输出为空字符串,Claude Code 会认为你允许继续执行。如果输出一个 JSON 对象,比如:
{ "decision": "block", "message": "用量已达到本地阈值,本次调用被拦截" }Claude Code 就会阻止当前动作,并把 message 返回给上层处理。
这里有一个非常关键的细节:脚本在运行过程中的 console.log 输出会写入 stdout,这会污染 HookOutput,导致 Claude Code 解析失败。所以 AgentObs 的脚本里,所有日志信息都必须写入 stderr,只有最终的决策 JSON 才写入 stdout。
3.2 用量统计的数据来源
AgentObs 需要回答一个问题:当前已经消耗了多少 token?
最直接的方式是读取 Claude Code 的会话日志。Claude Code 会把每次会话的内容以 JSONL 格式保存到用户主目录下的.claude/projects目录中,一个会话对应一个.jsonl文件。文件里每一行是一条 JSON 记录,其中助理消息的message.usage字段通常包含本轮消耗的输入 token 数和输出 token 数。
一个简化的日志记录结构大致如下:
{ "type": "assistant", "message": { "role": "assistant", "usage": { "input_tokens": 1200, "output_tokens": 800 } } }AgentObs 的采集脚本会递归扫描这个目录,读取所有.jsonl文件,把其中的input_tokens和output_tokens累加起来,得到累计用量。
需要提醒的是,usage字段的具体位置和名称在不同 Claude Code 版本中可能变化。你可以在自己的会话日志文件里先搜索一下"usage",确认实际结构后再决定解析逻辑。真实项目里更推荐用 Claude Code 官方提供的调试接口、SDK 或代理网关来获取用量,日志解析适合作为入门方案。
3.3 阈值决策模型
AgentObs 的决策逻辑可以设计成软硬两档:
| 阈值 | 行为 | 目的 |
|---|---|---|
| softLimit | 只写预警日志,不拦截 | 提醒开发者注意剩余额度 |
| hardLimit | 输出 block 决策,阻止工具调用 | 强制暂停,避免超出限额 |
判断逻辑很简单:
if totalTokens >= hardLimit: 返回 block else if totalTokens >= softLimit: 记录预警日志,返回 allow else: 记录正常日志,返回 allow在实际工程中,你可以在硬性拦截之前增加“倒计时预测”。比如根据最近 10 分钟的平均 token 消耗速度,估算剩余额度还能支撑多久,并在消息里提示用户。这一步属于进阶优化,后面会单独讲。
4. 完整实战:实现一个最小可用的 AgentObs
这一节我们用一个具体项目来实现 AgentObs。整个项目只有三个核心文件,外加一份 settings.json 配置,不依赖任何 npm 包。
4.1 创建项目结构
在终端执行以下命令:
mkdir -p ~/.agentobs项目目录结构如下:
~/.agentobs/ ├── config.json # 阈值和提示语配置 ├── collect.js # 用量采集脚本 ├── agentobs-hook.js # hook 决策脚本 └── stats.json # 统计结果文件(由 collect.js 自动生成)先把配置文件写好。创建~/.agentobs/config.json:
{ "softLimit": 80000, "hardLimit": 90000, "blockMessage": "[AgentObs] 本地累计 token 已达到硬性限额,本次调用已阻止。" }这里softLimit和hardLimit的单位是 token。你可以根据自己的套餐和单次任务规模调整,比如日常任务一次大概消耗 2 万 token,那软性阈值可以设成 16 万,硬性阈值设成 18 万。
4.2 编写用量采集脚本
创建~/.agentobs/collect.js,这个脚本负责扫描 Claude Code 的会话日志,把累计 token 写入stats.json。
#!/usr/bin/env node // 路径:~/.agentobs/collect.js const fs = require('fs'); const path = require('path'); const os = require('os'); const CLAUDE_DIR = path.join(os.homedir(), '.claude', 'projects'); const STATS_FILE = path.join(os.homedir(), '.agentobs', 'stats.json'); function getAllTranscriptFiles(dir) { const results = []; if (!fs.existsSync(dir)) return results; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory()) { results.push(...getAllTranscriptFiles(full)); } else if (entry.name.endsWith('.jsonl')) { results.push(full); } } return results; } function collectUsage() { let inputTokens = 0; let outputTokens = 0; const files = getAllTranscriptFiles(CLAUDE_DIR); for (const file of files) { try { const lines = fs.readFileSync(file, 'utf8').split('\n'); for (const line of lines) { if (!line.trim()) continue; const record = JSON.parse(line); const usage = record && record.message && record.message.usage; if (!usage) continue; inputTokens += usage.input_tokens || 0; outputTokens += usage.output_tokens || 0; } } catch (err) { console.error(`[AgentObs] 解析日志失败: ${file} => ${err.message}`); } } const stats = { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens, updatedAt: new Date().toISOString() }; fs.mkdirSync(path.dirname(STATS_FILE), { recursive: true }); fs.writeFileSync(STATS_FILE, JSON.stringify(stats, null, 2)); console.error(`[AgentObs] 当前累计用量: ${stats.totalTokens} tokens`); } collectUsage();这段代码的核心逻辑是遍历.claude/projects目录下的所有.jsonl文件,逐行解析,抽取message.usage里的输入、输出 token,最后累加并写入stats.json。
这里有两个设计点需要说明:
第一,脚本把统计结果写到 stderr,而不是 stdout。因为collect.js和后面的agentobs-hook.js会串联在同一段 hook 命令中,任何 stdout 输出都会干扰 HookOutput 的正常解析。第二,首次解析会扫描全部历史日志,如果历史文件很多,可能比较慢。生产环境建议改成增量统计,只解析新增部分。
4.3 编写 hook 决策脚本
创建~/.agentobs/agentobs-hook.js,这个脚本负责读取stats.json,根据阈值决定返回 allow 还是 block。
#!/usr/bin/env node // 路径:~/.agentobs/agentobs-hook.js const fs = require('fs'); const path = require('path'); const os = require('os'); const CONFIG_FILE = path.join(os.homedir(), '.agentobs', 'config.json'); const STATS_FILE = path.join(os.homedir(), '.agentobs', 'stats.json'); const DEFAULT_CONFIG = { softLimit: 80000, hardLimit: 90000, blockMessage: '[AgentObs] 已达到本地硬性限额,本次调用已阻止。' }; function readJSON(file, fallback) { try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (err) { return fallback; } } function main() { let input = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', (chunk) => { input += chunk; }); process.stdin.on('end', () => { let hookInput = {}; try { hookInput = input ? JSON.parse(input) : {}; } catch (err) { hookInput = {}; } const config = readJSON(CONFIG_FILE, DEFAULT_CONFIG); const stats = readJSON(STATS_FILE, { totalTokens: 0 }); const total = stats.totalTokens || 0; const eventName = hookInput.hook_event_name || 'unknown'; if (total >= config.hardLimit) { const message = config.blockMessage + ` 当前累计 ${total} tokens。`; process.stdout.write(JSON.stringify({ decision: 'block', message })); } else if (total >= config.softLimit) { console.error(`[AgentObs] 软性预警:当前累计 ${total} tokens,已接近硬性限额。`); process.stdout.write(''); } else { console.error(`[AgentObs] 用量正常:当前累计 ${total} tokens。`); process.stdout.write(''); } console.error(`[AgentObs] event=${eventName} total=${total}`); }); } main();这个脚本的核心是“读取 hook 输入 → 读取统计文件 → 判断阈值 → 输出决策”。需要注意,process.stdout.write('')输出空字符串,表示允许继续执行。只有当totalTokens超过hardLimit时,才会输出带decision: block的 JSON。
为了安全和可追溯,可以额外增加一个审计日志文件。在最佳实践一节我会专门说明。
4.4 注册到 Claude Code 的 settings.json
现在把 AgentObs 挂到 Claude Code 的 hook 事件上。编辑~/.claude/settings.json,加入以下内容:
{ "hooks": { "PreToolUse": [ { "hooks": [ { "type": "command", "command": "node ~/.agentobs/collect.js && node ~/.agentobs/agentobs-hook.js" } ] } ], "Notification": [ { "hooks": [ { "type": "command", "command": "node ~/.agentobs/collect.js && node ~/.agentobs/agentobs-hook.js" } ] } ], "Stop": [ { "hooks": [ { "type": "command", "command": "node ~/.agentobs/collect.js" } ] } ] } }这份配置做了三件事:
PreToolUse:在每次工具调用前,先采集用量,再执行阈值判断。Notification:在 Claude Code 输出通知时,同样执行采集和判断,便于及时感知异常。Stop:在每次回复结束后,只采集用量,不执行判断,避免不必要的 hook 开销。
如果你担心每个工具调用都执行采集脚本会拖慢速度,可以给PreToolUse增加matcher限制,例如只对 Bash 命令生效:
{ "matcher": "Bash", "hooks": [ { "type": "command", "command": "node ~/.agentobs/collect.js && node ~/.agentobs/agentobs-hook.js" } ] }具体事件名称和 matcher 的规则可能随版本变化,建议先查看当前版本的官方 hook 文档确认。
4.5 运行与验证
配置文件写好后,先在终端手动验证脚本是否正常工作。
第一步,运行采集脚本:
node ~/.agentobs/collect.js如果历史会话日志中有 usage 信息,终端会通过 stderr 输出累计用量,同时~/.agentobs/stats.json会生成类似下面的内容:
{ "inputTokens": 12000, "outputTokens": 8000, "totalTokens": 20000, "updatedAt": "2025-01-01T12:00:00.000Z" }第二步,模拟 hook 输入,测试决策脚本:
echo '{"hook_event_name":"PreToolUse","tool_name":"Bash"}' | node ~/.agentobs/agentobs-hook.js在用量未超过阈值时,stdout 为空字符串,说明脚本允许继续执行。然后手动把stats.json里的totalTokens改成一个大数,比如95000,再运行一次测试命令,可以看到 stdout 输出:
{ "decision": "block", "message": "[AgentObs] 已达到本地硬性限额,本次调用已阻止。 当前累计 95000 tokens。" }这说明 AgentObs 的拦截逻辑已经生效。测试完成后记得把stats.json改回正常值,或者重新运行collect.js恢复真实统计数据。
4.6 在 Claude Code 中观察效果
启动 Claude Code,开始正常对话。每次模型调用工具之前,你都可以在调试日志里看到 AgentObs 脚本的输出。当累计用量位于软性阈值区间时,日志会出现类似:
[AgentObs] 软性预警:当前累计 82000 tokens,已接近硬性限额。此时对话仍可继续,但你已经知道剩余空间不多了。当用量超过硬性阈值时,模型调用会被本地阻止,Claude Code 会返回你配置的拦截消息。这相当于给项目加了一道“本地熔断器”,避免在额度不足时继续执行高消耗任务。
5. 常见问题与排查思路
5.1 Could not locate the Claude CLI on path
有读者反馈,在 hook 脚本里直接调用claude命令时,会提示:
failed to run claude code: error: could not locate the claude cli on path这通常是 PATH 环境变量问题。Claude Code 执行 hook 脚本时,可能不会加载你 shell 里的完整 PATH。解决思路有两个:
- 在 settings.json 的 command 里使用
claude的绝对路径。先用which claude找到安装位置,再填入。 - 在 hook 脚本里先手动 export 正确的 PATH,再调用
claude。
另外要提醒一点:AgentObs 的设计里,hook 脚本不应该调用claude命令。如果采集脚本或决策脚本内部又启动了一个新的 Claude Code 进程,会再次触发 hook,造成递归调用。轻则日志刷屏,重则栈溢出卡死。这也是很多 hook 项目常见的坑。
5.2 模型名在当前版本不被识别
使用第三方模型或自定义模型时,可能出现类似:
"deepseek-v4-pro" is not a model this version of claude code recognizes这类报错和 AgentObs 本身关系不大,但会让使用者误以为是 hook 配置问题。常见原因有:
- Claude Code CLI 版本过旧,不认识新的模型标识。
- settings.json 或环境变量中的模型名写错。
- 第三方网关或中转服务返回了不兼容的模型列表。
排查时先执行claude --version确认版本,再检查ANTHROPIC_MODEL环境变量和 settings.json 里的模型配置。如果版本没问题,可以尝试把模型名改成当前版本支持的写法,或者更新 CLI。
5.3 settings.json 配置没有生效
Hook 配置没生效,通常表现为:脚本写好了、command 也能手动执行,但 Claude Code 运行时不触发任何 hook。按照下面顺序排查:
- 确认修改的是正确的 settings.json。项目级配置、用户级配置和系统级配置优先级不同,如果项目里也有
.claude/settings.json,用户级配置可能被覆盖。 - 检查 JSON 格式是否合法。多一个逗号、少一个引号都会导致整个配置被忽略。
- 修改配置后需要重启 Claude Code。hook 配置通常不会热加载。
- 临时去掉
matcher,看 hook 是否在任意事件下都能触发,缩小问题范围。
5.4 Hook 脚本超时或执行缓慢
如果每次工具调用前都要全量扫描所有历史日志,对话时间长了以后,性能会明显下降。表现就是模型调用前卡顿几秒甚至更久。
解决方案是在 collect.js 里做增量统计。记录每个日志文件已经解析到的字节偏移量,下次从偏移位置继续解析,而不是重新扫描全部文件。更好的方案是定期在后台执行采集任务,把结果写入内存或本地缓存,hook 脚本只读取结果,不做重活。
5.5 采集不到 usage 字段
不同版本的 Claude Code 日志结构可能不同。如果你发现stats.json里所有计数都是 0,先打开一个.jsonl文件,搜索"usage"确认字段是否存在。
如果日志里确实没有 usage 信息,可以考虑换一种用量来源,比如通过 Anthropic API 的用量接口查询,或者在你的代理网关注入统一统计。AgentObs 的核心价值是“本地主动管理”,数据来源本身可以根据你的环境替换。
6. 最佳实践与工程建议
6.1 配置与代码分离
AgentObs 的阈值、提示语、事件名称都应该放在 config.json 中,而不是写死在脚本里。这样调整策略时只需要改配置,不需要改代码。更进一步,可以把配置做成支持环境变量覆盖的形式,方便在测试环境和生产环境之间切换。
例如:
const config = readJSON(CONFIG_FILE, DEFAULT_CONFIG); const softLimit = Number(process.env.AGENTOBS_SOFT_LIMIT || config.softLimit); const hardLimit = Number(process.env.AGENTOBS_HARD_LIMIT || config.hardLimit);这样既保留了默认配置,又允许在特殊场景下临时覆盖阈值。
6.2 日志与审计
在生产环境,hook 脚本的每一次决策都应该记录到独立的日志文件,方便事后复盘成本和安全问题。建议至少记录以下信息:
- 触发事件名称。
- 当前统计文件中的 totalTokens。
- 决策结果是 allow 还是 block。
- 触发时间。
审计日志不要写到 stdout,统一写文件或 stderr。否则会污染 HookOutput,导致 Claude Code 行为异常。
6.3 使用 fail-open 还是 fail-closed
当 AgentObs 脚本自身出现异常时,怎么处理?两种策略:
- fail-open:脚本异常时默认放行,保证 Claude Code 的主流程不中断。
- fail-closed:脚本异常时默认拦截,宁可错杀不可放过,适合成本控制优先的场景。
对于成本敏感但不希望影响开发的场景,建议默认 fail-open,并在脚本里把异常信息写入审计日志。如果团队有严格的成本上限,可以改成 fail-closed,但一定要在告警里显著标识“AgentObs 异常”,避免开发者误以为模型出了问题。
6.4 防止并发写入冲突
Claude Code 的多个 hook 事件可能并发触发,如果多个进程同时读写stats.json,可能导致文件内容损坏。解决方案有两个:
- 使用文件锁,比如 Node.js 中的
proper-lockfile,或者直接使用系统级的flock。 - 把统计状态迁移到 SQLite 或 Redis,靠数据库的事务能力保证一致性。
对本地单机场景,文件锁已经足够。对团队共享场景,建议把 AgentObs 改造成一个独立的统计服务,多个 Claude Code 进程通过 HTTP 接口上报用量。
6.5 合法合规使用
最后必须强调一个原则:AgentObs 是本地主动管理工具,它的目的是帮你更好地控制成本和使用节奏,而不是绕过平台限制。不要在脚本里尝试伪造用量、躲避服务端配额或做任何违反平台规则的操作。在团队和公司场景中使用时,提前和财务、安全团队确认统计口径和阈值设定,比事后解释账单要轻松得多。
7. 总结与下一步
这篇文章从 Claude Code 的 hook 机制入手,完整实现了 AgentObs 的最小可用版本。你现在应该理解了 hook 的输入输出协议,知道了如何通过 settings.json 注册 PreToolUse、Notification、Stop 事件,也掌握了本地 token 统计和阈值拦截的核心思路。
AgentObs 的下一步发展方向很清晰:把全量扫描改成增量统计,加入基于时间窗口的消耗速率预测,把审计日志接入企业通知机器人,或者做成一个带 Web 页面的成本看板。如果只做一件事,我建议优先解决“统计实时性”,因为只有统计足够快,拦截才足够精准。
真正跑起来以后,你可能会发现阈值设置本身就是一门经验活。不同项目的任务长度、单次调用 token 消耗、团队预算都不一样。可以先从一个保守的阈值开始,跑几天看统计数据,再根据实际使用曲线调整。最终,你会把 AgentObs 打磨成最适合自己工作流的那个版本。