Skip to content

fix(hermesagent): stop destroying hooks.outbound and model the v0.20.0 MCP keys - #2610

Merged
dyoshikawa merged 2 commits into
mainfrom
resolve-issue-2414-hermes-outbound-mcp
Aug 7, 2026
Merged

fix(hermesagent): stop destroying hooks.outbound and model the v0.20.0 MCP keys#2610
dyoshikawa merged 2 commits into
mainfrom
resolve-issue-2414-hermes-outbound-mcp

Conversation

@dyoshikawa

Copy link
Copy Markdown
Owner

Summary

Implements the two v0.20.0 gaps from the 2026-08-06 re-check comment on #2414.

1. hooks.outbound was silently destroyed on every generate (highest severity)

Hermes v0.20.0 added an outbound webhook registry nested inside the same hooks: mapping Rulesync owns. shared-config-gateway.ts declared hooks: { kind: "replace-owned-keys", ownedKeys: ["hooks"] }, so the whole mapping was replaced and a user's hooks.outbound disappeared with no warning. Unlike a native event key, outbound is a list of webhook targets rather than a hook event, so it has no Rulesync spelling and no migration path — pure config loss.

Rather than adding a nested-key ownership form to the gateway, the hooks writer now recomputes the hooks: mapping before patching, the same way the plugins writers on the same file already recompute their key: every key that is not a native Hermes event is carried over from the existing file, while the event keys are replaced wholesale, so a hook deleted from the Rulesync source is still retracted. The gateway declaration is unchanged apart from a comment recording the narrowed ownership.

Import skips the outbound key outright. Its entries carry no command, so they were already dropped by the entry loop, but skipping the key means a future registry field can never be read as a hook event and written back in the wrong shape.

2. Three new per-server MCP keys were wiped on regenerate

Same data-loss shape as this issue's already-fixed gap 1, verified against the MCP config reference:

  • keepalive_interval (number, both transports — liveness ping cadence in seconds) and elicitation (mapping: enabled, timeout) now round-trip through copyHermesAdvancedFields, elicitation as an opaque object like sampling.
  • transport is a real upstream key now ("Set to sse to use the SSE transport instead of Streamable HTTP"), but the adapter still stripped it as a canonical-only alias, so a canonical type: "sse" server was emitted as a bare url: entry and Hermes connected over Streamable HTTP. A canonical SSE server is now written as transport: sse and imports back as type: "sse". Streamable HTTP is Hermes's default, so it stays implicit — sse is the only value upstream reads.

3. Compat metadata

Re-pinned from v0.19.0 to v0.20.0 in docs/reference/supported-tools.md and the hermesagent-hooks.ts runtime warning.

Still open on #2414

Modelling outbound webhooks as a first-class authoring surface is not in scope — this PR only stops Rulesync from deleting them. The issue's other design items (notably the shared-config transaction lock and project-scoped skills discussion) are untouched.

Testing

  • pnpm cicheck (full: code + content)
  • npx vitest run --config vitest.e2e.config.ts src/e2e/e2e-hooks.spec.ts src/e2e/e2e-mcp.spec.ts
  • New tests: hooks.outbound survives a regenerate that rewrites the event keys, a native event key removed from the source is still retracted, the three MCP keys generate, and they import back (with transport: sse landing on the canonical type).

Part of #2414

🤖 Generated with Claude Code

…0 MCP keys

Hermes v0.20.0 nests an outbound webhook registry inside the same hooks:
mapping rulesync owns, and the shared-config gateway replaced that mapping
wholesale, so every generate silently deleted a user's hooks.outbound. It is a
list of webhook targets rather than a hook event, so it has no rulesync
spelling and no migration path. The hooks writer now recomputes the mapping
from the existing file: keys that are not native Hermes events are carried over
untouched, while the event keys are still replaced wholesale so a hook deleted
from the rulesync source is retracted. Import skips the outbound key outright so
it can never be read as an event and written back in the wrong shape.

Also models the three v0.20.0 per-server MCP keys, which had the same data-loss
shape: keepalive_interval and elicitation round-trip through
copyHermesAdvancedFields, and a canonical sse server is now written as Hermes's
own transport: sse (without it Hermes connects over Streamable HTTP) and
imports back as type: sse.

Re-pins the compat metadata from v0.19.0 to v0.20.0.

Part of #2414.
…serving siblings

Addresses review findings on PR #2610. The sibling-preserving merge keyed off
'not a documented native event', which also covered the undocumented event
names rulesync itself emits from the hermesagent.hooks override for forward
compatibility. Those became unretractable: deleting one from the rulesync source
left it in config.yaml, still executing. Replacing the whole mapping used to
retract it, so that was a regression.

Both directions now ask one predicate, isHermesHookEventEntry: a documented
native event is always an event, and any other key is an event only when its
value looks like a hook list (a non-empty array of entries carrying a string
command). Everything rulesync writes matches that; a registry entry such as
hooks.outbound carries url/events and no command, so it is preserved. That also
replaces the outbound-specific check on the import side, so import and generate
cannot drift, and it keeps the next sibling key Hermes adds safe by default.

Extends the shared-file no-data-loss contract to seed hooks.outbound in the
Hermes config: the top-level sentinel alone is what let this class of bug ship.
Adds unit tests for the retraction case, for importing an outbound-only config,
and for an sse server spelled with the httpUrl alias, drops a spread that could
only ever throw at write time, and rewords the docs sentence.
@dyoshikawa
dyoshikawa merged commit 98cd872 into main Aug 7, 2026
9 checks passed
@dyoshikawa
dyoshikawa deleted the resolve-issue-2414-hermes-outbound-mcp branch August 7, 2026 10:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants