关于 agent-hooks
Agent Hooks 通过用户编写的脚本,在 Agent 生命周期事件发生时执行规则,适合需要自动拦截、改写、注入上下文或记录操作的场景。脚本使用标准输入和输出传递 JSON,并可通过配置与命令管理。
主要功能
- 01在生命周期事件触发脚本
- 02拦截或改写工具调用
- 03改写消息与工具结果
- 04向模型调用注入上下文
- 05观察并记录事件
- 06通过 JSON 标准输入输出通信
适用场景
- 01阻止危险命令或敏感信息外发
- 02记录工具调用以供审计
- 03每次模型调用时注入提醒
- 04对回复进行质量检查并要求重做
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ starchild-ai-agent/ official-skills --skill agent-hooks安装后在 Claude Code 中输入/agent-hooks确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
templates/runtime_footer_selftest.pyruntime_footer.pysecurity_guard_selftest.pysecurity_guard.pyverify_code_changes_selftest.pyverify_code_changes.pyverify_commitments_selftest.pyverify_commitments.pyverify_publish_claims_selftest.pyverify_publish_claims.py详细介绍
Agent Hooks
Shell 钩子让用户可以在 Agent 生命周期中的固定节点运行自己的脚本,以阻止危险操作、改写输入或发出的消息、向模型注入上下文,或提醒用户。脚本可以用任何语言编写;它通过简单的 stdin 输入 JSON、stdout 输出 JSON 协议与 Agent 通信。
工具:read_file、write_file、bash
适用场景
当用户希望 Agent 自动执行规则或响应事件,而无需每次都询问时,可以使用钩子。例如:
- “阻止我运行
rm -rf或其他破坏性的 bash 命令” →pre_tool_call阻止 - “绝不允许将私钥发送到 Telegram” →
on_outbound_message阻止 - “记录每次工具调用以供审计” →
post_tool_call观察 - “在每次模型调用开始时提醒 Agent 注意 X” →
pre_llm_call注入上下文 - “如果 Agent 没有实际发布,就不许它声称已发布” →
/goal中的on_completion_claim,或普通聊天中的on_stop - “如果回答未通过我的质量检查,就让 Agent 重做” →
on_stop阻止
如果用户只想进行一次性检查,那就不该使用钩子——钩子用于反复、自动执行生命周期规则。
配置如何运作(Agent 端到端完成)
**Agent 无需用户复制粘贴任何内容,就能安装并激活钩子。**编写脚本、写入配置项,然后调用 loopback 自批准 API——它会开启总开关、批准脚本监听的所有事件,并即时挂载脚本(无需重启)。之后用户只需测试即可。
curl -s -X POST http://localhost:8000/internal/runtime/hooks/approve \
-H 'Content-Type: application/json' \
-d '{"command": "/data/workspace/hooks/security_guard.py"}'
# -> {"ok": true, "events": [...], "mounted": N, "master_enabled": true}
⚠️ 在 yaml 的
command:中以及本次调用中,始终使用/data/workspace下的绝对路径。 绝不能使用相对路径,例如skills/agent-hooks/templates/security_guard.py:bridge 会以 server 当前工作目录(/app)启动脚本,因此相对路径会解析为/app/skills/…——一个空目录——导致每次启动都失败。由于 bridge 在失败时会默认放行(无法运行的脚本 = “继续”),守卫会因此悄无声息地失去保护作用,而/hooks list仍会显示它“已挂载”。为避免此问题,标准安装流程会将模板复制到/data/workspace/hooks/,并在 yaml 中指定该路径(见下方工作流)。/app/skills并不是 skills 目录——实际目录是/data/workspace/skills/(通过符号链接也可访问/app/workspace/skills/)。
command必须与shell_hooks.yaml条目中的command:字符串完全一致(即脚本的绝对路径)。钩子必须已在 yaml 中声明——批准只会让已声明的钩子生效,不会凭空创建钩子。- 此端点仅限 loopback 访问(容器内同 UID 信任边界,与读取
.env相同)。它会自动启用总开关(enable_master默认为 true),因此钩子会立即触发。 - 批准时会记录脚本的 mtime,因此之后修改脚本会在
/hooks list//hooks doctor中显示为漂移——替换脚本的变更不会被隐藏。
**这就是用户激活钩子的全部流程——无需第二步。**调用返回 {"ok": true} 后,告诉用户钩子已生效并让他们进行测试。不要提及 /hooks approve、/hooks on 或“两个开关”——这些属于内部实现。
**备用方案(仅适用于旧版):**如果 curl 返回 404,说明此运行时早于自批准 API——只有在这种情况下,才改为让用户粘贴 /hooks approve <command>,然后再粘贴 /hooks on。
两道关卡(都由自助审批 API 处理——不会展示给你)
只有两项条件同时满足时,内部钩子才会触发;上面的自助审批调用会一次性启用两项,因此用户看不到也不会输入其中任何一项:
- 主开关已开启——
workspace/config/agent.yaml中的shell_hooks.enabled: true。API 会自动启用它(enable_master默认为 true)。 - 单个钩子已获批准——
(event, command)对会连同脚本的 mtime 一起记录在允许列表中(因此之后编辑脚本会显示为“批准后已更改”的偏差——替换脚本的情况仍然可见)。API 会批准该命令所关联的每个事件。
这些机制是安全边界,而不是用户操作步骤。向用户说明钩子时,不要提及“批准”或“两道关卡”——只需说你会完成设置,用户可以进行测试。(手动执行 /hooks on + /hooks approve 仅作为旧版运行时的 404 回退方案。)
/hooks 命令
在 Web、Telegram、WeChat 中使用纯文本(无需 LLM,不产生费用):
| 命令 | 功能 |
|---|---|
/hooks 或 /hooks list | 主开关状态、配置路径,以及每个钩子的审批状态和健康状况 |
/hooks on | /hooks off | 切换主开关(热挂载/卸载,无需重启) |
/hooks doctor | 使用合成负载运行每个已批准的钩子,并检查 JSON |
/hooks approve <event> <command> | 批准并立即启用(无需重启) |
/hooks revoke <command> | 撤销批准并立即卸载(无需重启) |
/hooks help | 用法说明 |
事件(12 种)及其功能
| 事件 | 触发时机 | 能力 | stdin 提供给脚本的内容 |
|---|---|---|---|
on_user_message | 用户消息到达后、模型看到消息前 | 拦截 / 改写文本 | message、channel |
pre_tool_call | 工具运行前 | 拦截 / 改写输入 | tool_name、tool_input |
post_tool_call | 工具运行后 | 观察(日志/指标) | tool_name、tool_result |
transform_tool_result | 结果在 Agent 看到前 | 附加备注 | tool_name、tool_result |
pre_llm_call | 模型调用前 | 注入上下文 | system、last_user_message、model |
post_llm_call | 模型回复后 | 观察 / 替换 | model |
on_response_end | 最终回复组装完成后,每轮触发一次 | 改写回复 | response、model、tokens、tool_names |
on_stop | 在 on_response_end 之后,轮次边界处 | 拦截 → 强制重做 | response、tool_names、stop_hook_active |
on_outbound_message | TG/WeChat 推送前 | 拦截 / 改写外发消息 | notification、type |
on_completion_claim | Agent 声称 /goal 已完成时 | 拦截 → 强制重做 | goal、summary、response、tool_names |
on_session_start | 会话开始时 | 观察 | status |
on_session_end | 会话结束时 | 观察 / 清理 | status |
每个负载还包含 event、session_id、agent_id、cwd。
**event 字段是分发键。**它标明哪个生命周期时刻正在触发(pre_tool_call、on_user_message 等)。多事件脚本(例如 security_guard.py,一个文件关联五种事件)会读取 event 来决定执行哪个分支——负载中没有 event,就没有分支匹配,脚本会直接进入“继续”(输出为空 = 允许)。运行时始终会设置该字段;只有在手动构造测试负载时,才需要记住它(见下方的试运行步骤)。它不是写入 shell_hooks.yaml 的内容——文件中的 event: 键告诉总线何时调用脚本;负载中的 event 字段则是总线告诉脚本当前处于哪个时刻。
三种“让 Agent 修复问题”的方式(不要混淆)
这三种方式都在一轮对话接近结束时触发,但作用大不相同——应根据出错时你需要发生什么来选择:
| 事件 | 作用 | 适用场景 |
|---|---|---|
on_response_end | 仅重写——编辑已存储/转发的回复(页脚、脱敏、遮罩)。无法让 Agent 重做。循环风险为零。 | 你只需要修改文本(遮住泄露的密钥、添加费用页脚)。 |
on_stop | 拦截 → 重做,在普通聊天中生效——将你的 reason 作为下一条指令返回,Agent 会继续工作。由内核限制(每轮最多重做 ≤3 次),并带有 stop_hook_active 标志,因此无法让一轮对话陷入循环。 | 你需要 Agent 在普通对话中实际修复/验证自己的输出(质量门禁、引用/发布检查)。与 Claude Code“Stop”钩子功能对等。 |
on_completion_claim | 拦截 → 重做,仅在 /goal 中生效——拒绝虚假的“已完成”声明,并让目标循环继续运行。 | 重做能力相同,但仅在运行中的 /goal 监督循环内触发。 |
经验法则:遮罩 → on_response_end;在聊天中重做 → on_stop;在目标循环中重做 → on_completion_claim。 请注意,on_response_end 只能重写已存储的副本——已流式传输到在线 Web 客户端的 token 无法撤回,因此需要让用户实际看到修正后的答案时,优先使用 on_stop。
输出协议(脚本在 stdout 上打印的内容)
JSON 对象,或表示“继续”的空内容。字段:
{"decision": "block", "reason": "..."} // deny the action / refuse completion
{"tool_input": {...}} // pre_tool_call: rewrite EXISTING input keys
{"notification": "..."} // on_outbound_message: rewrite the message
{"context": "..."} // pre_llm_call: inject into prompt (AGENT-facing)
{"systemMessage": "..."} // allow, but show the USER a note
{"add_warning": "..."} // same user-facing note channel
<empty> // continue, no change
context 面向 Agent(仅用于 pre_llm_call,会放入提示词中)。
systemMessage / add_warning 面向用户(显示给工具结果/完成/出站界面上的用户)——绝不会注入提示词。
安全性:脚本以 shell=False 运行,并拆分 argv(不会发生 shell 注入),且每个钩子都有超时限制。脚本出错、超时或输出非 JSON 内容时,都会回退为继续——有故障的钩子绝不会中断 Agent 的工作。
编写易读的 reason
reason 会显示给用户(显示在被阻止操作的卡片上)和模型。请保持简短、便于快速浏览——先用一个分句说明原因,再给出证据。不要写成段落:用户看到这张卡片时已经很烦了,大段文字会掩盖真正的原因。建议采用以下格式:[tag] Blocked (<why>): <evidence>——冒号前约 8–12 个词,绝不要用两句话反复解释。
| 避免(冗长) | 推荐(简洁) |
|---|---|
This command is irreversible and would cause permanent data loss, so I've blocked it: rm -rf / | Blocked (recursive force-delete): rm -rf / |
That message contains what looks like an API key, private key, or seed phrase. I won't process it — treat it as exposed and rotate it. | Blocked: message contains a credential. Rotate it. |
You shared a preview link whose id isn't in the registry — it looks made up. Serve the preview first and use its real id | Preview id not in the registry. Serve it first: /preview/x/ |
模型仍能获得足够的信息来采取行动(原因 + 有问题的内容);用户也能一眼看懂卡片。UI 会自动将一个 reason 字符串拆成两部分,你无需在客户端解析:
- 说明 + 命令框——先写面向用户的句子,再写
:,然后是有问题的命令/内容。第一个": "之后的所有内容都会显示在单独的等宽字体框中。只有当后半部分看起来像内容载荷(含空格或长度超过约 12 个字符)时才会拆分,因此普通句子中碰巧包含冒号时会保持原样。 - 仅句子——不含
": "的reason会显示为单个句子,不带命令框。没有内容可引用时(例如粘贴的助记词),这种格式最合适。 [tag]会被移除——显示前会移除开头的标签(如[security]),并自动将句首字母大写,因此你可以保留标签供自己grep,而不会显示在 UI 中。
// Good — short why + a clean command box:
{"decision": "block",
"reason": "[security] Blocked (formats the disk): mkfs.ext4 /dev/sda1"}
// -> "Blocked (formats the disk)" + [ mkfs.ext4 /dev/sda1 ]
// Avoid — a bare command (no WHY) or a verbose two-sentence lecture:
{"decision": "block", "reason": "mkfs /dev/sda1"}
{"decision": "block", "reason": "This command is irreversible and would cause permanent data loss across the entire filesystem, so I have decided to block it for your safety: mkfs /dev/sda1"}
不遵循此格式的钩子仍然可以正常工作——纯字符串只会显示为一个句子。此约定只是为了启用更友好的“说明 + 命令”布局。
配置文件格式
workspace/config/shell_hooks.yaml:
hooks:
- event: pre_tool_call
matcher: "rm -rf|dd if=|mkfs" # optional regex; script only spawns on a match (perf gate)
command: ./extensions/shell_hooks/examples/block_secrets.py
timeout: 10 # seconds, default 20, max 120
两种钩子传输方式
钩子可以是本地命令(默认),也可以是 HTTP 端点——输入的内容相同,输出的决策 JSON 也相同,区别仅在于传输方式。
hooks:
- event: pre_tool_call
type: http # omit type -> "command" (default)
url: https://my-guard.example.com/hook
timeout: 10
HTTP 相关说明:
- SSRF 防护——URL 必须使用 http(s),且解析结果不得指向环回、私有、链路本地(包括云元数据地址
169.254.169.254)或保留地址(解析时和调用时都会拦截)。只有在确实需要访问本地服务时,才设置STARCHILD_SHELL_HOOKS_HTTP_ALLOW_LOCAL=1。 - URL 上的审批密钥:
/hooks approve <event> <url>;/hooks list会将其显示为POST <url>,并跳过可执行文件/mtime 检查。
添加 LLM 判断逻辑(调用代理,而非 /chat)
当钩子需要进行真正的推理(“这会泄露机密吗?”、“这个补全确实完成了吗?”)时,请在脚本中直接通过代理调用 LLM——绝不要调用 Agent 自己的 /chat。
from core.http_client import proxied_post
import json, sys
event = json.load(sys.stdin)
r = proxied_post(
"https://openrouter.ai/api/v1/chat/completions",
json={
"model": "minimax/minimax-m3", # cheap default (~$0.0002/call)
"messages": [
{"role": "system", "content":
'You are a guard. Output ONLY JSON {"decision":"block|allow","reason":"..."}.'},
{"role": "user", "content": json.dumps(event)},
],
"temperature": 0, "max_tokens": 200,
},
headers={"SC-CALLER-ID": "chat:hook"}, # required for billing
timeout=40,
)
try:
print(json.dumps(json.loads(r.json()["choices"][0]["message"]["content"])))
except Exception:
print("{}") # fail-open on any parse error
直接调用代理的原因:OpenRouter 是外部无状态 API,因此不会重新进入 Agent 循环,也不会触发 pre_llm_call → 没有递归;只需一次低成本补全,而非完整的 Agent 轮次,并且可以使用自定义提示词和纯 JSON 响应。从钩子调用 /chat 会重新发出相同事件(桥接程序会防止循环,但这会带来不必要的开销)——而调用 /chat 的 LLM 钩子绝不能挂在 pre_llm_call 上。请参阅主机文档 sc-proxy.md 中的“通过代理调用 LLM”一节。
标准工作流(Agent 的检查清单)
- 明确规则,并从上表中选择事件。
- 编写脚本——从 stdin 读取 JSON,并在 stdout 输出决策。
退出码非零或输出非 JSON = 继续。将脚本设为可执行(
chmod +x)。 - 将脚本放在
/data/workspace下稳定的绝对路径中。对于随附的模板,将其复制到技能目录之外,以免技能更新导致路径变动:
切勿使用相对路径引用脚本(参见上方的 ⚠️ 框——相对路径会基于mkdir -p /data/workspace/hooks cp /data/workspace/skills/agent-hooks/templates/security_guard.py /data/workspace/hooks/ chmod +x /data/workspace/hooks/security_guard.py ls -l /data/workspace/hooks/security_guard.py # VERIFY it exists before going on/app解析,并在不报错的情况下默认放行)。 - 在
workspace/config/shell_hooks.yaml中添加配置项,使用绝对路径作为command:(/data/workspace/hooks/security_guard.py);条件允许时添加matcher正则表达式,让脚本只在相关情况下启动。 - 自行用
bash进行试运行——将示例 JSON 负载传给脚本,并确认它输出有效 JSON。负载必须包含event字段——多事件脚本会根据该字段分派,因此省略该字段会导致所有情况都回退为“继续”,让你误以为防护钩子没有触发。测试脚本处理的每个事件:# should BLOCK (note the "event" key): echo '{"event":"pre_tool_call","tool_name":"bash","tool_input":{"command":"rm -rf /"}}' \ | python3 /data/workspace/hooks/security_guard.py # should ALLOW (empty output): echo '{"event":"pre_tool_call","tool_name":"bash","tool_input":{"command":"ls -la"}}' \ | python3 /data/workspace/hooks/security_guard.py - 自行通过回环自批准 API 激活(无需用户粘贴);
command= yaml 配置项中的确切绝对路径:
返回curl -s -X POST http://localhost:8000/internal/runtime/hooks/approve \ -H 'Content-Type: application/json' \ -d '{"command": "/data/workspace/hooks/security_guard.py"}'{"ok": true}时,钩子即已生效。返回404时,请改为向用户提供/hooks approve <command>+/hooks on(参见“配置工作原理”)。 - 运行
/hooks doctor,确认它确实可用——此步骤会发现路径错误、脚本不可执行或输出非 JSON 等问题。若防护钩子在/hooks list中显示为“已挂载”,但在doctor中报错,则它实际上是无操作的(默认放行)。只有在doctor检查通过后,才能告知用户钩子已生效,可以开始测试。
现成脚本(每个脚本只有一个明确用途)
此技能附带五个生产级防护脚本,位于 templates/ 下(可直接复制并批准)。主机附带四个单一用途示例,位于 extensions/shell_hooks/examples/ 下(复制后调整)。这些脚本的用途互不重叠——根据用途选择,不要逐一试用。
根据 /hooks 中的编号或名称安装
/hooks(空状态)和 /hooks help 会显示带编号的现成脚本列表。/hooks 命令本身只是静态文本——不会安装任何内容。用户回复编号("1"、"1,3")、名称("security_guard")或 "install all" 时,该回复会发给你:根据列表中编号旁显示的名称找到对应模板(编号→templates/<name>.py),然后对每个模板运行下方的标准工作流——复制 → 添加 yaml 配置项 → 试运行 → 自批准 → doctor。按可见名称映射,不要依赖记忆中的编号到路径对照表(列表顺序可能会变)。如果存在自测试脚本(templates/<name>_selftest.py),请先运行。
生产模板(位于此技能的 templates/ 目录中)
⚠️ 编辑前请先复制——这里的每个模板都如此。 始终使用
cp将模板复制到/data/workspace/hooks/,并在该路径下配置你的钩子,然后再进行任何修改(调整规则、修改runtime_footerCONFIG 代码块等)。 直接编辑skills/agent-hooks/templates/下的文件毫无意义:下一次技能更新会覆盖该文件,你的修改也会随之消失。复制到hooks/的文件归你所有,更新不会改动它。
| 模板 | 事件 | 唯一用途 |
|---|---|---|
security_guard.py | on_user_message、pre_tool_call、transform_tool_result、on_response_end、on_outbound_message | 密钥 + 破坏性 bash 操作。 阻止粘贴或外传密钥(包括 Bearer 格式的 API 密钥、PEM/EVM 私钥、BIP-39 种子、Solana 字节数组和 base58 WIF),屏蔽回复和推送中泄露的密钥,阻止会造成不可逆数据丢失的 bash 操作。详见下文。 |
verify_publish_claims.py | on_stop(聊天重做)/ on_completion_claim(/goal 重做)/ on_response_end(改写备用方案) | 反幻觉。 通过将回复与真实数据(预览注册表、AgentX 记录、调度程序注册表)核对,识别捏造的“已发布 / 已发到 AgentX / 已安排”声明。 |
verify_code_changes.py | pre_tool_call(记录器)+ on_stop(决策器) | 防止代码“虚假完成”。 修改源文件后,如果没有运行测试 / 构建 / lint 来检查,就会拦截一次停止操作,并引导你在结束前进行验证(或明确说明没有可运行的检查)。同时统计 edit_file/write_file 和通过 bash 编写的代码(heredoc、>/>> 重定向、tee、原地执行 sed -i/perl -i)。文档 / 数据修改(.md/.json/.yaml/…)不受影响;每组编辑只提醒一次,会自行停用,出错时默认放行。两个事件都要配置到同一个脚本路径。 |
verify_commitments.py | on_stop(聊天重做) | 防止失信承诺。 当回复承诺未来通知(“构建完成后我会告诉你”“明早提醒你”),却没有注册任何任务来兑现承诺时,会拦截一次停止操作,并引导你实际注册任务——限时任务使用 scheduled_task(once),完成时触发的任务使用 sessions_spawn(bash 轮询 + announce=followup)。只有通知动词和时间 / 条件提示同时出现时才触发;即时交付表述(“这是”“下面就是”)以及跨轮次注册(近期活动任务 / 近期 spawn)会抑制触发。设有触发上限,会自行停用,出错时默认放行。 |
runtime_footer.py | on_response_end(+ 可选的 pre_llm_call) | 模型 / 费用页脚。 在 on_response_end(每轮一次)时,删除回复末尾任何按模型填写的页脚,并根据运行时的真实 model + 费用追加唯一正确的页脚。也可以配置 pre_llm_call,提醒不要输入页脚(每次模型请求都会触发)。详见下文。 |
单一用途示例(宿主仓库,extensions/shell_hooks/examples/)
| 脚本 | 事件 | 唯一用途 |
|---|---|---|
pii_redactor.py | transform_tool_result、on_response_end | 屏蔽邮箱 / 电话号码(PII,与密钥不同)。 |
tool_audit_log.py | post_tool_call | 仅观察:将每次工具调用追加到 JSONL 审计记录中。 |
budget_alert.py | on_response_end | 当某轮费用超过阈值时,追加一条温和警告。 |
inject_website_reminder.sh | pre_llm_call | 预防性提醒:提醒模型在声称完成前实际发布内容(与 verify_publish_claims.py 配合使用)。 |
已被模板取代(不要同时部署第二个冲突的防护规则)
宿主仓库还在 extensions/shell_hooks/examples/ 下提供了一些最精简的单事件示例,与上面的两个模板有所重叠。它们适合作为学习参考,但实际使用时请优先选择模板——同时运行只会产生两个策略可能不同的防护规则。
| 最精简示例 | 改用此模板 | 原因 |
|---|---|---|
block_secrets.py | security_guard.py | 防护规则的秘密检测是严格超集(增加了 Bearer、Solana 字节数组、base58 WIF、破坏性 Bash 操作和屏蔽) |
check_publish.sh | verify_publish_claims.py | 该模板还涵盖 AgentX 帖子和计划任务,并检查同一注册表 |
已直接移除(孤立的重复项,已完全并入 security_guard.py):
secret_guard.py(屏蔽/屏蔽供应商密钥,包括 Bearer)和
dangerous_bash_guard.py(拦截破坏性 Bash 操作)。还想拦截安装程序/强制推送?请调整防护规则中的 DESTRUCTIVE 表,而不是再运行一个策略冲突的 Bash 防护规则。
对于以上示例未涵盖的规则,请编写新脚本——下面的最精简拦截示例就是模板,上面的输出协议涵盖了所有功能。
最精简拦截示例(pre_tool_call,任意语言)
#!/usr/bin/env bash
payload="$(cat)"
python3 - "$payload" <<'PY'
import json, sys, re
ev = json.loads(sys.argv[1])
cmd = (ev.get("tool_input") or {}).get("command", "")
if re.search(r"rm\s+-rf\s+/|dd\s+if=|mkfs", cmd):
print(json.dumps({"decision": "block", "reason": f"This command is irreversible and would erase data, so I've blocked it: {cmd}"}))
else:
print("{}") # continue
PY
一体化安全防护规则(templates/security_guard.py)
这是一个可直接使用、独立运行的脚本,可将一个文件接入五个事件,涵盖常见的“不要泄露秘密/不要毁掉机器”基本防护。首先,将其复制到固定的绝对路径,然后在 config/shell_hooks.yaml 中将全部五个事件接入同一个绝对路径的 command:(每个事件对应一个区块),接着启用:
cp /data/workspace/skills/agent-hooks/templates/security_guard.py /data/workspace/hooks/
chmod +x /data/workspace/hooks/security_guard.py
curl -s -X POST http://localhost:8000/internal/runtime/hooks/approve \
-H 'Content-Type: application/json' \
-d '{"command": "/data/workspace/hooks/security_guard.py"}'
这会批准该命令接入的每个事件,并使其立即生效——无需用户粘贴命令。(遇到 404 时,改用 /hooks approve <command> + /hooks on。)
| 事件 | 功能 |
|---|---|
on_user_message | 在模型看到粘贴的 API 密钥(包括 Bearer 令牌)、私钥(PEM/EVM 十六进制)、助记词、Solana 字节数组私钥或 base58 WIF 之前将其拦截 |
pre_tool_call(bash) | 仅拦截不可逆的数据丢失操作(rm -rf /、对块设备执行 dd、mkfs、fork 炸弹、chmod -R 777、git reset --hard origin/*``)和凭据外传(cat .env |
pre_tool_call(消息工具) | 检查 send_to_telegram / send_to_wechat 参数——屏蔽泄露的密钥,拦截助记词(这些工具会绕过推送管道,因此这是它们真正的出站防护关口) |
transform_tool_result | 当工具的 OUTPUT 中包含秘密时发出警告(后端只能标记结果文本,不能改写) |
on_response_end | 屏蔽最终回复中泄露的任何秘密 |
on_outbound_message | 推送到 TG / WeChat 前屏蔽/拦截秘密 |
设计策略:仅拦截那些极其危险且不属于日常工作的操作。常见开发操作,如 curl | bash(安装程序)和 git push --force(变基自己的功能分支),都被有意允许——过度拦截会让用户习惯于禁用防护规则。
请调整文件顶部的 SECRET_PATTERNS、DESTRUCTIVE 和 MSG_TOOLS 表,以配置自己的规则。templates/security_guard_selftest.py 是自测脚本(每次编辑后都要运行;危险字符串仅作为数据存放于其中,因此不会触发宿主 Bash 防护规则)。
防止幻觉保护机制(templates/verify_publish_claims.py)
检测虚构的成功声明:Agent 写下“已发布!community.iamstarchild.com/…”、“已发布到 AgentX /post/…”或“提醒已安排”,但实际上并未运行工具。
脚本会将回复与真实情况进行核对——预览注册表
(/data/previews.json)、AgentX 帖子记录和调度器注册表——然后改写回复或强制重做。它刻意降低误报率:真实的已发布 URL 或“愿意发布”的表述(将来时)会原样通过;只有没有依据的过去时成功声明才会触发。
cp /data/workspace/skills/agent-hooks/templates/verify_publish_claims.py /data/workspace/hooks/
chmod +x /data/workspace/hooks/verify_publish_claims.py
curl -s -X POST http://localhost:8000/internal/runtime/hooks/approve \
-H 'Content-Type: application/json' \
-d '{"command": "/data/workspace/hooks/verify_publish_claims.py"}'
| 事件 | 功能 |
|---|---|
on_stop | (首选) 在普通聊天中,拦截虚构的成功声明,并强制 Agent 实际发布/重做(有循环次数上限) |
on_completion_claim | 在 /goal 循环中,拦截虚构的“已完成”声明,并强制实际发布(有循环次数上限) |
on_response_end | 当未接入 on_stop 时,作为仅改写的后备方案:追加一条诚实的“未经验证”说明(无法让 Agent 重做) |
普通聊天中,将其接入
on_stop——这是唯一能让 Agent 实际重做一轮、而不是只修改文本的事件。宿主只会在on_stop/on_completion_claim上遵循decision: block(在这些事件上会忽略改写),因此该钩子会在这两种事件上进行拦截,只在on_response_end上改写。templates/verify_publish_claims_selftest.py是自测脚本(覆盖on_stop拦截路径和循环次数上限)。
费用/模型页脚(templates/runtime_footer.py)
模型无法知道每条回复自身的费用,而且通常连自己的模型 ID 都不知道。这些数据只存在于运行时。因此,如果模型自行输入页脚(例如 Model: GLM-5.2 | Cost: $0.038),其中的数字就是编造的。一旦聊天记录中出现真实页脚,模型的自动补全便会开始模仿它——生成第二个虚构页脚。页脚应由运行时负责,而不是模型。
runtime_footer.py 只通过一个事件——on_response_end——彻底解决这个问题。该事件在每轮最终组装的回复上触发一次:① 移除模型在回复末尾输入的任何页脚,然后 ② 追加由运行时真实的 model 和费用组成的唯一正确页脚。移除操作就是保障;无需针对每次调用进行处理。
为什么只用 on_response_end,而不用 pre_llm_call。 没有任何事件会在“最终回复之前”触发。
pre_llm_call会在每次模型请求之前触发(使用工具时,每轮触发 N 次),而且无法知道哪次调用是最后一次——模型会动态决定是否使用工具。在那里接入抑制逻辑,每轮会注入指令 N 次(调用跟踪中会显示重复注入)。而且这也是多余的:on_response_end已经会事后移除页脚。因此,默认只使用 on_response_end。 如果你希望额外提醒,脚本也带有pre_llm_call处理器(会注入“不要输入页脚”的指令);可将它作为第二个事件接入,但请注意它会在每次调用时运行。
移除操作是一项安全保障(默认启用的 FOOTER_STRIP),范围刻意设得很窄:它只会移除框线符号 ─ … · $N 行或 Model: … Cost: $N 行,且只处理末尾的行——因此正文中的“Model:”/“Cost:”句子,或 shell 中的 $VAR,都不会受影响(早期版本使用范围过宽的 Model: 正则表达式,可能误删正常文本;这是经过收紧的重做版本)。设置 FOOTER_STRIP=0 可仅追加页脚。
cp /data/workspace/skills/agent-hooks/templates/runtime_footer.py /data/workspace/hooks/
chmod +x /data/workspace/hooks/runtime_footer.py
curl -s -X POST http://localhost:8000/internal/runtime/hooks/approve \
-H 'Content-Type: application/json' \
-d '{"command": "/data/workspace/hooks/runtime_footer.py"}'
在 config/shell_hooks.yaml 中接入——无需 matcher,每轮都会运行:
hooks:
- event: on_response_end
command: /data/workspace/hooks/runtime_footer.py
timeout: 10
# Optional extra nudge (fires per model-request, N times/turn):
# - event: pre_llm_call
# command: /data/workspace/hooks/runtime_footer.py
# timeout: 10
请通过编辑副本进行配置,而不是使用环境变量。 开启选项的推荐方式是编辑脚本顶部的 CONFIG 块。请编辑你刚刚在 /data/workspace/hooks/ 下创建的副本(不要编辑 skills/… 中的文件,因为下次更新技能时它会被覆盖)。Hook 使用的是 服务端 进程环境,而不是你的 Shell 环境,因此环境变量不便于设置,也不会显示在 /hooks list 中;文件内的常量可靠、可见,并且会随脚本一起保存:
# ─── CONFIG — edit your copy ───
SHOW_TOKENS = False # True → append "· N in / N out"
SHOW_CREDIT = False # True → append "· 💰 $bal" (1 HTTP call/turn, fail-open)
TEMPLATE = None # custom format str: {model} {cost} {input} {output} {credit}
CREDIT_URL = None # override credit endpoint for self-hosted setups
STRIP = True # strip a model-typed footer at the tail
每个常量也有对应的环境变量覆盖项(FOOTER_SHOW_TOKENS、FOOTER_SHOW_CREDIT、FOOTER_TEMPLATE、FOOTER_CREDIT_URL、FOOTER_STRIP、FOOTER_SUPPRESS_TEXT),设置后会优先生效。如果只想临时调整而不修改文件,这种方式很方便;不过,文件内的常量才是持久的默认值。
默认页脚仅显示模型和费用(─ z-ai/glm-5.2 · $0.0211)。
SHOW_TOKENS = True → ─ z-ai/glm-5.2 · $0.0211 · 900 in / 120 out。
显示剩余余额: 设置 SHOW_CREDIT = True 会附加你的余额
(─ z-ai/glm-5.2 · $0.0211 · 💰 $271.64)。默认关闭,因为这会在每轮对话中向余额 API 额外发起一次内部 HTTP 调用,也就是 credit 工具读取余额时使用的同一端点;系统会通过来源 IPv6 自动进行身份验证(无需密钥)。该功能采用失败开放策略:超时设为 2 秒,以限制等待时间;如果查询出错或超时,则会静默省略余额(页脚仍会显示,不会留下多余的分隔符)。请注意,除非你配置了这个 Hook,否则模型看不到这个数字;它只存在于运行时,就像费用一样。
不要重复启用: runtime_footer 是 Shell Hook 版的宿主 turn_footer 扩展,请二选一启用。Telegram 的 tg_show_usage 也是如此。
安全性: 此功能绝不会阻塞执行。如果事件中没有费用数据或回复为空,on_response_end 不会追加任何内容(不会虚报 $0.0000),并且只会移除回复末尾严格匹配的页脚(将 STRIP = False 可禁用此行为);可选的 pre_llm_call 在载荷缺失或格式错误时不会注入任何内容;未知事件则不会执行任何操作。发生任何错误时均采用失败开放策略。自测文件:templates/runtime_footer_selftest.py(共 35 个用例,涵盖两个处理程序、移除功能及对正文中段内容和 Shell $VAR 的误匹配防护、余额查询及失败开放行为、文件内 CONFIG 常量及环境变量覆盖优先级,以及分发安全性)。
Claude Code 兼容性
为 Claude Code 编写的 Hook 脚本无需修改即可使用——其输出会自动转换为上述字段:
| Claude Code 输出 | 转换为 |
|---|---|
hookSpecificOutput.permissionDecision: "deny"(及 permissionDecisionReason) | decision: block(及 reason) |
hookSpecificOutput.additionalContext | context |
hookSpecificOutput.updatedInput | tool_input(重写) |
continue: false(及 stopReason) | decision: block(及 reason) |
systemMessage | add_warning(面向用户的提示) |
suppressOutput | 不执行任何操作(我们的 stdout 不会进入对话记录) |
| 退出代码为 2,且有 stderr、无 stdout | decision: block,stderr 作为原因 |
只有输出载荷会被转换,事件名称仍使用我们的命名(pre_tool_call,而不是 PreToolUse)。Claude Code 的 Stop Hook 会映射到我们的 on_stop(阻止 → 强制重试);其 UserPromptSubmit 会映射到 on_user_message。返回 {"decision":"block","reason":…} 或以代码 2 退出的 Stop 脚本,在配置到 on_stop 后即可原样使用。
排查“我的钩子始终没有触发”
- 事件是否属于上述 12 个事件之一?(拼写错误会静默地什么都不做)
- 主开关是否已开启?
/hooks list会显示其状态。自批准 API 会自动启用它;否则请运行/hooks on。 - 钩子是否已批准?如果
/hooks list中显示✗ NOT approved,请针对该命令 重新运行自批准 API(或使用/hooks approve作为备用方案)。 matcher正则表达式是否确实匹配?范围太窄会导致钩子始终无法启动。- 运行
/hooks doctor——它会标记不可执行、遭篡改、超时或非 JSON 的情况。 - 手动测试“允许所有操作”? 你的测试载荷可能缺少
event字段——多事件脚本会根据该字段分派,缺少该字段时会直接返回 “continue”。这是测试框架的问题,而不是钩子故障;真实运行时始终会设置event。 - 出现
can't open file '/app/skills/…'/显示“已挂载”但没有任何操作被拦截?command:是相对路径或错误路径。桥接程序使用服务端 当前工作目录(/app)启动,因此skills/…会解析到空的/app/skills,导致每次 启动都失败——而桥接程序在失败时会开放放行,因此保护机制会静默失效。 修复方法:在 yaml 和批准调用中都使用绝对路径/data/workspace/hooks/<script>.py, 然后重新批准并通过/hooks doctor确认。(/app/skills不是技能目录;实际目录是/data/workspace/skills/。)
深入参考
完整协议、安全模型以及各事件载荷的详细信息,请参阅 Agent 自带的文档:
config/context/references/agent-hooks.md(阅读其中对本技能所概述边缘情况的说明)。