Overview
Hermes Agent (Nous Research) has a first-classpre_tool_call hook that can run before built-in and plugin tools
and veto a call. Sigil Open Framework (SOF) registers a shell hook that
forwards each matched tool call to Sigil Sign /v1/authorize.
Coverage depends on the matcher, successful hook registration, and Hermes host
failure handling. Set Hermes fail_closed: true. Without that host setting, a
hook spawn failure, timeout, or malformed result can allow the tool to proceed
even when the Sigil request itself uses closed mode.
@sigilcore/agent-hooks ships a dedicated Hermes export:
createHermesPreToolCallHook. It normalizes Hermes hook payloads, maps common
tool names to Sigil actions, resolves task ids, and returns Hermesβ block shape
consistently.
HTTP note: the adapter promotes a web call to typed http only when its input explicitly contains a valid method. It does not infer GET; otherwise the call remains web_fetch.
Prerequisites
You need a Sigil API key and a signedwarranty.md policy file deployed to Sigil Sign.
- Get an API key: sigilcore.com/tools/keys
- Generate a policy: sigilcore.com/tools/warrant
1. Add the shell hook
Hermes shell hooks are declared in~/.hermes/config.yaml and run as
subprocesses when the matching event fires, in both CLI and gateway sessions.
matcher is a regex over the tool name. Widen or narrow it to match the
actions your policy governs.
2. Add the hook script
~/.hermes/agent-hooks/sigil-pre-tool-call.mjs:
SIGIL_API_KEY in your environment. On first use Hermes prompts once to
approve the (event, command) pair and persists the decision. For non-interactive
gateway or cron runs, pre-approve with HERMES_ACCEPT_HOOKS=1 or
hooks_auto_accept: true in config.yaml.
The adapter resolves task ids in this order: SIGIL_TASK_ID, config.taskId,
session_id, conversation_id, then run_id. ## execution_limits uses that
value to stop runaway tool loops within one task.
Model Budget Brakes
The shell hook above gates tool execution only. It does not see provider token usage by itself. To enforcemax_model_spend_usd_per_task or
max_model_tokens_per_task, the Hermes host or plugin must record provider
usage after model calls with recordModelUsage and call checkModelBudget with
the same task id.
How It Works
Hermes pipes a JSON payload to the hook onstdin and reads JSON back from
stdout. The script maps the Hermes tool name to a Sigil action type, submits the
intent to /v1/authorize, and on a DENIED or PENDING decision returns the
canonical block shape. Hermes then short-circuits the tool and hands the reason
back to the model as the tool error.
{"decision": "block", "reason": "..."} and
{"action": "block", "message": "..."}.
Tool Name Mapping
Plugin Hook Alternative
If you ship a Hermes plugin, you can register the same check in-process instead of as a subprocess. In your pluginβsregister():
Fail Mode
The script usesfailMode: 'closed', so the adapter returns a block if Sigil
Sign is unreachable. Hermes fail_closed: true separately blocks if the shell
hook cannot produce a valid result. Both settings are required for a closed
deployment, and they are not sufficient on their own: a script that throws
before it writes any output ends without a block response, and host handling
of that empty exit can vary by Hermes version. Wrap the script body in a
top-level try/catch that prints {"decision": "block", "reason": "hook error"} before a nonzero exit, and verify by test that a hook crash, a hook
timeout, and a Sign outage each block the tool before calling the deployment
closed.
Configuration
Troubleshooting a hook that never fires
A declared hook that never registers is the first thing to check, because nothing reports an error when it happens. The agent keeps running, no tool call is blocked, and Sigil looks installed while every action executes ungoverned. Two causes account for most of it. Keep thehooks: block in ~/.hermes/config.yaml. That is the active
config file and the one step 1 uses. Nousβs hooks page also refers to
cli-config.yaml in two places, which points at cli-config.yaml.example, a
commented reference file rather than a config Hermes loads. A hooks: block
placed there never registers and never raises an error, so the agent runs
ungoverned while the file looks correct.
Pre-approve the hook on any non-interactive run. Hermes prompts once to
approve each (event, command) pair and remembers the answer. A gateway, cron,
or CI run has no terminal to answer that prompt, and Nous states the consequence
plainly, which is that a newly added hook silently stays unregistered. Set one
of the three escape hatches before the first non-interactive run.
HERMES_ACCEPT_HOOKS=1in the environment--accept-hookson the CLI, for examplehermes --accept-hooks chathooks_auto_accept: truein the config file
HERMES_ACCEPT_HOOKS=1 on the specific service unit
over hooks_auto_accept: true, which blanket-approves every hook the config
can introduce.
Verify enforcement instead of assuming it. After install, run a tool call
your policy denies and confirm Hermes blocks it. A passing deny is the only
evidence that the hook registered. failMode: 'closed' does not cover this
case, because a hook that never runs has no fail mode to apply.
Source
- github.com/Sigil-Core/agent-hooks β TypeScript package, MIT License
- Hermes Agent Event Hooks documentation