Skip to content

feat(agents): wire gortex into Hermes pre_tool_call hooks (graph-first enforcement, parity with Claude Code) #51

@rolldav

Description

@rolldav

Follow-up to #42 (the Hermes adapter I QA'd). The merged adapter wires the gortex MCP server + skills into ~/.hermes, but not hooks — whereas the Claude Code adapter additionally installs PreToolUse hooks (--hook-mode deny|enrich|consult-unlock|nudge, per gortex install --help) that redirect Grep/Glob/Read of indexed source to graph tools. Hermes ships an equivalent primitive, so this is a clean parity gap.

Hermes supports exactly the right primitive

Verified against Hermes Agent v0.15.1, official docs website/docs/user-guide/features/hooks.md:

  • pre_tool_call shell hooks, declared in the hooks: block of ~/.hermes/config.yaml, can block a tool call. (hooks: {} is also present in every per-profile config.yaml — same shape as mcp_servers.)
  • JSON wire protocol: Hermes pipes {"hook_event_name","tool_name","tool_input","session_id","cwd","extra"} to the hook script's stdin and reads a decision from stdout.
  • It already accepts the Claude-Code block shape verbatim — the docs list both as equivalent:
    {"decision": "block", "reason":  "..."}   // Claude-Code style
    {"action":   "block", "message": "..."}   // Hermes-canonical
    So gortex's existing Claude Code hook output needs little-to-no translation.
  • Matcher: optional regex on tool_name, used for pre_tool_call / post_tool_call.
  • pre_llm_call is the documented UserPromptSubmit equivalent (context injection) — covers an enrich posture. The docs say so explicitly: "Claude Code's UserPromptSubmit event is intentionally not a separate Hermes event — pre_llm_call fires at the same place and already supports context injection."
  • The hermes hooks CLI (list / test / doctor) is built-in for verification.

Proposal

Extend the Hermes adapter so gortex install --agents=hermes [--hooks --hook-mode ...] upserts a pre_tool_call shell hook (and, for enrich, a pre_llm_call hook) into the hooks: block, mirroring the Claude Code treatment.

Tool-name mapping (Hermes's tool surface differs from Claude Code's):

  • File reads → read_file (matcher read_file): block + redirect to get_symbol_source / graph read_file when the path is indexed source.
  • grep / find / cat → inside the terminal tool (matcher terminal): inspect tool_input.command for grep|rg|find|cat against indexed paths and redirect to search_symbols / find_usages. Hermes has no separate Grep/Glob tool — that shell-tool indirection is the one structural difference from the Claude Code adapter.

The hook script asks the running daemon whether the target is indexed (the same decision the Claude Code hook already makes) and returns the block-with-redirect message.

Why it's low-cost

  • The stdout protocol already speaks the Claude-Code {"decision":"block","reason":...} shape → reuse the existing hook's output path.
  • The daemon is already running; the indexed-or-not decision is identical to Claude Code.
  • The hooks: write rides the same atomic-write + comment-preserving MergeYAML the adapter just added for mcp_servers.

Caveats / open questions (flagging honestly)

  1. Consent: shell hooks require first-use consent. Non-TTY contexts (gateway, cron) need --accept-hooks, HERMES_ACCEPT_HOOKS=1, or hooks_auto_accept: true, else a newly-added hook silently stays unregistered (and logs a warning). The installer should at least surface this in its output / docs.
  2. Security: the hooks: block runs scripts with full user credentials. Writing into it is privileged — keep it opt-in (--hooks) and log exactly what was written, same posture as the Claude Code adapter.
  3. Per-profile vs global: hooks: {} is present in both the global and every profile config here (same shape as mcp_servers), but the docs only document the global config.yaml. Worth confirming whether per-profile shell hooks are actually honored before deciding per-profile vs global-only. If global-only, this is simpler than the mcp_servers per-profile dance.
  4. Script location: docs convention is ~/.hermes/agent-hooks/; the installer should drop the script there and reference it by path.

Acceptance

  • gortex install --agents=hermes --hooks writes a valid pre_tool_call (and optional pre_llm_call) entry; --no-hooks skips; idempotent re-run is a no-op.
  • hermes hooks list shows the entry; hermes hooks test pre_tool_call --for-tool read_file returns a block→redirect for an indexed path and {} otherwise.
  • init doctor reports it; the uninstall note covers it.

Environment: Hermes Agent v0.15.1 · gortex v0.35.3 · docs website/docs/user-guide/features/hooks.md.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions