Agent Hooks
Manage shell hooks — user scripts that run at agent lifecycle points to block, rewrite, or warn on actions, via the /hooks command.
- Skill ID
- starchild-ai-agent/official-skills/agent-hooks
- Publisher
- starchild-ai-agent
- Repository
- official-skills
- Installs
- 0
- Files
- 11
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
starchild-ai-agent/official-skills/agent-hooksInstalls these files- SKILL.md
- templates/runtime_footer.py
- templates/runtime_footer_selftest.py
- templates/security_guard.py
- templates/security_guard_selftest.py
- templates/verify_code_changes.py
- templates/verify_code_changes_selftest.py
- templates/verify_commitments.py
- templates/verify_commitments_selftest.py
- templates/verify_publish_claims.py
- templates/verify_publish_claims_selftest.py
What this skill tells the agent
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
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_callblock - "Never let a private key get pushed to Telegram" →
on_outbound_messageblock - "Log every tool call for audit" →
post_tool_callobserve - "Remind the agent of X at the start of every model call" →
pre_llm_callcontext - "Don't let the agent claim it published when it didn't" →
on_completion_claim(in/goal) oron_stop(in normal chat) - "If the answer fails my quality check, make the agent redo it" →
on_stopblock
If the user just wants a one-off check, that's not a hook — hooks are for recurring, automatic lifecycle enforcement.
How configuration works (the agent does it end-to-end)
The agent installs and activates a hook with zero user copy-paste. Write the script, write the config entry, then call the loopback self-approve API — it flips the master switch on, approves the script for every event it's wired to, and hot-mounts it live (no restart). The user just tests it afterward.
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}⚠️ ALWAYS use an absolute path under `/data/workspace` in both the yamlcommand:and this call. Never a relative path likeskills/agent-hooks/templates/security_guard.py: the bridge spawns the script with the server cwd (/app), so a relative path resolves to/app/skills/…— an empty dir — and every spawn fails. Because the bridge fails OPEN (a script it can't run = "continue"), the guard then silently protects nothing while/hooks liststill shows it "mounted". To avoid this, the standard install copies the template into `/data/workspace/hooks/` and points the yaml there (see the workflow below)./app/skillsis NOT the skills dir — the real one is/data/workspace/skills/(a.k.a./app/workspace/skills/via symlink).
commandMUST be the exactcommand:string from theshell_hooks.yamlentry (the absolute script path). The hook MUST already be declared in the yaml — approval flips a declared hook to live, it can't conjure one out of thin air.- The endpoint is loopback-only (same-uid-in-container trust boundary, same as
.envreads). It auto-enables the master switch (enable_masterdefaults true), so the hook fires immediately. - Approval records the script's mtime, so a later edit surfaces as drift in
/hooks list//hooks doctor— a swap-the-script change stays visible.
This is the whole activation story for the user — there isn't a second step. After the call returns {"ok": true}, tell them it's live and to test it. Do not mention /hooks approve, /hooks on, or "two gates" — those are internal.
Fallback (older builds only): if the curl returns 404, this runtime predates the self-approve API — only then fall back to asking the user to paste /hooks approve <command> then /hooks on.
The two gates (both handled by the self-approve API — you don't surface them)
Internally a hook fires only when BOTH hold; the self-approve call above flips both in one shot, so the user never sees or types either:
- Master switch ON —
shell_hooks.enabled: trueinworkspace/config/agent.yaml. The API auto-enables it (enable_masterdefaults true). - Per-hook approval — the
(event, command)pair is recorded in the allowlist with the script's mtime (so a later edit shows as "changed since approval" drift — a swapped script stays visible). The API approves every event the command is wired to.
These exist as a security boundary, not as a user step. Do NOT mention "approve" or "two gates" when explaining hooks to a user — just say you'll set it up and they can test it. (Manual /hooks on + /hooks approve exist only as the 404 fallback for older runtimes.)
The /hooks command
Plain text on web / Telegram / WeChat (no LLM, no cost):
| Command | What 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 |
Events (12) and what each can do
| Event | Fires | Capability | stdin gives the script |
|---|---|---|---|
on_user_message | a user message arrives, before the model sees it | block / rewrite text | message, channel |
pre_tool_call | before a tool runs | block / rewrite input | tool_name, tool_input |
post_tool_call | after a tool runs | observe (log/metrics) | tool_name, tool_result |
transform_tool_result | result before agent sees it | append a note | tool_name, tool_result |
pre_llm_call | before a model call | inject context | system, last_user_message, model |
post_llm_call | after a model reply | observe / swap | model |
on_response_end | final reply assembled, once per turn | rewrite reply | response, model, tokens, tool_names |
on_stop | turn boundary, after on_response_end | block → force a redo | response, tool_names, stop_hook_active |
on_outbound_message | before a TG/WeChat push | block / rewrite outbound | notification, type |
on_completion_claim | agent claims a /goal done | block → force a redo | goal, summary, response, tool_names |
on_session_start | session begins | observe | status |
on_session_end | session ends | observe / cleanup | status |
Every payload also includes event, session_id, agent_id, cwd.
The `event` field is the dispatch key. It names which lifecycle moment is firing (pre_tool_call, on_user_message, …). A multi-event script (like security_guard.py, one file wired to five events) reads event to decide which branch to run — no event in the payload means no branch matches, so the script falls through to "continue" (empty output = allow). The runtime always sets it; you only have to remember it when hand-crafting a test payload (see the dry-run step below). It is NOT something you put in shell_hooks.yaml — there the event: key tells the bus when to call you; the event field in the payload is the bus telling the script which moment it is.
The three "make the agent fix it" levers (don't mix them up)
These three fire near the end of a turn but have very different power — pick by what you need to happen when something's wrong:
| Event | Power | Use when |
|---|---|---|
on_response_end | rewrite only — edit the stored/forwarded reply (footer, redaction, mask). Cannot make the agent redo. Zero loop risk. | You only need to change the text (mask a leaked key, add a cost footer). |
More from starchild-ai-agent
Use this skill in a project
Open any RiverX project, search the Skills panel for starchild-ai-agent/official-skills/agent-hooks, and install it in one click.
