Long-lived daemon caches environment (SHELL) with no drift detection or restart; fish-login users silently get the wrong Bash-tool shell; CLAUDE_CODE_SHELL is undocumented
Summary
On Linux, a long-lived transient daemon (claude daemon run --origin transient) captures its process environment at launch and never re-checks it. If the user's environment changes afterward (e.g. login shell migrated to fish, SHELL updated via systemctl --user set-environment / ~/.config/environment.d/, or a settings.json edit), the already-running daemon keeps serving the stale value to every session it spawns, with no drift detection, no warning, and no restart command. The most visible symptom: the Bash tool runs commands under the wrong shell for hours. A related gap is that the one reliable fix (CLAUDE_CODE_SHELL) is undocumented, and settings.json env does not behave the way users reasonably expect.
This was diagnosed via static reading of the v2.1.177 bundled JS, so identifiers are minified and behavior is version-specific — please treat code-path claims as best-effort, but the observable behavior is reproducible.
Environment
- Claude Code v2.1.177, Linux (ARM64)
- Login shell: fish (a POSIX shell — bash — is also installed)
The core defect: daemon freezes its environment
- The transient daemon inherits a snapshot of
process.envat launch and stays alive for a long time (observed ~10h). - The Bash-tool shell is resolved from
process.env(CLAUDE_CODE_SHELL, thenSHELL) and the result is memoized for the process lifetime (the memo is only cleared on plugin refresh, not on a settings change). - The per-session env is built from the daemon's frozen
process.envsnapshot, so every spawned session inherits the stale value. - Changing the login shell, the systemd
--userenvironment, orenvironment.ddoes not affect the running daemon — and there is noclaude daemon restart, onlyclaude daemon stop [--any].
Net effect: a user can correctly fix their shell everywhere and still have every Claude session silently run under the old shell until they manually discover they must kill the daemon. A non-developer has essentially no way to diagnose this.
Shell selection specifics (fish users)
- The resolver only accepts a path containing
bashorzsh; fish is never selectable. - It only throws the clear
No suitable shell found...error when neither bash nor zsh exists. If the user has fish as login shell and zsh happens to be installed, the tool silently selects zsh with no warning — the user never learns their intended shell isn't being used. - The purpose-built override
CLAUDE_CODE_SHELL(checked first, validated to bash/zsh) is undocumented. The documenteddefaultShell(bash|powershell) governs interactive!commands, not the Bash tool's POSIX shell — so users can't find the right knob without reverse-engineering the binary.
settings.json env does not work the way users expect
A natural attempt to fix this is { "env": { "SHELL": "/usr/bin/bash" } } in settings.json. In practice:
envis applied viaObject.assign(process.env, ...)once per session init (memoized), not "per command."- It therefore cannot un-freeze an already-running daemon — it only affects newly started/reclaimed sessions, i.e. it still requires a daemon restart.
- There is also no Linux existence check on the resolved shell path, so a pinned path that doesn't exist on a given machine makes every Bash command fail (worse than the fallback).
This combination makes the problem very easy to misdiagnose and burns significant time.
Steps to reproduce
- Start
claude(spawns the transient daemon) whileSHELLpoints at shell A. - Change your login shell /
SHELLto shell B (or editsettings.jsonenv.SHELL). - Open new sessions under the still-running daemon → the Bash tool still uses shell A. No warning is shown.
Expected behavior / requests
- Detect environment drift in a long-lived daemon (at minimum
SHELL) and surface a clear, actionable warning, or re-resolve per session. - Add a documented
claude daemon restart(today users muststop --anythen relaunch). - Document
CLAUDE_CODE_SHELLas the supported way to pin the Bash-tool shell, and/or extenddefaultShellto accept an explicit POSIX shell path. - For fish (and other non-POSIX) login shells, emit a one-line, copy-pasteable diagnostic at session start when the Bash-tool shell differs from the login shell or the login shell is unsupported — instead of silently falling back to whatever bash/zsh happens to exist.
Related secondary concern (privacy / transparency)
/feedback uploads the conversation transcript and redacts only known API-key/token patterns. Custom third-party key formats are not recognized and would be transmitted and retained. Consider broadening redaction (or warning the user pre-upload when an unrecognized but secret-shaped value is present in the transcript).
Thanks — happy to provide more detail on any of the above.