fix(goose): preserve builtin extensions in config.yaml and stop emitting dead hook matchers - #2579
Merged
Merged
Conversation
…x inert hook matchers Global MCP generate replaced the entire extensions map in ~/.config/goose/config.yaml, deleting the user's developer/memory builtin extensions and leaving the agent without its shell and text-editor tools. The block now goes through the shared-config gateway: non-MCP extension types are carried over untouched while MCP-typed entries stay rulesync-owned, so a deleted server is still retracted. Import mirrored the same confusion: a builtin/platform entry came back as an stdio server with no command, which regenerated into an extension Goose cannot start. Non-MCP extension types are now skipped on import, and generate never writes a stdio extension without cmd. Goose compiles a hook matcher with Regex::new and drops the whole rule when it fails to compile, so the canonical catch-all matcher never ran. It is now emitted as no matcher, the same normalization four other targets already apply.
Review follow-up: only an entry rulesync can positively identify as an MCP server is rulesync's to drop, so an entry of an unknown shape or a future extension type survives a generate. Retracted MCP extensions are named in a warning, and the import-side skip is aggregated into one line instead of one per builtin.
…ates none, and stop warning about our own output Review follow-up: a canonical server that cannot be converted no longer removes the existing extension of the same name in silence (it is named in the retraction warning), and regenerating a non-MCP entry rulesync itself wrote no longer reports a collision on every generate.
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.
Fixes the three concrete Goose bugs recorded in the 2026-07-30 re-check comment on #2404. The open design item (gap 1,
goose reviewchecks-surface ownership) is untouched, so the issue stays open.1. Global MCP generate deleted the user's builtin extensions (data loss)
GooseMcp.fromRulesyncMcpmerged with{ ...config, extensions: convertToGooseFormat(...) }, replacing the entireextensions:map. Starting from aconfig.yamlholding the standarddeveloperandmemorybuiltins, a global MCP generate produced a file containing only the rulesync servers — and losingdevelopercosts the agent its shell and text-editor tools.~/.config/goose/config.yamlis now declared in the shared-config gateway withmcpowningextensions, and the block is recomputed from the existing file before being applied: entries whose type is not one ofstdio/streamable_http/sse(Goose's ownbuiltin/platform/frontend/inline_pythonextensions) are carried over untouched, while every MCP-typed entry stays rulesync-owned, so a server deleted from.rulesync/.mcp.jsonis still retracted. Every other Goose setting (model, provider, ...) is preserved by the gateway'sreplace-owned-keyspolicy, and the file is declaredinvalidRootPolicy: "error"so a config that cannot be parsed aborts the write instead of being replaced.2. Import rewrote
builtin/platformextensions as brokenstdioentriesdeveloper: {type: builtin, bundled: true, timeout: 300}imported to a type-less server and regenerated as{name: developer, type: stdio, timeout: 300}with nocmd— a working builtin turned into an extension Goose cannot start. Non-MCP extension types are now skipped on import with a warning (they stay inconfig.yaml, which generation preserves), and generate never synthesizes astdioextension withoutcmd: a canonical server with neithercommandnorurlis skipped with a warning instead.3. A
"*"hook matcher silently killed the whole Goose hook ruleGoose compiles
matcherwithRegex::newand, on failure, logs "Invalid hook matcher regex; skipping rule" and drops the entire rule; a bare"*"is not a valid regex, and the v1.44.0 docs state outright that it is silently skipped. rulesync emitted the canonical matcher verbatim, so a shared canonical hooks file produced dead hooks on Goose.The shared hooks converter gained a
wildcardMatcherMeansAlloption — the same "treat"*"as no matcher" ruleamp-plugin-generator.ts,opencode-style-generator.ts,pi-extension-generator.tsandvibe-hooks.tsalready apply — andgoose-hooks.tsopts in. It normalizes at grouping time, so a"*"group collapses into the matcher-less group rather than emitting a second indistinguishable bare entry.Verification
New unit tests cover builtin/platform preservation with retraction of a deleted MCP server, the command-less skip, the import skip, and the matcher collapse. Full
pnpm cicheckis green, pluse2e-mcp.spec.tsande2e-hooks.spec.ts.Part of #2404
🤖 Generated with Claude Code