Skip to content

fix(settings): honor an explicitly saved null in get-effective-settings - #88

Open
JeanBaptisteRenard wants to merge 1 commit into
doctly:mainfrom
devsuitup:fix/effective-settings-explicit-null
Open

fix(settings): honor an explicitly saved null in get-effective-settings#88
JeanBaptisteRenard wants to merge 1 commit into
doctly:mainfrom
devsuitup:fix/effective-settings-explicit-null

Conversation

@JeanBaptisteRenard

Copy link
Copy Markdown
Contributor

What breaks

get-effective-settings merges saved settings over SETTING_DEFAULTS, skipping a scope's value when it is null:

if (global[key] !== undefined && global[key] !== null) { ... }
if (project[key] !== undefined && project[key] !== null) { ... }

But null is not "unset" for permissionMode. settings-panel.js persists the "Default (none)" option as value || null:

permissionMode: () => settingsViewerBody.querySelector('#sv-perm-mode').value || null,

So a user who deliberately picks "Default" — meaning prompt for every action, pass no --permission-mode flag — is stored identically to a user who never opened settings. The DB already distinguishes the two; only the read side collapses them.

Two consequences:

1. A project cannot narrow permissionMode back to "Default". Set a global mode, then choose "Default" for one project, and the project still launches with the global mode. The narrower, more restrictive choice silently loses — which is the wrong direction for a permissions setting.

2. Any non-null SETTING_DEFAULTS value is unreachable at project scope, since a project-level null can never override it. That constrains what defaults can be changed to later.

Reproduction

  1. Settings → global → Permission Mode → Bypass (any non-default mode works).
  2. Open a project's settings, untick "use global" for Permission Mode, choose Default (none).
  3. Start a session in that project.

Expected: no --permission-mode flag, Claude prompts for each action.
Actual: the session launches with --permission-mode bypassPermissions. The project's saved null is discarded and the global mode is inherited.

The fix

Only undefined — the key was never saved at that scope — should fall through to the next-broader value. Dropping the two !== null clauses is the entire behavioral change. Other falsy values (false, 0, '') were already handled correctly and are unaffected; there is a test pinning that.

I extracted the merge loop into resolve-effective-settings.js so the rule can be unit-tested without booting Electron — the same shape as folder-index-state.js and the decodeOsc52Payload export, which are already covered that way. If you'd rather not carry the extra module, say so and I'll inline it back into main.js; the fix itself is two lines.

Tests

10 tests in test/resolve-effective-settings.test.js, covering scope precedence, the explicit-null-wins cases, null vs undefined not being conflated, other falsy values surviving, and no input mutation.

Verified adversarially: restoring the !== null clauses turns exactly three of them red ("a project can narrow permissionMode back to Default over a global mode", "an explicit global null beats a non-null default", "null and undefined are not conflated").

npm test on this branch: 33/33 pass (23 before, +10).

Found while syncing a fork against v0.0.31. My fork additionally changes SETTING_DEFAULTS.permissionMode to 'auto', which depends on this fix — that part is a local preference and is deliberately not included here.

get-effective-settings skipped a saved value when it was `null`, treating it
as "never configured". But the settings panel persists permissionMode's
"Default (none)" choice as `value || null`, so `null` is a deliberate value
there meaning "pass no --permission-mode flag" — not an absent key.

Two consequences:

- A project that narrowed permissionMode back to "Default" kept inheriting
  the global mode. Set a global mode, then pick "Default" for one project,
  and the project still launches with the global mode. The narrower, more
  restrictive choice silently loses.

- Any SETTING_DEFAULTS entry whose value is not null is unreachable at
  project scope, because a project-level null can never override it.

Only `undefined` should fall through to the next-broader scope. Other falsy
values (`false`, `0`, `''`) were already handled correctly and stay that way.

Extracts the merge loop into resolve-effective-settings.js so the rule can be
unit-tested without booting Electron, matching how folder-index-state.js and
decodeOsc52Payload are already covered. Happy to inline it back into main.js
if you'd rather not have the extra module — the behavioral change is the two
dropped `!== null` clauses.

10 tests in test/resolve-effective-settings.test.js. Three of them fail if the
`!== null` clauses are put back.
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.

1 participant