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

Open 💬 0 comments Opened Jun 14, 2026 by kairin

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

  1. The transient daemon inherits a snapshot of process.env at launch and stays alive for a long time (observed ~10h).
  2. The Bash-tool shell is resolved from process.env (CLAUDE_CODE_SHELL, then SHELL) and the result is memoized for the process lifetime (the memo is only cleared on plugin refresh, not on a settings change).
  3. The per-session env is built from the daemon's frozen process.env snapshot, so every spawned session inherits the stale value.
  4. Changing the login shell, the systemd --user environment, or environment.d does not affect the running daemon — and there is no claude daemon restart, only claude 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 bash or zsh; 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 documented defaultShell (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:

  • env is applied via Object.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

  1. Start claude (spawns the transient daemon) while SHELL points at shell A.
  2. Change your login shell / SHELL to shell B (or edit settings.json env.SHELL).
  3. Open new sessions under the still-running daemon → the Bash tool still uses shell A. No warning is shown.

Expected behavior / requests

  1. Detect environment drift in a long-lived daemon (at minimum SHELL) and surface a clear, actionable warning, or re-resolve per session.
  2. Add a documented claude daemon restart (today users must stop --any then relaunch).
  3. Document CLAUDE_CODE_SHELL as the supported way to pin the Bash-tool shell, and/or extend defaultShell to accept an explicit POSIX shell path.
  4. 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.

View original on GitHub ↗