fix(hermesagent): stop destroying hooks.outbound and model the v0.20.0 MCP keys - #2610
Merged
Merged
Conversation
…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.
This was referenced Aug 7, 2026
Merged
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the two v0.20.0 gaps from the 2026-08-06 re-check comment on #2414.
1.
hooks.outboundwas 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.tsdeclaredhooks: { kind: "replace-owned-keys", ownedKeys: ["hooks"] }, so the whole mapping was replaced and a user'shooks.outbounddisappeared with no warning. Unlike a native event key,outboundis 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 thepluginswriters 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
outboundkey outright. Its entries carry nocommand, 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) andelicitation(mapping:enabled,timeout) now round-trip throughcopyHermesAdvancedFields,elicitationas an opaque object likesampling.transportis a real upstream key now ("Set tosseto use the SSE transport instead of Streamable HTTP"), but the adapter still stripped it as a canonical-only alias, so a canonicaltype: "sse"server was emitted as a bareurl:entry and Hermes connected over Streamable HTTP. A canonical SSE server is now written astransport: sseand imports back astype: "sse". Streamable HTTP is Hermes's default, so it stays implicit —sseis the only value upstream reads.3. Compat metadata
Re-pinned from v0.19.0 to v0.20.0 in
docs/reference/supported-tools.mdand thehermesagent-hooks.tsruntime 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.tshooks.outboundsurvives 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 (withtransport: sselanding on the canonicaltype).Part of #2414
🤖 Generated with Claude Code