agent-hooks

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Agent 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_file
,
write_file
,
bash
Shell钩子允许用户在Agent生命周期的固定节点运行自定义脚本——用于阻止危险操作、重写输入或出站消息、向模型注入上下文,或是向用户发出警告。脚本可以用任意语言编写;它通过简单的“标准输入传JSON、标准输出传JSON”协议与Agent交互。
工具:
read_file
,
write_file
,
bash

When 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
    rm -rf
    / destructive bash" →
    pre_tool_call
    block
  • "Never let a private key get pushed to Telegram" →
    on_outbound_message
    block
  • "Log every tool call for audit" →
    post_tool_call
    observe
  • "Remind the agent of X at the start of every model call" →
    pre_llm_call
    context
  • "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自动执行规则或响应事件,而非每次都需要手动触发时,就可以使用钩子。示例:
  • “阻止我执行
    rm -rf
    这类破坏性bash命令” →
    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)
/hooks approve <event> <command>
Write/extend
workspace/config/shell_hooks.yaml
/hooks on
(or the Preferences toggle)
Dry-run the script via
bash
/hooks doctor
(verify)
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
/hooks approve …
and
/hooks on
commands to paste.
Agent负责准备所有内容;用户负责激活。 这种分工是刻意设计的安全特性——斜杠命令在LLM处理前就会被解析,因此提示注入无法伪造激活指令。
Agent在对话中完成的操作用户需输入的激活指令
编写钩子脚本(任意语言)
/hooks approve <event> <command>
编写/扩展
workspace/config/shell_hooks.yaml
/hooks on
(或通过偏好设置开关)
通过
bash
试运行脚本
/hooks doctor
(验证)
解释/调试
钩子会在容器中执行任意代码,因此**“谁可以激活”**的权限仅对真实人类开放。最后务必向用户提供可直接粘贴的
/hooks approve …
/hooks on
指令。

The two gates

双重启用条件

A hook fires only when BOTH hold:
  1. Master switch ON
    shell_hooks.enabled: true
    in
    workspace/config/agent.yaml
    (flipped by
    /hooks on|off
    or the Preferences toggle; legacy env
    STARCHILD_SHELL_HOOKS=1
    forces it on as a fallback).
  2. Per-hook approval — each
    (event, command)
    pair approved once via
    /hooks approve
    (recorded with the script's mtime, so a later edit is flagged "changed since approval" — a swap-the-script attack is visible).
Switch on but unapproved → inert (shows ✗ in
/hooks list
). Approved but switch off → inert until
/hooks on
.
只有同时满足以下两个条件时,钩子才会触发:
  1. 总开关开启 ——
    workspace/config/agent.yaml
    shell_hooks.enabled: true
    (可通过
    /hooks on|off
    或偏好设置开关切换;旧版环境变量
    STARCHILD_SHELL_HOOKS=1
    可强制开启作为备用方案)。
  2. 单钩子已批准 —— 每个
    (event, command)
    配对需通过
    /hooks approve
    批准一次(记录脚本的修改时间,因此后续编辑会被标记为“自批准后已更改”——脚本替换攻击会被识别)。
总开关开启但未批准 → 无效(
/hooks list
中显示✗)。已批准但总开关关闭 → 无效,直到执行
/hooks on

The
/hooks
command

/hooks
命令

Plain text on web / Telegram / WeChat (no LLM, no cost):
CommandWhat it does
/hooks
or
/hooks list
master switch state, config path, every hook + approval/health
/hooks on
|
/hooks off
flip the master switch (hot mount/unmount, no restart)
/hooks doctor
run each approved hook against a synthetic payload, check JSON
/hooks approve <event> <command>
approve + activate live (no restart)
/hooks revoke <command>
revoke + detach live (no restart)
/hooks help
usage
在网页/Telegram/微信中以纯文本输入(无需LLM,无成本):
命令功能
/hooks
/hooks list
显示总开关状态、配置路径、所有钩子及其批准/健康状态
/hooks on
|
/hooks off
切换总开关(热挂载/卸载,无需重启)
/hooks doctor
使用模拟负载运行每个已批准的钩子,检查JSON格式
/hooks approve <event> <command>
批准并实时激活(无需重启)
/hooks revoke <command>
撤销并实时停用(无需重启)
/hooks help
显示使用说明

Events (9) and what each can do

9种事件及其功能

EventFiresCapabilitystdin gives the script
pre_tool_call
before a tool runsblock / rewrite input
tool_name
,
tool_input
post_tool_call
after a tool runsobserve (log/metrics)
tool_name
,
tool_result
transform_tool_result
result before agent sees itappend a note
tool_name
,
tool_result
pre_llm_call
before a model callinject context / block
system
,
last_user_message
,
model
post_llm_call
after a model replyobserve
model
on_outbound_message
before a TG/WeChat pushblock / rewrite outbound
notification
,
type
on_completion_claim
agent claims "done"refuse a fake completion
goal
,
summary
,
response
,
tool_names
on_session_start
session beginsobserve / inject
status
on_session_end
session endsobserve / cleanup
status
Every payload also includes
event
,
session_id
,
agent_id
,
cwd
.
事件触发时机能力脚本从标准输入获取的内容
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_outbound_message
推送至TG/微信前阻止/重写出站内容
notification
,
type
on_completion_claim
Agent声称“已完成”时拒绝虚假完成声明
goal
,
summary
,
response
,
tool_names
on_session_start
会话开始时监控/注入
status
on_session_end
会话结束时监控/清理
status
所有负载还包含
event
,
session_id
,
agent_id
,
cwd
字段。

Output 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 change
context
is agent-facing
(goes into the prompt,
pre_llm_call
only).
systemMessage
/
add_warning
is user-facing
(shown to the human on the tool-result / completion / outbound surfaces) — never injected into the prompt.
Safety: scripts run with
shell=False
+ 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.
JSON对象,或为空表示“继续执行”。字段说明:
jsonc
{"decision": "block", "reason": "..."}   // 拒绝操作/拒绝完成声明
{"tool_input": {...}}                     // pre_tool_call:重写现有输入字段
{"notification": "..."}                   // on_outbound_message:重写消息内容
{"context": "..."}                        // pre_llm_call:注入提示(面向Agent)
{"systemMessage": "..."}                  // 允许执行,但向用户显示备注
{"add_warning": "..."}                    //   同用户备注渠道
<empty>                                   // 继续执行,无修改
context
面向Agent
(注入提示,仅
pre_llm_call
可用)。
systemMessage
/
add_warning
面向用户
(在工具结果/完成声明/出站内容界面显示给人类)——绝不会注入到提示中。
安全特性:脚本以
shell=False
+ 参数拆分方式运行(无Shell注入风险),且每个钩子有超时限制。脚本报错、超时或打印非JSON内容时,会默认继续执行——损坏的钩子绝不会导致Agent故障。

Config file format

配置文件格式

workspace/config/shell_hooks.yaml
:
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
workspace/config/shell_hooks.yaml
:
yaml
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: 10
HTTP specifics:
  • SSRF guard — the URL must be http(s) and must NOT resolve to a loopback / private / link-local (incl. cloud metadata
    169.254.169.254
    ) / reserved address (blocked at parse AND call time). Set
    STARCHILD_SHELL_HOOKS_HTTP_ALLOW_LOCAL=1
    only to intentionally hit a local service.
  • Approval keys on the URL:
    /hooks approve <event> <url>
    ;
    /hooks list
    shows it as
    POST <url>
    and skips the executable/mtime checks.
钩子可以是本地命令(默认)或HTTP端点——输入负载和输出决策JSON相同,仅传输方式不同。
yaml
hooks:
  - event: pre_tool_call
    type: http                          // 省略type则默认是"command"
    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>
    ,并跳过可执行文件/修改时间检查。

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
/chat
.
python
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
Why proxy-direct: OpenRouter is an external stateless API, so it does not re-enter the agent loop or fire
pre_llm_call
-> no recursion, one cheap completion instead of a full agent turn, your own prompt + pure-JSON response. Calling
/chat
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
/chat
must never sit on
pre_llm_call
. See the host docs
sc-proxy.md
section "Calling an LLM through the proxy".
当钩子需要真实推理能力时(如“这是否泄露了机密?”“这个完成声明是否真实?”),请从脚本中直接通过代理调用LLM——绝不要调用Agent自身的
/chat
接口。
python
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循环或触发
pre_llm_call
——无递归,仅需一次低成本的完成调用,而非完整的Agent轮次,且可使用自定义提示和纯JSON响应。从钩子调用
/chat
会重新触发相同事件(虽然有防护避免循环,但会产生不必要的开销)——且基于LLM的钩子绝不能设置在
pre_llm_call
事件上。详见主机文档
sc-proxy.md
中“通过代理调用LLM”章节。

Standard workflow (the agent's checklist)

标准流程(Agent操作清单)

  1. Clarify the rule and pick the event from the table above.
  2. Write the script — read JSON on stdin, print a decision on stdout. Exit non-zero / non-JSON = continue. Make it executable (
    chmod +x
    ).
  3. Add a config entry in
    workspace/config/shell_hooks.yaml
    (add a
    matcher
    regex when possible so the script only spawns when relevant).
  4. Dry-run it yourself with
    bash
    — pipe a sample JSON payload into the script and confirm it prints valid JSON. (The agent CANNOT run
    /hooks
    — that is a user-typed command. Ask the user to run
    /hooks doctor
    to verify after approval.)
  5. Hand the user the activation commands to paste:
    /hooks approve <event> <command>
    /hooks on
  6. Confirm with
    /hooks list
    that it shows ✓ approved and live.
  1. 明确需求:确认规则并从上方表格中选择对应事件。
  2. 编写脚本:读取标准输入的JSON,打印决策到标准输出。非零退出码/非JSON输出=继续执行。设置脚本为可执行(
    chmod +x
    )。
  3. 添加配置项:在
    workspace/config/shell_hooks.yaml
    中添加条目(尽可能添加
    matcher
    正则表达式,使脚本仅在相关场景下启动)。
  4. 自行通过
    bash
    试运行
    :将示例JSON负载传入脚本,确认输出有效的JSON。(Agent无法执行
    /hooks
    命令——这是用户输入的命令。请用户在批准后运行
    /hooks doctor
    进行验证。)
  5. 向用户提供激活指令:让用户直接粘贴以下指令:
    /hooks approve <event> <command>
    /hooks on
  6. 通过
    /hooks list
    确认钩子显示为✓已批准且处于激活状态。

Ready-made example scripts

现成示例脚本

Shipped in-repo under
extensions/shell_hooks/examples/
(copy + adapt):
ScriptEventPurpose
block_secrets.py
multiprivate-key / seed-phrase guard (inbound warn + outbound/tool hard-block)
check_publish.sh
on_completion_claim
block "claimed published but no publish tool ran / cited a fake URL"
inject_website_reminder.sh
pre_llm_call
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/
目录下(可复制并修改):
脚本事件用途
block_secrets.py
多事件私钥/助记词防护(入站警告+出站/工具强阻止)
check_publish.sh
on_completion_claim
阻止“声称已发布但未执行发布工具/引用虚假URL”的情况
inject_website_reminder.sh
pre_llm_call
当已发布网站存在时,注入上下文备注
对于其他规则(阻止危险bash命令、审计工具调用、编辑出站PII、警告生产环境写入等),请编写新脚本——下方的最小阻止示例是模板,上述输出协议涵盖了所有功能。

Minimal block example (
pre_tool_call
, any language)

最小阻止示例(
pre_tool_call
,任意语言)

bash
#!/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
PY
bash
#!/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
PY

Claude Code compatibility

Claude Code兼容性

Hook scripts written for Claude Code work unchanged — their output is auto-translated into the fields above:
Claude Code outputTranslated to
hookSpecificOutput.permissionDecision: "deny"
(+
permissionDecisionReason
)
decision: block
(+
reason
)
hookSpecificOutput.additionalContext
context
hookSpecificOutput.updatedInput
tool_input
(rewrite)
continue: false
(+
stopReason
)
decision: block
(+
reason
)
systemMessage
add_warning
(user-facing note)
suppressOutput
no-op (our stdout never enters the transcript)
exit code 2 with stderr, no stdout
decision: block
, stderr is the reason
Only the output payload is translated — event NAMES stay ours (
pre_tool_call
, not
PreToolUse
).
Claude Code编写的钩子脚本可直接使用——其输出会自动转换为上述字段:
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
无操作(我们的标准输出绝不会进入对话记录)
退出码2且有标准错误输出、无标准输出
decision: block
,标准错误输出作为原因
仅输出负载会被转换——事件名称仍使用我们的命名(
pre_tool_call
,而非
PreToolUse
)。

Troubleshooting "my hook never fires"

故障排查:“我的钩子从未触发”

  1. Is the event one of the 9 above? (a typo is a silent no-op)
  2. Is the master switch on?
    /hooks list
    shows it;
    /hooks on
    to enable.
  3. Is the hook approved?
    ✗ NOT approved
    in
    /hooks list
    /hooks approve
    .
  4. Does the
    matcher
    regex actually match? Too narrow = never spawns.
  5. Run
    /hooks doctor
    — it flags non-executable / tampered / timed-out / non-JSON.
  1. 事件是否为上述9种之一?(拼写错误会导致静默失效)
  2. 总开关是否开启?
    /hooks list
    会显示状态;可执行
    /hooks on
    开启。
  3. 钩子是否已批准
    /hooks list
    中显示
    ✗ NOT approved
    → 执行
    /hooks approve
  4. matcher
    正则表达式是否真的匹配?范围过窄会导致脚本从未启动。
  5. 运行
    /hooks doctor
    ——它会标记不可执行/被篡改/超时/非JSON的脚本。

Deep reference

深度参考

Full protocol, security model, and per-event payload detail live in the agent's own docs:
config/context/references/agent-hooks.md
(read it for edge cases this skill summarizes).
完整协议、安全模型和各事件负载细节请查看Agent自身文档:
config/context/references/agent-hooks.md
(本技能仅做总结,如需了解边缘情况请阅读该文档)。