[BUG] Claude Desktop: permissions.defaultMode=bypassPermissions in settings.json stopped being honored (regression — worked previously)

Status Open
Maintainer reply None cached
Activity 4 comments · opened Jul 7, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Environment:

  • Windows 11 Pro
  • Running inside Claude Desktop (env var CLAUDE_CODE_ENTRYPOINT=claude-desktop)
  • CLAUDE_AGENT_SDK_VERSION=0.3.202

Issue:
For many past sessions, setting "defaultMode": "bypassPermissions" in
.claude/settings.json worked correctly and permission prompts did not appear.
As of this week, permission prompts appear on ordinary tool calls (e.g. a
simple Bash command) despite this setting being correctly present.

Confirmed facts:

  • "permissions": { "defaultMode": "bypassPermissions" } is set in BOTH

~/.claude/settings.json (user/global) and the project's
.claude/settings.local.json

  • Both files are valid JSON (parsed successfully, no syntax errors)
  • No managed-settings.json exists anywhere on the machine (checked all

standard Windows locations: C:\ProgramData\ClaudeCode\,
%APPDATA%\Claude\, %APPDATA%\Claude Code\, %LOCALAPPDATA%\ClaudeCode\,
%LOCALAPPDATA%\Claude\ — none present, so this is not an enterprise
policy override)

  • The app UI shows a "Manual permissions" label, indicating the session

is actually running in the default/manual (ask-every-time) mode, not
bypassPermissions

  • Problem persists across brand-new sessions started after confirming

the settings files — this is not a stale-session/config-not-reloaded
issue

  • No UI control was found (via Shift+Tab or elsewhere) to manually

switch the session's permission mode from inside Claude Desktop

Ask:
Please confirm whether Claude Desktop is expected to read
permissions.defaultMode from settings.json the same way the terminal
CLI does, and if so, why it is not taking effect. This previously
worked reliably across many sessions and has regressed.

What Should Happen?

Claude Desktop should honor "defaultMode": "bypassPermissions" set in
settings.json and skip permission prompts, the same way it did across
many previous sessions before this regressed.

Error Messages/Logs

Steps to Reproduce

  1. Set "permissions": { "defaultMode": "bypassPermissions" } in both

~/.claude/settings.json (global) and the project's
.claude/settings.local.json

  1. Confirm both files are valid JSON (no syntax errors)
  2. Start a brand-new session in Claude Desktop on Windows
  3. Ask Claude to run any ordinary Bash command (e.g. command -v ffmpeg)
  4. A permission prompt ("Allow Claude to run X?" with Deny / Always

allow / Allow once) appears anyway — even though defaultMode is set
to bypassPermissions and no managed-settings.json exists anywhere
on the machine (checked all standard Windows locations)

This exact settings-file configuration worked correctly (no prompts)
across many prior sessions. The behavior has regressed recently.

Claude Model

Sonnet (default)

Is this a regression?

Yes, this worked in a previous version

Last Working Version

_No response_

Claude Code Version

Sonnet 5

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Terminal.app (macOS)

Additional Information

_No response_

View original on GitHub ↗

3 Comments

rpelevin · 3 months ago

This looks like a mode-resolution bug, but I would avoid treating it as only a settings reload question.

The useful diagnostic boundary is: configured permission mode, managed policy override, entrypoint, UI label, and effective tool execution behavior should all resolve through one auditable chain.

A regression test could cover:

  • user/global settings present and valid
  • project-local settings present and valid
  • managed policy absent
  • desktop entrypoint active
  • effective mode reported to the UI
  • first ordinary tool call either prompts or does not prompt according to that resolved mode

If the desktop surface intentionally differs from the terminal CLI, the product should expose that as an explicit override source rather than silently falling back to manual permissions.

The important invariant is that the user should be able to distinguish "the setting was not read", "the setting was overridden", "the setting was read but not supported in this entrypoint", and "the UI label/runtime behavior is stale."

That gives maintainers a narrow acceptance boundary: permission mode is not just config text; it is a bound state transition from source config to effective runtime authority.

Boundary: diagnostic/regression-test feedback only; no claim about Anthropic fix, Claude Code adoption, implementation correctness, official alignment, partnership, customer interest, integration, or Neura usage.

brianbebel · 2 months ago

Adding a mechanism for this, traced through the shipped Desktop bundle. I think this
explains the whole cluster (#61436, #61958, #61860, #62076, #61415, #55095, #77555) —
none of which currently name a cause, which I suspect is why #77555 was closed as not
planned despite a clean config dump.

Environment: macOS 27.0, Claude Desktop 1.24012.9, bundled Claude Code 2.1.219,
individual Max plan, local session (not Cowork/remote/cloud).

Config is not the cause. On my machine every gate is already correct:

| surface | value |
|---|---|
| ~/.claude/settings.json permissions.defaultMode | bypassPermissions |
| desktop pref bypassPermissionsModeEnabled | true |
| desktop pref bypassPermissionsGateByAccount[<uuid>] | true |
| desktop pref bypassPermissionsOptInByAccount[<uuid>] | true |
| epitaxy-folder-permission-mode.<uuid>.<cwd> | bypassPermissions |
| live session argv | --permission-mode bypassPermissions --allow-dangerously-skip-permissions |
| GrowthBook tengu_disable_bypass_permissions_mode | false |
| managed-settings.json | absent (both macOS paths) |

Note this differs from #62084: the --dangerously-skip-permissions flag is being
passed here. The launch is correct and the mode is still refused.

Mechanism — a not-yet-loaded config is treated as a denial.

In Contents/Resources/ion-dist/assets/v1/index-C9LAU4f-*.js, the bypass-availability
helper builds its gating object as:

gating: o ? {
  source: "desktop-sdk",
  configLoading: null === i,          // i = config, null until the async load resolves
  deviceManagedDisablesBypass: "disable" === i?.disableBypassPermissionsMode?.value,
  accessStatus: t.status
} : { source: "legacy", legacyOrgEnabled: t.isAvailable }

and evaluates it with:

case "desktop-sdk":
  if (e.gating.configLoading) return;   // bare return -> undefined
  if (e.gating.deviceManagedDisablesBypass) return !1;
  if ("available" !== e.gating.accessStatus &&
      "blocked_by_org_admin" !== e.gating.accessStatus) return !1;
  break;

The landing composer in c360a9e1c-*.js then consumes it as:

b = (x ?? !1) && !v     // x = availability signal, v = rootDetected

So configLoading produces undefined, and ?? false converts that into a hard
"not available." The sibling errorCategory helper does the same thing:

e.desktopResolvesDisableModes
  ? e.configLoading ? null : (...)  // null while loading

null matches none of the renderer's known categories (root_detected, ant_account,
pref_disabled), so it falls through to the generic toast — "Bypass permissions isn't
available for this session. Asking for permissions instead."
— and clamps to default.

Reproduction (confirmed on my machine, 2026-07-29):

  1. Quit and relaunch Claude Desktop. Start a session immediately → banner appears,

mode clamps to Manual.

  1. Leave the app running several minutes so all config has demonstrably resolved.

Start another new session → banner appears again, identical behavior.

  1. Manually select Bypass Permissions in the running session → works, and the UI

correctly reports Bypass Permissions.

Step 2 is the important one: this is not a cold-start latency race. It reproduces
on a fully warmed app, on every new session. The gate state is held in
useState(null) with an async useEffect fetch, so each composer mount re-enters the
unresolved state regardless of how long the app has been running:

const [s, a] = useState(null);
useEffect(() => (Ar?.getPreferences?.().then(e => { a(e.bypassPermissionsGateByAccount) }), ...

Why this fits the reports:

  • Fires on every new session, warm or cold — the composer's first render always

precedes the async resolution, every time it mounts.

  • Manually re-selecting the mode seconds later works, because by then the fetch has

resolved and the helper returns a real boolean.

  • auto and bypassPermissions fail together (@ShivSync's observation in #77555) —

both run through this same gate, which is exactly what a shared unresolved-config
early-return would produce.

  • Config audits always come back clean, because config was never the gate. Note the

code explicitly tolerates accessStatus === "blocked_by_org_admin" without failing.
A genuine org block does not stop you here. Only the loading state does.

Suggested fix: distinguish "unknown" from "denied". Either have the composer wait on
the already-tracked configSettled / sessionMetaSettled flags before evaluating
availability, or make the unresolved case default to the user's configured
defaultMode rather than collapsing to false via ?? false.

Happy to supply the full deobfuscated trace or the surrounding chunk if useful.

jasalo · 2 months ago

Another macOS data point that I think isolates a second, distinct failure mode from the loading race @brianbebel traced above — and which points back at #62205.

On my machine the gate is not racing. It is deterministically false, and the mode it clamps to is named in a GrowthBook flag value.

Environment: macOS 14.6 (Darwin 23.6.0), Claude Desktop 1.24012.9, bundled Claude Code 2.1.220, individual Stripe subscription, organizationRole: admin (so no enterprise admin above me), no managed-settings.json on either macOS path.

The same account, same settings.json, same machine — CLI works, Desktop does not

I audited every session transcript in one project directory and grouped by entrypoint:

| session | entrypoint | effective permissionMode |
|---|---|---|
| 2edcfde0 | cli | ✅ bypassPermissions |
| 35cd02ad | cli | ✅ bypassPermissions |
| ae247a1c | cli | ✅ bypassPermissions |
| c52ddb3c | cli | ✅ bypassPermissions |
| 0a4bacf5 | claude-desktop | ❌ acceptEdits |
| 5d80aaec | claude-desktop | ❌ acceptEdits |
| 89e7d90e | claude-desktop | ❌ default |

~/.claude/settings.json contains "permissions": { "defaultMode": "bypassPermissions" } and "skipDangerousModePermissionPrompt": true throughout. 100% of CLI sessions honor it; 100% of desktop sessions do not. This is a clean entrypoint split, not a per-session or resume artifact.

The clamped mode is literally a flag value

From ~/.claude.json cachedGrowthBookFeatures:

tengu_quill_harbor                    = "acceptEdits"     <-- exactly what desktop clamps to
tengu_permission_friction             = true
tengu_disable_bypass_permissions_mode = false             <-- bypass NOT disabled
tengu_harbor_permissions              = true
tengu_slate_quill                     = true

tengu_quill_harbor holds the string "acceptEdits", and acceptEdits is precisely the mode 2 of 3 desktop sessions landed in — despite tengu_disable_bypass_permissions_mode being false, i.e. nothing is supposed to be disabling bypass. That correspondence is hard to read as coincidence, and it matches the mechanism proposed in #62205.

The per-account gate is false, not racing

From ~/Library/Application Support/Claude/claude_desktop_config.json:

"preferences": {
  "bypassPermissionsModeEnabled": true,
  "bypassPermissionsGateByAccount": { "47b62927-…": false }
}

Two things to note versus @brianbebel's dump:

  1. bypassPermissionsGateByAccount[<my uuid>] is false (theirs is true). Feeding false into the composer's b = (x ?? !1) && !v yields a hard deny with no async involvement — so on this machine the outcome is deterministic on every session, warm or cold, rather than a first-render race.
  2. bypassPermissionsOptInByAccount does not exist at all in my preferences — the key is entirely absent, not false.

So there appear to be at least two separate paths to the same user-visible symptom: an unresolved-config race (gate true), and a genuinely-false account gate that seems to be set by experiment assignment rather than by anything the user or an org admin did.

The part that actually cost me hours

bypassPermissionsModeEnabled reads true while bypassPermissionsGateByAccount[<uuid>] reads false. The UI toggle therefore reports the feature as enabled while the effective gate refuses it, and the session silently downgrades to acceptEdits with no banner, no log line, and no diff between "you asked for bypassPermissions" and "you got acceptEdits". I spent a long debugging session chasing settings.json syntax, managed policies, and session-resume semantics — all of which were fine — because nothing in the product surfaces the real reason.

Endorsing @brianbebel's suggested fix and adding two requests:

  • Never let an explicit permissions.defaultMode be silently downgraded. If the resolved mode differs from the configured mode, say so once, in the session, with the reason (config_loading / account_gate / experiment / org_policy / root_detected). Any of those four words would have ended my debugging in seconds.
  • Don't let an A/B experiment silently override an explicit user setting for a permission-model behavior. If tengu_quill_harbor / tengu_permission_friction can clamp a configured mode, an explicit defaultMode in settings.json should take precedence over the experiment arm — or the experiment should be disclosed in /status or /doctor.

Happy to provide the full (redacted) config dump or per-session transcript metadata if it helps. Related: #62205, #73587, #59112.

Showing cached comments. Read the full discussion on GitHub ↗