关于 cc-hooks
这是用于配置 Claude Code 生命周期钩子的技能,可通过 Shell 命令执行范围明确、可测试的规则。它包含默认策略分发器和可选防护,并要求成功时保持静默,避免干扰工具调用。
主要功能
- 01生命周期事件钩子
- 02工具调用前拦截与转发命令
- 03策略注册表与分发器
- 04可选的读取和技能编辑防护
- 05成功时静默并明确拦截
适用场景
- 01执行前拦截有风险的命令
- 02将已安装技能的编辑转回源文件
- 03限制超长文件读取
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ boshu2/ agentops --skill cc-hooks安装后在 Claude Code 中输入/cc-hooks确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
skill.spec.jsonhooks/codex-read-budget-guard.shinstalled-skill-edit-guard.shpolicy-dispatch.shread-budget-guard.shskill-first-coord-guard.shpolicies/policies.jsonreferences/DCG-RCH.mdGUARDRAIL-VALUE-PROOF.mdHOOK-EVENTS.mdINSTALLED-SKILL-EDIT-GUARD.mdJSON-OUTPUT.mdPATTERNS.mdREAD-BUDGET-GUARD.mdSKILL-FIRST-COORDINATION-GUARD.mdscripts/install-hooks.shlint-policies.sh详细介绍
Claude Code 钩子
在 Claude Code 生命周期的特定时间点触发的 Shell 命令。
钩子可以强制执行仅靠文字说明无法做到的事:模型可以想办法绕过一条指令,但无法绕过退出码 2——这正是每个钩子都必须范围明确、保持静默且可撤销的原因。
命名失败模式——正常路径话太多:退出码为 0 时输出 stdout 的钩子会破坏它所守护的工具调用;成功时保持静默是契约的一部分,而非风格偏好。
提示词
Add a PreToolUse hook to fleet-router/.claude/settings.json that blocks `git push --force` on the main branch. Keep it silent on exit 0, exit 2 with a message on block, and confirm it fires with a manual test invocation before committing the change.
满足以下条件即表示正常工作
- 当钩子阻止
git push --force时,脚本以退出码2退出并输出 stderr 消息;在允许的路径上则以退出码0退出,且不输出 stdout。 .claude/settings.json为新钩子新增一个匹配器条目,与现有钩子列表并列,而不是替换它。- 在提交更改之前,针对新匹配器手动调用测试,转录记录中显示拦截触发,且能看到退出码
2。 - 钩子只检查它所守护的
PreToolUse调用,其他文件一律不动。
约束
- 强制执行钩子(PreToolUse 策略分发器)默认启用:插件安装会自动接入
hooks/hooks.json;技能副本和检出版本通过一条命令接入(scripts/install-hooks.sh)。操作者可以按主机禁用(/plugin disable,或移除设置中的匹配器)。 - 注入钩子(SessionStart/UserPromptSubmit 上下文填充)保持禁用状态——#511 的拆除验证表明,在 10.35M 个驻留 token 下,delta=0。切勿启用此类钩子;无钩子冷启动门禁仍会强制执行这一要求。
- 保持正常路径静默,并且仅依据事件文档规定的退出码/JSON 契约进行拦截,因为多余的 stdout 可能会破坏工具调用。
- 使用
stop_hook_active限制 Stop 钩子,并缩小匹配器范围,以防止递归和拦截无关命令。
快速开始
添加到 ~/.claude/settings.json(用户级)或 .claude/settings.json(项目级):
{"hooks":{"PreToolUse":[{"matcher":"Bash","hooks":[{"type":"command","command":"my-validator.sh"}]}]}}
钩子事件
| 事件 | 触发时机 | 能否拦截? | 常见用途 |
|---|---|---|---|
PreToolUse | 工具运行前 | 是 | 拦截/修改命令 |
PostToolUse | 工具成功后 | 提供反馈 | 自动格式化、lint |
PermissionRequest | 权限对话框 | 是 | 自动批准/拒绝 |
UserPromptSubmit | 提交提示词时 | 是 | 添加上下文、验证 |
Stop | Claude 完成时 | 是 | 强制继续 |
SessionStart | 会话开始时 | 否 | 加载上下文、设置环境变量 |
Notification | 通知时 | 否 | 桌面提醒 |
完整架构:HOOK-EVENTS.md
匹配器
"Bash" → exact match
"Edit|Write" → regex OR
"mcp__.*__write" → MCP tools
"*" or "" → all tools
工具:Bash、Read、Write、Edit、Glob、Grep、Task、WebFetch、WebSearch
退出码
| 代码 | 效果 |
|---|---|
| 0 | 成功——从 stdout 解析 JSON |
| 2 | 拦截——将 stderr 传给 Claude |
| 其他 | 非拦截性错误 |
拦截工具
简单方式(退出码 2):
echo "Blocked: reason" >&2 && exit 2
JSON 方式(退出码 0):
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Blocked"}}
决策:"allow"(自动批准)、"deny"(拦截)、"ask"(显示对话框)
修改输入
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow",
"updatedInput":{"command":"modified-command"}}}
实际应用:DCG + RCH
{"hooks":{"PreToolUse":[{"matcher":"Bash","hooks":[
{"type":"command","command":"dcg"},
{"type":"command","command":"rch"}
]}]}}
- DCG:拦截
git reset --hard、rm -rf、git push --force - RCH:将构建任务路由到远程工作器
详情:DCG-RCH.md
Skill-First Coordination Guard(可选启用)
可复制粘贴的 PreToolUse 配方,提醒 Agent 先加载协调技能,再手动编写 am/atm/ntm/tmux send-keys CLI。此配方不会自动安装;需针对每个宿主选择启用(不同于默认随附的策略分发器)。
钩子的上下文预算原则:钩子是最强大的强制机制(机械执行,无法通过推理绕过),但会污染上下文,因此应谨慎使用。钩子在正常情况下必须保持静默(退出码为 0,不输出 stdout/stderr),仅在确实违规时触发(理想情况下每个会话仅触发一次,并由哨兵控制);优先使用 PreToolUse 违规防护,而非 UserPromptSubmit/SessionStart 逐轮注入器;在退出码为 0 的 PreToolUse 路径上,绝不能输出多余的 stdout(它会被解析为 JSON 并导致工具调用失败)。通过退出码 2 + stderr 进行拦截。
此配方原样提供两个脚本、精确的仅匹配命令开头的匹配器(因此 br create --body "...am/atm/ntm..." 绝不会误触发)、双匹配器的可选启用 settings.json 片段,以及一项能验证所有触发和静默场景的 bats 测试。
配方:SKILL-FIRST-COORDINATION-GUARD.md
Installed-Skill-Edit Guard(可选启用)
一个 PreToolUse Edit|Write 防护机制,可将对已安装技能副本(*/.claude/skills/**、.codex、.gemini)的编辑引导回仓库中唯一可信的源文件 skills/<name>/。这是一个真正会造成错误的操作:编辑已安装或符号链接的副本没有合理用途(安装时会被覆盖,或者符号链接会指向工厂检出目录)。误报面为零:它只匹配 tool_input.file_path,因此正文中仅提及 claude/skills 的文档绝不会触发。可撤销 → 它会进行引导(退出码 2 + 单行重定向提示),而非硬性拦截。其他任何情况下均保持静默;每个会话仅触发一次。默认不启用 — 需使用可选启用安装器:
scripts/install-installed-skill-edit-guard.sh # user scope; --project for project
配方:INSTALLED-SKILL-EDIT-GUARD.md
价值验证(为什么移除无钩子机制后,这项防护仍值得保留)
核心防护提供不受闸门影响的逐次触发遥测:每次触发时,它都会向 ${AGENTOPS_HOME:-~/.agents/ao}/guardrail-telemetry.jsonl 追加且仅追加一行 JSONL — {ts, session, token_class, path_sha256}(可通过 AGENTOPS_GUARDRAIL_TELEMETRY 覆盖)。路径经过 SHA-256 哈希处理,绝不记录原文(保护隐私);正常情况下不会写入任何内容;只有安装并触发防护后,传感器才会启用。预先注册的方法 — 指标 = 随时间推移,触发尝试率逐渐下降(这是重定向无法伪造的信号,不是循环定义的手动编写率)、最小 N、噪声下限,以及N 较小时得出无结论是可接受的结果 — 满足 ADR-0002 第 58 行(“证明其价值的测试或评估证据”),该标准的缺失曾导致 2.x 钩子被淘汰(#511)。
读取预算防护(选择启用)
一个 PreToolUse Read|Bash 防护,用于拒绝读取行数超出预算且未设上限的操作(AOP_READ_BUDGET_LINES,默认值为 350):没有 limit 的 Read,或有效行数超出预算的 cat/head/tail。Spotify 事件的教训是:CLAUDE.md 中的同一规则只是建议,因此被忽略;超出预算的读取还会在后续每一轮重新发送这些行。判断条件是查找(对确切参数执行 wc -l),因此它是独立防护,绝不是注册表策略。受 limit 限制的片段、行数不超过预算的文件、管道、重定向,或仅提及 cat 的引号文本,都会通过。保留字面引号或转义的路径;不支持的 Shell 语法和切换目录的命令链会放行。对于负数 head 行数,按 GNU head 实际保留的行数计算;在检查可执行文件身份后,Darwin 系统工具不支持的标志会放行。带正号的正数 head 行数按其数值进行限制。一旦字节进入上下文,就无法撤回 → 每次尝试都会拦截(退出码 2 + stderr):每个会话仅首次显示完整消息,说明两个正确做法(分片读取,或委派给插件的 agentops:bulk-reader 子 Agent/agentops:bulk-read 工作流),之后只显示一条简短提示。仅在运行时列出裸名称时,才将其用于独立定义或链接。通过 AOP_WAIVE=core.context:unbounded-read 豁免一次;哈希遥测会添加 tool、lines、budget,以及分发器的 mode/decision 对。
默认禁用 — 通过选择启用的安装程序启用:
scripts/install-read-budget-guard.sh # user scope; --project for project
配置说明:READ-BUDGET-GUARD.md
策略分发引擎(默认启用)
准入控制层(史诗 age-4qw1):一个 PreToolUse 分发器 — hooks/policy-dispatch.sh — 评估以数据定义的策略注册表(policies/policies.json,契约为 schemas/hooks-manifest.v2.schema.json),取代 N 个手动接线的设置条目。这是工具调用层级上的隔离层:使用相同的词汇,但层级低于推送时的棘轮/闸门。
每项策略包括:dcg 风格的 ID(domain.object:token)、mode: deny | route | audit、匹配器(工具 + command/file_path 正则表达式)、指明正确工具的 route_message、理由,以及预先注册的 value_proof(ADR-0002 的生命周期承诺:没有累积证明 → 退役该策略)。
谓词约束,由架构强制执行(#511 的反面教训):只有 predicate_class: pure —— 针对命令或文件路径的语法错误标记 —— 才能 deny/route。查找/有状态谓词在通过审查的触发记录促成晋升前,只能以 audit 模式启用。scripts/lint-policies.sh 以机械方式强制执行此规则(仅依赖 jq;在 bats 和 CI 中运行)。
可接受的误报范围: 由于纯谓词会匹配原始命令字符串中的任何位置,其保护标记即使被引号括起作为数据(提交消息正文、dcg test "..." 探测、here-doc 载荷),仍可能触发,即使实际不会执行任何有害操作。这是“只有纯谓词才能拒绝”这一规则有意接受的代价;另一种方案(查询仓库/上下文)恰好属于该约束禁止用于 deny 的有状态谓词。每次触发都可撤销:使用一次性的 AOP_WAIVE=<policy-id> 或添加一行 policy-waivers 即可解除。
语义:正常路径 = 退出码 0,无输出。deny = 退出码 2 + 一条 stderr 路由提示(每个会话仅首次显示完整消息,之后显示简短提示——每次尝试仍会被拦截)。route = 退出码 0 + permissionDecision:"ask" JSON。audit = 放行并记录。每次触发都会追加一条哈希防护遥测记录(token_class = 策略 ID,并包含 mode/decision)。使用 AOP_WAIVE=<policy-id> 豁免一次,或在 policy-waivers 文件中添加一行 <policy-id> <expiry-epoch>。缺少注册表或 jq 时会放行。
强制执行组(全部为纯正则、高痛点)。前四项属于首日维护者组(age-wnyt)——用于保护本仓库的构件。第五项保护的是产品自身的不变量,因此会在每个使用方仓库中触发,而不仅限于当前仓库:
| 策略 | 拦截 | 路由至 |
|---|---|---|
core.git:add-beads-ledger | git add 指定 _beads/(私有账本一旦泄露就无法挽回) | 推送账本仓库本身——绝不要在公共目录树中执行 git add _beads |
core.provenance:ledger-hand-append | 重定向/tee/Edit/Write 到 docs/provenance/ledger.jsonl(哈希链式记录,已封存) | ao provenance add |
core.skills:copy-into-installed | cp/rsync/mv 到 `~/.claude | .codex |
core.skills:edit-installed-copy | 对已安装技能副本执行 Edit/Write(仅检查 file_path——不会因文本内容触发) | 编辑仓库中的 skills/<name>/ |
core.verdicts:hand-edit | 对 .agents/ao/verdicts/ 执行 Edit/Write,或使用 Bash >/>>/tee/cp/rsync/mv 写入其中(强制检查目标位置)——文件名就是内容的 SHA-256,手动编辑会破坏摘要身份 | 重新运行验证,让它持久化存储新的产物(validate.py store-verdict) |
core.verdicts:hand-edit 是唯一一个针对 承诺 而非仓库的策略:如果判定结果的哈希值不再与其文件名相符,它就是伪造证据,而上述机制都无法在工具调用层面捕获这种情况。读取存储内容不受影响——对判定结果执行 cat/ls/jq/rg/diff,以及将其复制出来检查,都不会触发;只有写入存储的操作才会触发——包括原位编辑器(sed -i、perl -pi/-ni)和删除命令(rm、unlink、shred)。匹配基于标志参数标记,因此读取操作的脚本文本仅仅包含 -i 时不会触发(已通过 bats 双向验证)。尚未披露的缺口:noclobber 覆盖重定向(>|)。
如何触达用户——每种安装路径都会部署钩子:
| 安装路径 | 部署方式 |
|---|---|
Claude Code 插件(claude plugin install agentops@agentops-marketplace) | 自动——插件包含 hooks/hooks.json(${CLAUDE_PLUGIN_ROOT} 路径);安装后钩子即处于激活状态,无需接线步骤 |
npx skills@latest add boshu2/agentops / skills.sh 复制 | 技能包自带安装器:~/.claude/skills/cc-hooks/scripts/install-hooks.sh(一个命令即可;复制文件无法自行接线) |
| git clone / brew checkout | scripts/install-policy-dispatch.sh(委托给同一个内嵌于技能的安装器) |
安装器会在接线前检查注册表,备份设置,并且可以重复运行。可通过 /plugin disable agentops 或从设置中移除两个 PreToolUse 匹配器来按主机禁用。
契约测试:tests/scripts/policy-dispatch.bats(每项策略的拦截+消息+遥测、意外标准输出风险、豁免、审计/路由模式、失败时放行)。
编写自己的钩子
最简 Python:
#!/usr/bin/env python3
import json, sys
data = json.load(sys.stdin)
cmd = data.get('tool_input', {}).get('command', '')
if 'dangerous' in cmd:
print("Blocked: dangerous", file=sys.stderr)
sys.exit(2)
sys.exit(0) # Allow
钩子输入(stdin):
{"tool_name":"Bash","tool_input":{"command":"npm test"},"session_id":"...","cwd":"..."}
环境变量
| 变量 | 作用范围 | 用途 |
|---|---|---|
CLAUDE_PROJECT_DIR | 全部 | 项目根目录 |
CLAUDE_ENV_FILE | SessionStart/Setup | 持久化环境变量 |
Stop 钩子(强制继续)
{"decision":"block","reason":"Tests failing. Fix before stopping."}
**重要:**检查 stop_hook_active,以防止无限循环。
反模式
| 不要 | 应该 |
|---|---|
| 旧对象格式 | 使用包含 matcher 的数组格式 |
不加引号的 $VAR | "$VAR" |
| 搭配 JSON 使用退出码 2 | 退出码 2 仅使用 stderr |
跳过 stop_hook_active 检查 | 在 Stop 钩子中始终检查 |
调试
claude --debug # Hook execution details
/hooks # View/edit in REPL
输出规范
- 路径: 用户的
~/.claude/settings.json或项目的.claude/settings.json,以及明确命名的钩子脚本。PreToolUse 策略分发器默认随附(每种安装路径都会接入它——参见“策略分发引擎”);其他防护方案(技能优先协调、独立的已安装技能编辑防护、读取预算防护)在主动启用前均不生效。 - 文件名: 保留
settings.json;为脚本指定描述性且可执行的文件名,不要将大型 shell 程序嵌入 JSON。 - 格式: 有效的 Claude 钩子 JSON,使用事件数组、匹配器和命令对象;钩子的标准输出/标准错误和退出码遵循所选事件的架构。
- 退出码: 使用
jq -e '.hooks | type=="object"' <settings.json>验证,并针对每个匹配器执行一次有代表性的静默/触发测试;任何解析错误、正常路径产生多余输出或递归风险都会阻止启用。 - 下游交接: 只有在报告确切范围、撤销命令、测试证据和选择加入位置后,操作员才会使用。
质量检查清单
- 匹配器会在预期事件/输入上触发,并在有代表性的相似但不匹配的情况下保持静默。
- 阻止和允许路径使用文档规定的退出码和输出通道,且不会泄露上下文。
- 钩子可撤销、范围狭窄、不会递归,并明确标记为选择加入的主机策略。
参考资料
- HOOK-EVENTS.md - 所有事件及其完整架构
- DCG-RCH.md - 生产示例(dcg、rch)
- INSTALLED-SKILL-EDIT-GUARD.md - 选择加入的防护,将已安装技能的编辑路由到仓库技能(基石)
- READ-BUDGET-GUARD.md - 选择加入的防护,拒绝超出行数预算的无界读取;与批量读取/代码编写委派配合使用
- GUARDRAIL-VALUE-PROOF.md - 预先注册的价值证明方法 + 每次触发的遥测契约(ADR-0002 l.58)
- PATTERNS.md - 自动格式化、日志记录、通知
- JSON-OUTPUT.md - 响应架构