fix(cli): stop forwarding a literal 'default' permission mode to Claude - #1733
fix(cli): stop forwarding a literal 'default' permission mode to Claude#1733f-liva wants to merge 2 commits into
Conversation
`mapToClaudeMode` passed the literal 'default' through unchanged, so the Claude child was spawned with an explicit `--permission-mode default`. A CLI flag outranks the settings file, so a user whose `permissions.defaultMode` is `auto` silently lost it and was prompted on nearly every tool call — a worse outcome than sending no mode at all. Claude's `default` is the ambient "no override" value, the same thing the app expresses by omitting the field, so it now maps to undefined and the SDK applies the user's own configuration. Codex's `safe-yolo` and `read-only` still map to a literal 'default': for them asking is the policy that was picked, not the absence of one. Normalizing at the SDK boundary rather than in the app also makes the CLI robust against client builds that predate 4cdb8b1 and still send the literal spelling on every spawn and resume. `handleModeChange` now guards on the mapped value, so switching to Default no longer tries to push undefined into a live query — matching the behavior its own comment already described. Refs slopus#1695 Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
The daemon looks up a session for `resume-happy-session` in two places: `pidToTrackedSession` for live sessions, and `sessionIdToFinishedSession` for ones that have already exited. Only `onChildExited` ever populates the second map, and it fires exclusively for processes the daemon spawned itself. Sessions started from a terminal register through the local webhook, so they are tracked but are not daemon children. When such a process dies, the only thing that notices is the heartbeat's stale-PID sweep, which dropped the entry from `pidToTrackedSession` without preserving it. From that moment `resume-happy-session` failed with "Session <id> is not tracked by this daemon", and stayed broken until a daemon restart repopulated `sessionIdToFinishedSession` from `~/.happy/sessions.json`. Route the stale-PID sweep through `onChildExited` so both exit paths preserve the session the same way. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
|
Pushed a second, related daemon fix onto this branch ( Terminal-started sessions become unresumable once they exit.
A session started from a terminal registers via It stayed broken until the next daemon restart, which repopulates The fix routes the stale-PID sweep through |
Fixes the half of #1695 that lives in the CLI.
The problem
mapToClaudeModereturns the literal'default'unchanged, soclaudeRemote.tsputs it intosdkOptions.permissionModeand the child is spawned with an explicit flag:A CLI flag outranks the settings file, so
permissions.defaultModein~/.claude/settings.jsonis overridden. On my machine that is"auto"; the session lands indefaultinstead and asks about nearly every tool call.permissions.allowstill applies — I verified that a rule-matchinglsruns unprompted — so the prompting is entirely the lostdefaultMode.That makes forwarding the literal strictly worse than sending nothing: an omitted field would have let the user's own configuration stand.
Measured on one session, all Bash calls, none matching an allow rule:
This is the same root cause @chaehyun2 traced in the issue thread; I hit it independently on a second machine and their analysis matches what I see here.
Why fix it in the CLI
4cdb8b1 already taught the app to omit the field for Default, and
mapToClaudeMode's own doc comment states the contract: "Undefined is a meaningful value, not a missing one: it is how 'Default' reaches the SDK, which then applies Claude's own configuration."But the CLI still honors the literal if it receives one, and it does receive one — any app build predating that commit sends it on every spawn and resume. Normalizing at the SDK boundary makes the contract hold regardless of which client is on the other end, which is the point of having a boundary.
The change
mapToClaudeModemaps Claude's'default'toundefined, alongside the existingundefinedcase.safe-yoloandread-onlystill map to a literal'default'. For them "ask first" is the policy the user picked, not the absence of one, so they must not fall back to whateverdefaultModethe user configured for Claude — that could be more permissive than what they chose.PermissionModeno longer guarantees a defined result.PermissionHandler.handleModeChangeguards on the mapped value rather than the raw one, so switching to Default no longer pushesundefinedinto a live query. Its comment already described this behavior ("Only a concrete mode is pushed"); the guard now matches it.Tests
src/claude/utils/permissionMode.test.ts— the'default'pass-through expectation is replaced by its inverse, plus a case pinning the Codex ask-first modes so a later simplification cannot fold them into the same branch.Full suite: 814 passed, 12 failing — all pre-existing and unrelated (difftastic/ripgrep binaries absent in my sandbox, and the Codex/daemon integration suites needing the docker environment). None touch the permission path.
Not included
Two adjacent things I saw while tracing, left alone to keep this reviewable:
runClaude.tsadds--dangerously-skip-permissionsonly when the initial mode is bypass, so a mid-session switch to YOLO logsCannot set permission mode to bypassPermissions because the session was not launched with --dangerously-skip-permissions. Happy's own handler auto-approves anyway viaisClaudeBypassEquivalent, so the toggle works in practice and only the SDK-level sync is a no-op.claudeRemoteLauncher.tsthe replayedpendingbatch returns without restoringmodeHash/mode, which the restart detector below depends on.@chaehyun2 mentioned having both on a fork — happy to defer to their PR for those.
Generated with Claude Code
via Happy