agentleFS
iapiap/hermes-guide/skills/diagnosing-hooks/SKILL.md

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.

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
  1. Very specific:

    After a timeout the same callback is suppressed for 60s (_HOOK_TIMEOUT_SUPPRESSION_SECONDS = 60.0, same module), so a hung plugin cannot …

    line 50

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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:

SystemDeclared inRuns inCan block/inject
Gateway hooks$HERMES_HOME/hooks/<name>/HOOK.yaml + handler.py (async handle(event_type, context))Gateway only — never fires in the CLIMostly observer — command:* can deny/rewrite/handle
Plugin hooksctx.register_hook(<event>, cb) in a plugin's register()CLI + gatewayYes (pre_tool_call block, pre_llm_call context, pre_verify continue)
Shell hookshooks: block in config.yaml, pointing at a script (convention: $HERMES_HOME/agent-hooks/)CLI + gatewayYes (same events as plugin hooks)
Outbound webhookshooks.outbound: list in config.yamlCLI + gatewayNo — 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 failure

Protocol: 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 classOn timeout
Policy hooks — pre_tool_callfail closed: the tool is blocked (pre_tool_call plugin callback timed out or is still running)
Bounded hooksfail open: the callback is abandoned, the agent continues
Low-frequency lifecycle hooksintentionally 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)

  1. 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 bare hermes for the TUI) and accept the prompt for the specific hook (approves only the hook being diagnosed). --accept-hooks / HERMES_ACCEPT_HOOKS=1 also 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) or hooks_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.
  2. 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.
  3. Block not blocking — exit code 2 or block JSON only works on pre_tool_call; a plugin-registered pre_tool_call may have blocked first (plugins register before shell hooks; first valid block wins); fail_closed on other events is ignored with a warning; a timed-out plugin pre_tool_call callback also blocks (policy hooks fail closed on timeout). → Scope the hook correctly.
  4. Hook times out — timeouts over 300s are clamped; a slow script needs to be async. → Lower the work or raise timeout within the cap.
  5. Stdout produces nothing / agent warns about bad JSON — print responses as single-line JSON (printf '{"context": ...}'); stack traces on stdout are unparseable. With fail_closed: true on pre_tool_call, non-JSON stdout blocks the call — intended.
  6. 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; use secret_env + verify X-Hermes-Signature-256 (HMAC-SHA256) on the receiver.
  7. Gateway hook fires but handler errors — handler must be named handle, take (event_type, context), errors are caught and logged. → hermes logs and fix the handler.

4. Localization workflow

  1. Which system? (directory hook → gateway; hooks: in config → shell; hooks.outbound: → webhook; plugin code → plugin hooks.)
  2. Right surface? Gateway hooks need the gateway running (hermes gateway status).
  3. hermes hooks doctor / hermes hooks list → consent, exec bit, drift (pitfalls 1, 2).
  4. hermes hooks test <event> → behavior under a synthetic payload (3, 5).
  5. Apply the fix, restart the session/gateway, re-test.
  6. Re-run the detector: hermes guide hooks --deep headless or /hermes-doctor hooks --deep in-session; + means healthy, ~ means informational, and x/? means unresolved. The unqualified hermes guide hooks / /hermes-doctor hooks inspects static state (config, consent, exec bit) and never executes a hook — use it when you only need to confirm registration. Add --deep when the diagnosis turns on runtime behavior (a hook that runs but misbehaves) or mtime drift, because that flag is what runs hermes 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 list

Pass: 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 doctor

Pass: 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

SeverityWhatEvidence
HIGHShell hook not on consent allowlisthermes hooks list shows consent: not approved for the hook's (event, command) pair
MEDIUMHook registered during non-TTY starthermes 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 file
  • agent/shell_hooks.py — BLOCK_EXIT_CODE = 2, ALLOWLIST_FILENAME
  • hermes_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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.