agent-hooks
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAgent Hooks
Agent Hooks
Shell hooks let a user run their own script at fixed points in the agent's
lifecycle — to block a dangerous action, rewrite an input or an outbound
message, inject context into the model, or warn the user. The script can
be written in any language; it talks to the agent over a simple JSON-on-stdin,
JSON-on-stdout protocol.
Tools: , ,
read_filewrite_filebashShell钩子允许用户在Agent生命周期的固定节点运行自定义脚本——用于阻止危险操作、重写输入或出站消息、向模型注入上下文,或是向用户发出警告。脚本可以用任意语言编写;它通过简单的“标准输入传JSON、标准输出传JSON”协议与Agent交互。
工具:, ,
read_filewrite_filebashWhen to use
适用场景
Reach for hooks when the user wants the agent to automatically enforce a rule
or react to an event without being asked each time. Examples:
- "Stop me from running / destructive bash" →
rm -rfblockpre_tool_call - "Never let a private key get pushed to Telegram" → block
on_outbound_message - "Log every tool call for audit" → observe
post_tool_call - "Remind the agent of X at the start of every model call" → context
pre_llm_call - "Don't let the agent claim it published when it didn't" →
on_completion_claim
If the user just wants a one-off check, that's not a hook — hooks are for
recurring, automatic lifecycle enforcement.
当用户希望Agent自动执行规则或响应事件,而非每次都需要手动触发时,就可以使用钩子。示例:
- “阻止我执行这类破坏性bash命令” →
rm -rf阻止pre_tool_call - “绝不允许私钥被发送到Telegram” → 阻止
on_outbound_message - “记录所有工具调用用于审计” → 监控
post_tool_call - “在每次模型调用开始时提醒Agent注意X事项” → 上下文注入
pre_llm_call - “不允许Agent谎称已完成发布操作” →
on_completion_claim
如果用户只是想要一次性检查,那不需要使用钩子——钩子适用于重复、自动的生命周期规则执行。
How configuration works (who does what)
配置机制(职责划分)
The agent prepares everything; the user activates. This split is a
deliberate security property — slash commands are parsed before the LLM sees
them, so prompt injection cannot forge an activation.
| Agent does (in conversation) | User must type (activation) |
|---|---|
| Write the hook script (any language) | |
Write/extend | |
Dry-run the script via | |
| Explain / debug |
A hook is arbitrary code execution in the container, so the "who may activate"
gate is reserved for a real human. Always finish by handing the user the exact
and commands to paste.
/hooks approve …/hooks onAgent负责准备所有内容;用户负责激活。 这种分工是刻意设计的安全特性——斜杠命令在LLM处理前就会被解析,因此提示注入无法伪造激活指令。
| Agent在对话中完成的操作 | 用户需输入的激活指令 |
|---|---|
| 编写钩子脚本(任意语言) | |
编写/扩展 | |
通过 | |
| 解释/调试 |
钩子会在容器中执行任意代码,因此**“谁可以激活”**的权限仅对真实人类开放。最后务必向用户提供可直接粘贴的和指令。
/hooks approve …/hooks onThe two gates
双重启用条件
A hook fires only when BOTH hold:
- Master switch ON — in
shell_hooks.enabled: true(flipped byworkspace/config/agent.yamlor the Preferences toggle; legacy env/hooks on|offforces it on as a fallback).STARCHILD_SHELL_HOOKS=1 - Per-hook approval — each pair approved once via
(event, command)(recorded with the script's mtime, so a later edit is flagged "changed since approval" — a swap-the-script attack is visible)./hooks approve
Switch on but unapproved → inert (shows ✗ in ). Approved but switch
off → inert until .
/hooks list/hooks on只有同时满足以下两个条件时,钩子才会触发:
- 总开关开启 —— 中
workspace/config/agent.yaml(可通过shell_hooks.enabled: true或偏好设置开关切换;旧版环境变量/hooks on|off可强制开启作为备用方案)。STARCHILD_SHELL_HOOKS=1 - 单钩子已批准 —— 每个配对需通过
(event, command)批准一次(记录脚本的修改时间,因此后续编辑会被标记为“自批准后已更改”——脚本替换攻击会被识别)。/hooks approve
总开关开启但未批准 → 无效(中显示✗)。已批准但总开关关闭 → 无效,直到执行。
/hooks list/hooks onThe /hooks
command
/hooks/hooks
命令
/hooksPlain text on web / Telegram / WeChat (no LLM, no cost):
| Command | What it does |
|---|---|
| master switch state, config path, every hook + approval/health |
| flip the master switch (hot mount/unmount, no restart) |
| run each approved hook against a synthetic payload, check JSON |
| approve + activate live (no restart) |
| revoke + detach live (no restart) |
| usage |
在网页/Telegram/微信中以纯文本输入(无需LLM,无成本):
| 命令 | 功能 |
|---|---|
| 显示总开关状态、配置路径、所有钩子及其批准/健康状态 |
| 切换总开关(热挂载/卸载,无需重启) |
| 使用模拟负载运行每个已批准的钩子,检查JSON格式 |
| 批准并实时激活(无需重启) |
| 撤销并实时停用(无需重启) |
| 显示使用说明 |
Events (9) and what each can do
9种事件及其功能
| Event | Fires | Capability | stdin gives the script |
|---|---|---|---|
| before a tool runs | block / rewrite input | |
| after a tool runs | observe (log/metrics) | |
| result before agent sees it | append a note | |
| before a model call | inject context / block | |
| after a model reply | observe | |
| before a TG/WeChat push | block / rewrite outbound | |
| agent claims "done" | refuse a fake completion | |
| session begins | observe / inject | |
| session ends | observe / cleanup | |
Every payload also includes , , , .
eventsession_idagent_idcwd| 事件 | 触发时机 | 能力 | 脚本从标准输入获取的内容 |
|---|---|---|---|
| 工具运行前 | 阻止/重写输入 | |
| 工具运行后 | 监控(日志/指标) | |
| 结果被Agent处理前 | 添加备注 | |
| 模型调用前 | 注入上下文/阻止 | |
| 模型回复后 | 监控 | |
| 推送至TG/微信前 | 阻止/重写出站内容 | |
| Agent声称“已完成”时 | 拒绝虚假完成声明 | |
| 会话开始时 | 监控/注入 | |
| 会话结束时 | 监控/清理 | |
所有负载还包含, , , 字段。
eventsession_idagent_idcwdOutput protocol (what the script prints on stdout)
输出协议(脚本在标准输出打印的内容)
JSON object, or empty for "continue". Fields:
jsonc
{"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 changecontextpre_llm_callsystemMessageadd_warningSafety: scripts run with + argv split (no shell injection) and a
per-hook timeout. A script that errors, times out, or prints non-JSON falls
through to continue — a broken hook can never break the agent.
shell=FalseJSON对象,或为空表示“继续执行”。字段说明:
jsonc
{"decision": "block", "reason": "..."} // 拒绝操作/拒绝完成声明
{"tool_input": {...}} // pre_tool_call:重写现有输入字段
{"notification": "..."} // on_outbound_message:重写消息内容
{"context": "..."} // pre_llm_call:注入提示(面向Agent)
{"systemMessage": "..."} // 允许执行,但向用户显示备注
{"add_warning": "..."} // 同用户备注渠道
<empty> // 继续执行,无修改contextpre_llm_callsystemMessageadd_warning安全特性:脚本以 + 参数拆分方式运行(无Shell注入风险),且每个钩子有超时限制。脚本报错、超时或打印非JSON内容时,会默认继续执行——损坏的钩子绝不会导致Agent故障。
shell=FalseConfig file format
配置文件格式
workspace/config/shell_hooks.yamlyaml
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 120workspace/config/shell_hooks.yamlyaml
hooks:
- event: pre_tool_call
matcher: "rm -rf|dd if=|mkfs" // 可选正则表达式;仅匹配时才启动脚本(性能优化)
command: ./extensions/shell_hooks/examples/block_secrets.py
timeout: 10 // 超时时间(秒),默认20秒,最大120秒Two hook transports
两种钩子传输方式
A hook is either a local command (default) or an HTTP endpoint — same
payload in, same decision JSON out, only the transport differs.
yaml
hooks:
- event: pre_tool_call
type: http # omit type -> "command" (default)
url: https://my-guard.example.com/hook
timeout: 10HTTP specifics:
- SSRF guard — the URL must be http(s) and must NOT resolve to a loopback /
private / link-local (incl. cloud metadata ) / reserved address (blocked at parse AND call time). Set
169.254.169.254only to intentionally hit a local service.STARCHILD_SHELL_HOOKS_HTTP_ALLOW_LOCAL=1 - Approval keys on the URL: ;
/hooks approve <event> <url>shows it as/hooks listand skips the executable/mtime checks.POST <url>
钩子可以是本地命令(默认)或HTTP端点——输入负载和输出决策JSON相同,仅传输方式不同。
yaml
hooks:
- event: pre_tool_call
type: http // 省略type则默认是"command"
url: https://my-guard.example.com/hook
timeout: 10HTTP细节:
- SSRF防护 —— URL必须是http(s)协议,且不能解析为环回/私有/链路本地(包括云元数据地址)/保留地址(解析和调用阶段都会拦截)。仅当需要访问本地服务时,才设置
169.254.169.254。STARCHILD_SHELL_HOOKS_HTTP_ALLOW_LOCAL=1 - URL中的批准标识:;
/hooks approve <event> <url>会显示为/hooks list,并跳过可执行文件/修改时间检查。POST <url>
Adding an LLM judgement (call the proxy, NOT /chat)
加入LLM判断(调用代理,而非/chat)
When a hook needs real reasoning ("does this leak a secret?", "is this
completion actually done?"), call an LLM directly through the proxy from your
script — never the agent's own .
/chatpython
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 errorWhy proxy-direct: OpenRouter is an external stateless API, so it does not
re-enter the agent loop or fire -> no recursion, one cheap
completion instead of a full agent turn, your own prompt + pure-JSON response.
Calling from a hook re-emits the same event (the bridge guards against
the loop, but it's needless overhead) — and an LLM hook that calls must
never sit on . See the host docs section
"Calling an LLM through the proxy".
pre_llm_call/chat/chatpre_llm_callsc-proxy.md当钩子需要真实推理能力时(如“这是否泄露了机密?”“这个完成声明是否真实?”),请从脚本中直接通过代理调用LLM——绝不要调用Agent自身的接口。
/chatpython
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", // 低成本默认模型(约$0.0002/调用)
"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"}, // 计费必填
timeout=40,
)
try:
print(json.dumps(json.loads(r.json()["choices"][0]["message"]["content"])))
except Exception:
print("{}") // 解析错误时默认允许执行为什么要直接调用代理:OpenRouter是外部无状态API,因此不会重新进入Agent循环或触发——无递归,仅需一次低成本的完成调用,而非完整的Agent轮次,且可使用自定义提示和纯JSON响应。从钩子调用会重新触发相同事件(虽然有防护避免循环,但会产生不必要的开销)——且基于LLM的钩子绝不能设置在事件上。详见主机文档中“通过代理调用LLM”章节。
pre_llm_call/chatpre_llm_callsc-proxy.mdStandard workflow (the agent's checklist)
标准流程(Agent操作清单)
- Clarify the rule and pick the event from the table above.
- Write the script — read JSON on stdin, print a decision on stdout.
Exit non-zero / non-JSON = continue. Make it executable ().
chmod +x - Add a config entry in (add a
workspace/config/shell_hooks.yamlregex when possible so the script only spawns when relevant).matcher - Dry-run it yourself with — pipe a sample JSON payload into the script and confirm it prints valid JSON. (The agent CANNOT run
bash— that is a user-typed command. Ask the user to run/hooksto verify after approval.)/hooks doctor - Hand the user the activation commands to paste:
/hooks approve <event> <command> /hooks on - Confirm with that it shows ✓ approved and live.
/hooks list
- 明确需求:确认规则并从上方表格中选择对应事件。
- 编写脚本:读取标准输入的JSON,打印决策到标准输出。非零退出码/非JSON输出=继续执行。设置脚本为可执行()。
chmod +x - 添加配置项:在中添加条目(尽可能添加
workspace/config/shell_hooks.yaml正则表达式,使脚本仅在相关场景下启动)。matcher - 自行通过试运行:将示例JSON负载传入脚本,确认输出有效的JSON。(Agent无法执行
bash命令——这是用户输入的命令。请用户在批准后运行/hooks进行验证。)/hooks doctor - 向用户提供激活指令:让用户直接粘贴以下指令:
/hooks approve <event> <command> /hooks on - 通过确认钩子显示为✓已批准且处于激活状态。
/hooks list
Ready-made example scripts
现成示例脚本
Shipped in-repo under (copy + adapt):
extensions/shell_hooks/examples/| Script | Event | Purpose |
|---|---|---|
| multi | private-key / seed-phrase guard (inbound warn + outbound/tool hard-block) |
| | block "claimed published but no publish tool ran / cited a fake URL" |
| | inject a context note when a published site exists |
For any other rule (block dangerous bash, audit tool calls, redact outbound
PII, warn on prod writes, ...), write a fresh script — the minimal block
example below is the template, and the output protocol above covers every
capability.
已随代码库发布在目录下(可复制并修改):
extensions/shell_hooks/examples/| 脚本 | 事件 | 用途 |
|---|---|---|
| 多事件 | 私钥/助记词防护(入站警告+出站/工具强阻止) |
| | 阻止“声称已发布但未执行发布工具/引用虚假URL”的情况 |
| | 当已发布网站存在时,注入上下文备注 |
对于其他规则(阻止危险bash命令、审计工具调用、编辑出站PII、警告生产环境写入等),请编写新脚本——下方的最小阻止示例是模板,上述输出协议涵盖了所有功能。
Minimal block example (pre_tool_call
, any language)
pre_tool_call最小阻止示例(pre_tool_call
,任意语言)
pre_tool_callbash
#!/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"refusing destructive command: {cmd}"}))
else:
print("{}") # continue
PYbash
#!/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"refusing destructive command: {cmd}"}))
else:
print("{}") # continue
PYClaude Code compatibility
Claude Code兼容性
Hook scripts written for Claude Code work unchanged — their output is
auto-translated into the fields above:
| Claude Code output | Translated to |
|---|---|
| |
| |
| |
| |
| |
| no-op (our stdout never enters the transcript) |
| exit code 2 with stderr, no stdout | |
Only the output payload is translated — event NAMES stay ours
(, not ).
pre_tool_callPreToolUse为Claude Code编写的钩子脚本可直接使用——其输出会自动转换为上述字段:
| Claude Code输出 | 转换为 |
|---|---|
| |
| |
| |
| |
| |
| 无操作(我们的标准输出绝不会进入对话记录) |
| 退出码2且有标准错误输出、无标准输出 | |
仅输出负载会被转换——事件名称仍使用我们的命名(,而非)。
pre_tool_callPreToolUseTroubleshooting "my hook never fires"
故障排查:“我的钩子从未触发”
- Is the event one of the 9 above? (a typo is a silent no-op)
- Is the master switch on? shows it;
/hooks listto enable./hooks on - Is the hook approved? in
✗ NOT approved→/hooks list./hooks approve - Does the regex actually match? Too narrow = never spawns.
matcher - Run — it flags non-executable / tampered / timed-out / non-JSON.
/hooks doctor
- 事件是否为上述9种之一?(拼写错误会导致静默失效)
- 总开关是否开启?会显示状态;可执行
/hooks list开启。/hooks on - 钩子是否已批准?中显示
/hooks list→ 执行✗ NOT approved。/hooks approve - 正则表达式是否真的匹配?范围过窄会导致脚本从未启动。
matcher - 运行——它会标记不可执行/被篡改/超时/非JSON的脚本。
/hooks doctor
Deep reference
深度参考
Full protocol, security model, and per-event payload detail live in the agent's
own docs: (read it for edge cases this
skill summarizes).
config/context/references/agent-hooks.md完整协议、安全模型和各事件负载细节请查看Agent自身文档:(本技能仅做总结,如需了解边缘情况请阅读该文档)。
config/context/references/agent-hooks.md