diagnosing-hooks
Skill6 starsChanged 8 days agoMITView on GitHub
Diagnose Hermes hooks that do not fire — gateway HOOK.yaml hooks, plugin hooks, shell hooks stuck on consent, and outbound webhooks — using hermes hooks doctor.
In this repository32 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- diagnosing-authSkill
- diagnosing-backupSkill
- diagnosing-bot-modeSkill
- diagnosing-browserSkill
- diagnosing-cli-tuiSkill
- diagnosing-commandsSkill
- diagnosing-configSkill
- diagnosing-cronSkill
- diagnosing-desktopSkill
- diagnosing-gatewaySkill
- diagnosing-host-pressureSkill
- diagnosing-mcpSkill
- diagnosing-memoriesSkill
- diagnosing-memorySkill
- diagnosing-pathSkill
- diagnosing-pluginsSkill
- diagnosing-profilesSkill
- diagnosing-providersSkill
- diagnosing-runtimeSkill
- diagnosing-secretsSkill
- diagnosing-sessionsSkill
- diagnosing-skillsSkill
- diagnosing-storageSkill
- diagnosing-triageSkill
- diagnosing-updateSkill
- diagnosing-voiceSkill
- hermes-configuration-guideSkill
- hermes-post-updateSkill
- installing-hermesSkill
- plugin-authoringSkill
The file
We split this skill into the rules it gives an agent
1 rule. About 0 are common, 0 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 50
After a timeout the same callback is suppressed for 60s (_HOOK_TIMEOUT_SUPPRESSION_SECONDS = 60.0, same module), so a hung plugin cannot …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Diagnosing Hooks
Hermes has four separate hook systems. Most "my hook doesn't run" reports are a system/surface mismatch or a consent issue, not code. Identify the system first:
| System | Declared in | Runs in | Can block/inject |
| Gateway hooks | $HERMES_HOME/hooks/<name>/HOOK.yaml + handler.py (async handle(event_type, context)) | Gateway only — never fires in the CLI | Mostly observer — command:* can deny/rewrite/handle |
| Plugin hooks | ctx.register_hook(<event>, cb) in a plugin's register() | CLI + gateway | Yes (pre_tool_call block, pre_llm_call context, pre_verify continue) |
| Shell hooks | hooks: block in config.yaml, pointing at a script (convention: $HERMES_HOME/agent-hooks/) | CLI + gateway | Yes (same events as plugin hooks) |
| Outbound webhooks | hooks.outbound: list in config.yaml | CLI + gateway | No — HTTP push only, response ignored |
Valid hook-event names are the VALID_HOOKS set in hermes_cli/plugins.py (the count grows across releases — verify against the installed source rather than a hardcoded number). Gateway hooks use a different event vocabulary (gateway:startup, session:start, session:end, session:reset, agent:start|step|end, command:*, reaction:*).
1. Shell-hook essentials (the most common breakage)
hooks:
pre_tool_call:
- matcher: "terminal" # regex; pre/post_tool_call only
command: "~/.hermes/agent-hooks/scan.sh"
timeout: 10 # default 60, capped at 300
fail_closed: true # pre_tool_call only; blocks on failureProtocol: JSON payload on stdin, optional JSON response on stdout. Exit code 2 blocks the tool call (agent/shell_hooks.py: BLOCK_EXIT_CODE = 2, honoured only for blocking events) — Claude-Code compatible. Timeout defaults/caps are DEFAULT_TIMEOUT_SECONDS = 60 and MAX_TIMEOUT_SECONDS = 300 in the same module. Block shapes: {"decision": "block", "reason": ...} or {"action": "block", "message": ...}; context injection: {"context": "..."} for pre_llm_call.
Plugin-hook callback timeouts (how they fail)
Python plugin callbacks (ctx.register_hook(cb)) get a default 30s wall-clock timeout
(plugins.hook_callback_timeout overrides it; hard cap 600s) — hermes_cli/plugins_dispatch.py:
_HOOK_CALLBACK_TIMEOUT_SECS = 30.0, _MAX_HOOK_CALLBACK_TIMEOUT_SECS = 600.0. How a timeout resolves depends
on the hook class:
| Hook class | On timeout |
Policy hooks — pre_tool_call | fail closed: the tool is blocked (pre_tool_call plugin callback timed out or is still running) |
| Bounded hooks | fail open: the callback is abandoned, the agent continues |
| Low-frequency lifecycle hooks | intentionally unbounded |
After a timeout the same callback is suppressed for 60s (_HOOK_TIMEOUT_SUPPRESSION_SECONDS = 60.0, same module), so a hung plugin cannot re-fire immediately.
Consent gate: each unique (event, command) pair prompts once, then persists to $HERMES_HOME/shell-hooks-allowlist.json (ALLOWLIST_FILENAME; schema {"approvals": [...]}, verified). On non-TTY runs (gateway, cron, CI) an unapproved hook silently stays unregistered — bypass with --accept-hooks, HERMES_ACCEPT_HOOKS=1, or hooks_auto_accept: true, or hand-edit the allowlist (approvals array with exact event + command strings — a sha256-keyed object is the wrong format).
2. How to inspect
hermes hooks list— configured shell + outbound hooks, consent status, signed/unsigned.hermes hooks test <event> [--for-tool X] [--payload-file F]— fire matching hooks against a synthetic payload; an invalid event name prints the valid set.hermes hooks doctor— per hook: exec bit, allowlist state, script mtime drift, JSON validity, rough runtime. Run this first for any shell-hook report.hermes hooks revoke <command>— remove allowlist entries.hermes logs --follow— hook errors are logged and isolated; a broken hook never crashes the agent (which also means it fails quietly).
3. Pitfalls (symptom → cause → fix)
- Hook never fires — (a) gateway hook used in a CLI session (gateway-only); (b) shell hook not on the consent allowlist after a non-TTY start; (c) event name typo (config parse prints "Did you mean X?" and skips); (d) plugin providing it is disabled. → Temporary: for (b), approve the pair at the interactive TTY prompt — start
hermes --cli(or barehermesfor the TUI) and accept the prompt for the specific hook (approves only the hook being diagnosed).--accept-hooks/HERMES_ACCEPT_HOOKS=1also approves, but grants lasting consent to every unapproved hook — prefer the prompt. Permanent: the one-time approval persists in the allowlist. Prefer the interactive TTY prompt (approves only the specific hook being diagnosed) over--accept-hooks(approves every unapproved hook) orhooks_auto_accept: true(turns the consent gate off for every hook on non-TTY surfaces). Then match system to surface;hermes hooks doctor;hermes plugins list. - Hook ran once, then edits do nothing — consent keys on the exact command string; script edits are silently trusted, but if you changed the command in config it's a new pair needing fresh consent. →
hermes hooks list; re-approve. - Block not blocking — exit code 2 or block JSON only works on
pre_tool_call; a plugin-registeredpre_tool_callmay have blocked first (plugins register before shell hooks; first valid block wins);fail_closedon other events is ignored with a warning; a timed-out pluginpre_tool_callcallback also blocks (policy hooks fail closed on timeout). → Scope the hook correctly. - Hook times out — timeouts over 300s are clamped; a slow script needs to be async. → Lower the work or raise
timeoutwithin the cap. - Stdout produces nothing / agent warns about bad JSON — print responses as single-line JSON (
printf '{"context": ...}'); stack traces on stdout are unparseable. Withfail_closed: trueonpre_tool_call, non-JSON stdout blocks the call — intended. - Outbound webhook not delivering — endpoints are fire-and-forget with one retry on 5xx/connection errors, no redirects followed, bounded queue. → Check
hermes hooks list(is it listed/signed?), receiver logs; usesecret_env+ verifyX-Hermes-Signature-256(HMAC-SHA256) on the receiver. - Gateway hook fires but handler errors — handler must be named
handle, take(event_type, context), errors are caught and logged. →hermes logsand fix the handler.
4. Localization workflow
- Which system? (directory hook → gateway;
hooks:in config → shell;hooks.outbound:→ webhook; plugin code → plugin hooks.) - Right surface? Gateway hooks need the gateway running (
hermes gateway status). hermes hooks doctor/hermes hooks list→ consent, exec bit, drift (pitfalls 1, 2).hermes hooks test <event>→ behavior under a synthetic payload (3, 5).- Apply the fix, restart the session/gateway, re-test.
- Re-run the detector:
hermes guide hooks --deepheadless or/hermes-doctor hooks --deepin-session;+means healthy,~means informational, andx/?means unresolved. The unqualifiedhermes guide hooks//hermes-doctor hooksinspects static state (config, consent, exec bit) and never executes a hook — use it when you only need to confirm registration. Add--deepwhen the diagnosis turns on runtime behavior (a hook that runs but misbehaves) or mtime drift, because that flag is what runshermes hooks doctor.
Verification
Shell hooks (registered via hermes hooks) are checked with hermes hooks list / hermes hooks doctor; gateway HOOK.yaml handlers use a separate event system and are checked via gateway logs, not these commands. Plugin hooks are checked via hermes plugins list and their callback output; outbound webhooks are fire-and-forget — verify them via receiver logs. Consent approval (allowed/not allowlisted) applies to shell hooks only — gateway, plugin, and outbound hooks do not use the shell consent allowlist, and outbound webhooks make no blocking decision.
# Confirm consent state (shell hooks only)
hermes hooks listPass: the shell hook's (event, command) pair shows allowed (not not allowlisted). Gateway/plugin/outbound hooks are not subject to this check.
Fail: still not allowlisted — start an interactive session (hermes --cli) and accept the TTY prompt for this specific hook.
# Verify executable bit (shell hooks only)
hermes hooks doctorPass: no exec bit missing / permission errors.
Fail: fix the script's executable bit (chmod +x) or permissions, then re-run.
# Optional: fire the hook against a synthetic payload (only with explicit approval — runs the hook)
hermes hooks test <event>Pass: hook executes without error; blocking hooks show the expected allow/block decision. An observer hook returning no stdout is a valid success (the JSON response is optional).
Fail: an error, or — for blocking hooks — an incorrect allow/block decision. Check hermes logs for the handler error and fix the script or handler.
Report
This skill diagnoses hooks that do not fire — gateway HOOK.yaml hooks, plugin hooks, shell hooks stuck on consent, and outbound webhooks. When you run the diagnostic workflow, present findings in the standard format below.
Summary
Your shell hook is not firing because it is not on the consent allowlist. The hook was registered during a non-TTY start (gateway/cron), so the consent prompt was skipped and the hook silently stayed unregistered.
Findings
| Severity | What | Evidence |
| HIGH | Shell hook not on consent allowlist | hermes hooks list shows consent: not approved for the hook's (event, command) pair |
| MEDIUM | Hook registered during non-TTY start | hermes logs shows hook registered during non-TTY run; consent required |
Recommended Fix
Approve the hook by starting an interactive session (hermes --cli, or bare hermes for the TUI) and accepting the TTY prompt for this specific (event, command) pair — the approval persists in the allowlist. hermes hooks test fires the hook but does not record approval. Avoid --accept-hooks (approves every unapproved hook) and hooks_auto_accept: true (disables the consent gate for all hooks on non-TTY surfaces).
References
$HERMES_HOME/shell-hooks-allowlist.json— consent allowlist fileagent/shell_hooks.py—BLOCK_EXIT_CODE = 2,ALLOWLIST_FILENAMEhermes_cli/plugins_dispatch.py— hook callback timeout logic
Facts re-verified 2026-10-10 against upstream source at commit b406d25f24462a61f01541069c90b78051c2c257 — correction: consent guidance now prefers interactive TTY approval of a specific hook over --accept-hooks (which approves all hooks) or hooks_auto_accept: true, which turns the consent gate off for every hook on non-TTY surfaces (hermes_cli/config_defaults.py, hermes_cli/oneshot.py). All other facts unchanged from the 2026-09-14 pass (46a0daee58abbc1b07f84f505a5ba90f1958295c); sources are cited inline so the next reviewer can re-verify fast. Detector wording corrected 2026-10-24: the plugin's default hooks check is in-process, so the recheck step now says --deep (which runs hermes hooks doctor) for runtime or drift questions.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

