Skip to content

fix(goose): preserve builtin extensions in config.yaml and stop emitting dead hook matchers - #2579

Merged
dyoshikawa merged 3 commits into
mainfrom
resolve-issue-2404-goose-config-ownership
Aug 4, 2026
Merged

fix(goose): preserve builtin extensions in config.yaml and stop emitting dead hook matchers#2579
dyoshikawa merged 3 commits into
mainfrom
resolve-issue-2404-goose-config-ownership

Conversation

@dyoshikawa

Copy link
Copy Markdown
Owner

Fixes the three concrete Goose bugs recorded in the 2026-07-30 re-check comment on #2404. The open design item (gap 1, goose review checks-surface ownership) is untouched, so the issue stays open.

1. Global MCP generate deleted the user's builtin extensions (data loss)

GooseMcp.fromRulesyncMcp merged with { ...config, extensions: convertToGooseFormat(...) }, replacing the entire extensions: map. Starting from a config.yaml holding the standard developer and memory builtins, a global MCP generate produced a file containing only the rulesync servers — and losing developer costs the agent its shell and text-editor tools.

~/.config/goose/config.yaml is now declared in the shared-config gateway with mcp owning extensions, and the block is recomputed from the existing file before being applied: entries whose type is not one of stdio / streamable_http / sse (Goose's own builtin / platform / frontend / inline_python extensions) are carried over untouched, while every MCP-typed entry stays rulesync-owned, so a server deleted from .rulesync/.mcp.json is still retracted. Every other Goose setting (model, provider, ...) is preserved by the gateway's replace-owned-keys policy, and the file is declared invalidRootPolicy: "error" so a config that cannot be parsed aborts the write instead of being replaced.

2. Import rewrote builtin / platform extensions as broken stdio entries

developer: {type: builtin, bundled: true, timeout: 300} imported to a type-less server and regenerated as {name: developer, type: stdio, timeout: 300} with no cmd — a working builtin turned into an extension Goose cannot start. Non-MCP extension types are now skipped on import with a warning (they stay in config.yaml, which generation preserves), and generate never synthesizes a stdio extension without cmd: a canonical server with neither command nor url is skipped with a warning instead.

3. A "*" hook matcher silently killed the whole Goose hook rule

Goose compiles matcher with Regex::new and, 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 wildcardMatcherMeansAll option — the same "treat "*" as no matcher" rule amp-plugin-generator.ts, opencode-style-generator.ts, pi-extension-generator.ts and vibe-hooks.ts already apply — and goose-hooks.ts opts 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 cicheck is green, plus e2e-mcp.spec.ts and e2e-hooks.spec.ts.

Part of #2404

🤖 Generated with Claude Code

…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.
@dyoshikawa
dyoshikawa merged commit 184be17 into main Aug 4, 2026
9 checks passed
@dyoshikawa
dyoshikawa deleted the resolve-issue-2404-goose-config-ownership branch August 4, 2026 06:34
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