Skip to content

fix(cli): an unreadable extension-enablement config re-enables every extension - #29481

Open
lets-order-some-fries wants to merge 1 commit into
google-gemini:mainfrom
lets-order-some-fries:fix/extension-enablement-fail-open
Open

lets-order-some-fries wants to merge 1 commit into
google-gemini:mainfrom
lets-order-some-fries:fix/extension-enablement-fail-open

Conversation

@lets-order-some-fries

Copy link
Copy Markdown

Summary

An unreadable extension-enablement.json silently re-enables every extension the user
disabled
, and the next enable/disable/remove then erases the rest of the file.

Extensions can contribute MCP servers, tools, commands and hooks, so re-enabling one the
user switched off is a trust event, not a preference reset.

This is the same defect #29445 fixes for mcp-server-enablement.json, in the sibling
file. The two are worth reading together.

Details

readConfig() collapses a JSON.parse failure or a zod schema failure into the same {}
it returns for ENOENT:

} catch (error) {
  if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
    return {};
  }
  coreEvents.emitFeedback('error', 'Failed to read extension enablement config.', error);
  return {};
}

Downstream, isEnabled() starts from let enabled = true ("Extensions are enabled by
default"), so an empty config means everything is on. Then writeConfig() serialises
the whole map unconditionally —

fs.writeFileSync(this.configFilePath, JSON.stringify(config, null, 2));

— so enable() (and disable(), which delegates to it) and remove() persist that {}
plus one key, dropping every other entry.

What changed

  • readConfigResult() returns a result discriminated on ok / missing / unreadable.
    Only missing yields an empty config.
  • isEnabled() fails closed on unreadable.
  • enable() and remove() refuse to write, throwing ExtensionEnablementConfigError,
    so the file survives for the user to repair. Both call sites in extension-manager.ts
    already sit behind the try in handleDisable/handleEnable, which is how
    "Extension with name X does not exist." is already surfaced.
  • Valid JSON of the wrong shape is unreadable too — {"alpha": "disabled"} parses
    fine and fails open by the same route.
  • The error is reported once per stretch of failures rather than once per read, and
    now names the path and the remedy. isEnabled runs per extension per startup; on main
    a single extensions disable printed the message four times.
  • readConfig() keeps its signature for existing callers; every production path inside
    the class now goes through readConfigResult().

Related Issues

Same defect class as #29445 (mcp-server-enablement.json).

How to Validate

npx vitest run src/config/extensions/extensionEnablement.test.ts --root packages/cli

7 cases added; 6 fail without the source change. The seventh is a control asserting a
missing file still behaves exactly as before.

End to end, against a scratch home:

npm run bundle
CLI="$PWD/bundle/gemini.js"
export HOME=$(mktemp -d) && mkdir -p "$HOME/proj"
for n in alpha beta gamma; do
  mkdir -p "$HOME/.gemini/extensions/$n"
  printf '{"name":"%s","version":"1.0.0"}' "$n" > "$HOME/.gemini/extensions/$n/gemini-extension.json"
done
cd "$HOME/proj" && export GEMINI_CLI_TRUST_WORKSPACE=true

node "$CLI" extensions disable alpha
node "$CLI" extensions disable beta
node "$CLI" extensions list          # alpha/beta: Enabled (User): false

# truncate three bytes, as an interrupted write would
python3 -c "import io,sys;p=sys.argv[1];s=io.open(p).read();io.open(p,'w').write(s[:-3])" \
  "$HOME/.gemini/extensions/extension-enablement.json"

node "$CLI" extensions list          # on main: alpha/beta flip to true
node "$CLI" extensions disable gamma # on main: file now contains ONLY gamma

On main: the truncation flips alpha and beta to Enabled (User): true, and the
disable gamma afterwards leaves a file containing only gamma — both disables gone.

On this branch: both stay false, one error line names the file, and the write is refused
with the file byte-identical (verified by md5).

Suites on this branch: src/config/ + src/commands/extensions/ → 47 files, 1006
passed
. Full npm run test -w packages/cli → 465 of 466 files pass; the one failure
is src/gemini.test.tsx, which fails the same 9 tests on clean main in this checkout
with the identical FatalUntrustedWorkspaceError (my local checkout is not a trusted
folder). I compared the failure reason against a main run rather than just the count,
since two of those nine are hooks-and-trust tests and extensions contribute hooks.
npm run typecheck -w packages/cli and eslint packages/cli/src/config/extensions/ both
clean.

Pre-Merge Checklist

  • Updated relevant documentation and README (if needed) — no documented behaviour
    changes; extension-enablement.json is not described in /docs
  • Added/updated tests (if needed)
  • Noted breaking changes (if any) — behaviour change, not API: an unreadable config
    now disables extensions rather than silently enabling them, and enable/disable/remove
    refuse to write until it is repaired. A missing config behaves exactly as before.
  • Validated on required platforms/methods:
    • MacOS
      • npm run
      • npx
      • Docker
      • Podman
      • Seatbelt
    • Windows
      • npm run
      • npx
      • Docker
    • Linux
      • npm run
      • npx
      • Docker

Filesystem-and-JSON only; no platform-specific paths beyond
ExtensionStorage.getUserExtensionsDir(), which is untouched.

🤖 Generated with Claude Code

…hing

readConfig() collapsed a parse or schema failure into the same {} it uses
for ENOENT. Extensions default to enabled, so an unreadable file turns every
deliberate disable back on - and extensions can carry MCP servers, tools,
commands and hooks, so that is a trust event, not a preference reset.

writeConfig() then serialises the whole map unconditionally, so the next
enable/disable/remove persists that {} plus one key and drops every other
entry.

Reproduced on main: with alpha and beta disabled, truncating three bytes of
extension-enablement.json flips both to enabled, and `extensions disable
gamma` afterwards leaves a file containing only gamma. The read error was
also printed four times in one command.

readConfigResult() now discriminates ok/missing/unreadable; only missing
yields an empty config. isEnabled fails closed, enable() and remove() throw
ExtensionEnablementConfigError rather than writing, and the error is
reported once per stretch of failures with the path and the remedy. Valid
JSON of the wrong shape is unreadable too - it fails open by the same route.

readConfig() keeps its signature for existing callers; every production path
inside the class now goes through readConfigResult().

This is the same defect google-gemini#29445 fixes for mcp-server-enablement.json, in the
sibling file, and the two are worth reading together.
@lets-order-some-fries
lets-order-some-fries requested a review from a team as a code owner September 24, 2026 14:13
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical defect where an unreadable or corrupted extension-enablement.json file would cause the system to silently revert to a default state, effectively re-enabling all previously disabled extensions. By implementing a more robust configuration reading strategy and introducing a 'fail-closed' mechanism, the changes ensure that user preferences are respected even when the configuration file is damaged. Additionally, the PR adds safeguards to prevent further data loss by blocking write operations when the existing configuration cannot be safely parsed.

Highlights

  • Robust Config Reading: Introduced readConfigResult to distinguish between missing files and unreadable/corrupt configurations, preventing the system from defaulting to an 'all-enabled' state when the config is corrupted.
  • Fail-Closed Behavior: Updated isEnabled to fail closed (treating extensions as disabled) when the configuration file is unreadable, preventing accidental re-enabling of extensions.
  • Write Protection: Added ExtensionEnablementConfigError to prevent enable and remove operations from overwriting a corrupted config file, preserving user data for manual repair.
  • Improved Error Reporting: Implemented throttled error reporting to ensure that configuration issues are communicated clearly without flooding logs during startup.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@github-actions github-actions Bot added the size/m A medium sized PR label Sep 24, 2026
@github-actions

Copy link
Copy Markdown

📊 PR Size: size/M

  • Lines changed: 203
  • Additions: +184
  • Deletions: -19
  • Files changed: 2

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request improves the robustness of the extension enablement configuration handling in the Gemini CLI. It introduces a safer mechanism for reading the enablement configuration file, distinguishing between a missing file and an unreadable/corrupted file. When the file is unreadable, the manager now fails closed (treating extensions as disabled) and prevents any write operations (such as enabling or removing extensions) to avoid overwriting and losing existing configuration data. It also limits error reporting to once per stretch of failures to avoid spamming logs. Comprehensive unit tests have been added to verify these new behaviors. I have no feedback to provide as there are no review comments and the implementation is solid.

@gemini-cli gemini-cli Bot added priority/p1 Important and should be addressed in the near term. area/core Issues related to User Interface, OS Support, Core Functionality labels Sep 24, 2026
@gemini-cli

gemini-cli Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Hi there! Thank you for your interest in contributing to Gemini CLI.

To ensure we maintain high code quality and focus on our prioritized roadmap, we only guarantee review and consideration of pull requests for issues that are explicitly labeled as 'help wanted'.

This PR will be closed in 7 days if it remains without that designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core Issues related to User Interface, OS Support, Core Functionality priority/p1 Important and should be addressed in the near term. size/m A medium sized PR status/pr-nudge-sent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant