mcpField
A
agent-hooks

agent-hooks

github.com/starchild-ai-agentAgent 工作流302.2k 次安装6 天前更新
代码仓库

关于 agent-hooks

Agent Hooks 通过用户编写的脚本,在 Agent 生命周期事件发生时执行规则,适合需要自动拦截、改写、注入上下文或记录操作的场景。脚本使用标准输入和输出传递 JSON,并可通过配置与命令管理。

主要功能

  1. 01在生命周期事件触发脚本
  2. 02拦截或改写工具调用
  3. 03改写消息与工具结果
  4. 04向模型调用注入上下文
  5. 05观察并记录事件
  6. 06通过 JSON 标准输入输出通信

适用场景

  1. 01阻止危险命令或敏感信息外发
  2. 02记录工具调用以供审计
  3. 03每次模型调用时注入提醒
  4. 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.py
runtime_footer.py
security_guard_selftest.py
security_guard.py
verify_code_changes_selftest.py
verify_code_changes.py
verify_commitments_selftest.py
verify_commitments.py
verify_publish_claims_selftest.py
verify_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 处理——不会展示给你)

只有两项条件同时满足时,内部钩子才会触发;上面的自助审批调用会一次性启用两项,因此用户看不到也不会输入其中任何一项:

  1. 主开关已开启——workspace/config/agent.yaml 中的 shell_hooks.enabled: true。API 会自动启用它(enable_master 默认为 true)。
  2. 单个钩子已获批准——(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_messageTG/WeChat 推送前拦截 / 改写外发消息notification、type
on_completion_claimAgent 声称 /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 idPreview 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 的检查清单)

  1. 明确规则,并从上表中选择事件。
  2. 编写脚本——从 stdin 读取 JSON,并在 stdout 输出决策。 退出码非零或输出非 JSON = 继续。将脚本设为可执行(chmod +x)。
  3. 将脚本放在 /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 解析,并在不报错的情况下默认放行)。
  4. 在 workspace/config/shell_hooks.yaml 中添加配置项,使用绝对路径作为 command:(/data/workspace/hooks/security_guard.py);条件允许时添加 matcher 正则表达式,让脚本只在相关情况下启动。
  5. 自行用 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
    
  6. 自行通过回环自批准 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(参见“配置工作原理”)。
  7. 运行 /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_footer CONFIG 代码块等)。 直接编辑 skills/agent-hooks/templates/ 下的文件毫无意义:下一次技能更新会覆盖该文件,你的修改也会随之消失。复制到 hooks/ 的文件归你所有,更新不会改动它。

模板事件唯一用途
security_guard.pyon_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.pyon_stop(聊天重做)/ on_completion_claim(/goal 重做)/ on_response_end(改写备用方案)反幻觉。 通过将回复与真实数据(预览注册表、AgentX 记录、调度程序注册表)核对,识别捏造的“已发布 / 已发到 AgentX / 已安排”声明。
verify_code_changes.pypre_tool_call(记录器)+ on_stop(决策器)防止代码“虚假完成”。 修改源文件后,如果没有运行测试 / 构建 / lint 来检查,就会拦截一次停止操作,并引导你在结束前进行验证(或明确说明没有可运行的检查)。同时统计 edit_file/write_file 和通过 bash 编写的代码(heredoc、>/>> 重定向、tee、原地执行 sed -i/perl -i)。文档 / 数据修改(.md/.json/.yaml/…)不受影响;每组编辑只提醒一次,会自行停用,出错时默认放行。两个事件都要配置到同一个脚本路径。
verify_commitments.pyon_stop(聊天重做)防止失信承诺。 当回复承诺未来通知(“构建完成后我会告诉你”“明早提醒你”),却没有注册任何任务来兑现承诺时,会拦截一次停止操作,并引导你实际注册任务——限时任务使用 scheduled_task(once),完成时触发的任务使用 sessions_spawn(bash 轮询 + announce=followup)。只有通知动词和时间 / 条件提示同时出现时才触发;即时交付表述(“这是”“下面就是”)以及跨轮次注册(近期活动任务 / 近期 spawn)会抑制触发。设有触发上限,会自行停用,出错时默认放行。
runtime_footer.pyon_response_end(+ 可选的 pre_llm_call)模型 / 费用页脚。 在 on_response_end(每轮一次)时,删除回复末尾任何按模型填写的页脚,并根据运行时的真实 model + 费用追加唯一正确的页脚。也可以配置 pre_llm_call,提醒不要输入页脚(每次模型请求都会触发)。详见下文。
单一用途示例(宿主仓库,extensions/shell_hooks/examples/)
脚本事件唯一用途
pii_redactor.pytransform_tool_result、on_response_end屏蔽邮箱 / 电话号码(PII,与密钥不同)。
tool_audit_log.pypost_tool_call仅观察:将每次工具调用追加到 JSONL 审计记录中。
budget_alert.pyon_response_end当某轮费用超过阈值时,追加一条温和警告。
inject_website_reminder.shpre_llm_call预防性提醒:提醒模型在声称完成前实际发布内容(与 verify_publish_claims.py 配合使用)。
已被模板取代(不要同时部署第二个冲突的防护规则)

宿主仓库还在 extensions/shell_hooks/examples/ 下提供了一些最精简的单事件示例,与上面的两个模板有所重叠。它们适合作为学习参考,但实际使用时请优先选择模板——同时运行只会产生两个策略可能不同的防护规则。

最精简示例改用此模板原因
block_secrets.pysecurity_guard.py防护规则的秘密检测是严格超集(增加了 Bearer、Solana 字节数组、base58 WIF、破坏性 Bash 操作和屏蔽)
check_publish.shverify_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.additionalContextcontext
hookSpecificOutput.updatedInputtool_input(重写)
continue: false(及 stopReason)decision: block(及 reason)
systemMessageadd_warning(面向用户的提示)
suppressOutput不执行任何操作(我们的 stdout 不会进入对话记录)
退出代码为 2,且有 stderr、无 stdoutdecision: block,stderr 作为原因

只有输出载荷会被转换,事件名称仍使用我们的命名(pre_tool_call,而不是 PreToolUse)。Claude Code 的 Stop Hook 会映射到我们的 on_stop(阻止 → 强制重试);其 UserPromptSubmit 会映射到 on_user_message。返回 {"decision":"block","reason":…} 或以代码 2 退出的 Stop 脚本,在配置到 on_stop 后即可原样使用。

排查“我的钩子始终没有触发”

  1. 事件是否属于上述 12 个事件之一?(拼写错误会静默地什么都不做)
  2. 主开关是否已开启?/hooks list 会显示其状态。自批准 API 会自动启用它;否则请运行 /hooks on。
  3. 钩子是否已批准?如果 /hooks list 中显示 ✗ NOT approved,请针对该命令 重新运行自批准 API(或使用 /hooks approve 作为备用方案)。
  4. matcher 正则表达式是否确实匹配?范围太窄会导致钩子始终无法启动。
  5. 运行 /hooks doctor——它会标记不可执行、遭篡改、超时或非 JSON 的情况。
  6. 手动测试“允许所有操作”? 你的测试载荷可能缺少 event 字段——多事件脚本会根据该字段分派,缺少该字段时会直接返回 “continue”。这是测试框架的问题,而不是钩子故障;真实运行时始终会设置 event。
  7. 出现 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(阅读其中对本技能所概述边缘情况的说明)。

评论

支持 Markdown