settings.json env block: removed key reappears on session exit (in-memory snapshot flush?)

Resolved 💬 2 comments Opened Apr 22, 2026 by kiki830621 Closed May 27, 2026

Problem

Removing a key from ~/.claude/settings.json's env block while a Claude Code session is active: the key silently reappears after that session exits or a new session starts. Git working copy gets flagged every time and users can't tell WHO wrote it back.

Version: Claude Code 2.1.117 (Bun-built Mach-O binary on macOS 15.x)

Reproducer

  1. Edit ~/.claude/settings.json:

``jsonc
{
"env": {
"SOME_VAR": "original_value"
}
}
``

  1. Start claude. The process inherits SOME_VAR=original_value in env.
  2. In a second terminal, edit settings.json to remove "SOME_VAR". Save. cat confirms removed.
  3. Back in Claude: run a few interactions (optional — may not even be needed), then /exit.
  4. cat ~/.claude/settings.json again → SOME_VAR: original_value is back.

On my side reproduced with CLAUDE_CODE_EFFORT_LEVEL=max specifically (related to /effort UX from claude-hud plugin, but the write-back happens for any env key).

Expected

Once a key is removed from the env block on disk, it stays removed. Claude Code should either:

  • Never write env block back (it's user config, not session state), or
  • Re-read from disk before flushing (rather than flushing a stale in-memory snapshot), or
  • Diff & merge so that explicit user deletions are respected.

Actual

Timeline from my investigation (all 2026-04-22):

| Time | Event |
|------|-------|
| 12:34 | Edit settings.json, remove CLAUDE_CODE_EFFORT_LEVEL: "max"; git commit clean state |
| 12:48 | Active Claude session c43c4405 exits |
| 12:56 | New Claude session 46315 starts |
| 12:58 | settings.json mtime updates; diff shows only CLAUDE_CODE_EFFORT_LEVEL: "max" re-added |
| 13:04 | Remove again (session 46315 still alive) |
| 13:04–13:05 | Wait 60 s with fswatch armed — no rewrite (runtime/periodic sync ruled out) |
| 13:08 | /exit that session → restart |
| Post-restart | This time file stays clean; /effort xhigh works with no "overrides this session" warning |

Evidence

  1. Binary contains write-back strings (strings ~/.local/bin/claude | grep CLAUDE_CODE_EFFORT_LEVEL):

> ``
> CLAUDE_CODE_EFFORT_LEVEL=
> Not applied: CLAUDE_CODE_EFFORT_LEVEL=
> Cleared effort from settings, but CLAUDE_CODE_EFFORT_LEVEL=
>
``
Suggests internal awareness of the env/settings relationship, including a "cleared from settings but env still set" warning path — but no symmetric "env was removed from settings, honour it" path seems to fire.

  1. No filesystem watcher / plugin writes it (grep across ~/.claude/plugins, hooks, shell rc files, launchctl, LaunchAgents, .MacOSX/environment.plist, /etc/zshenv, VS Code terminal.integrated.env.osx, shell-snapshots 84 files) — zero matches for CLAUDE_CODE_EFFORT_LEVEL.
  1. Child process env ≠ parent shell env: ps eww -p <parent-zsh> has no CLAUDE_CODE_EFFORT_LEVEL, but env inside Claude does. Injection happens inside claude binary based on settings.json.env.
  1. Parallel-session pollution: when multiple Claude sessions run concurrently and one was started before the user's edit, its in-memory env snapshot still carries the old value. Its exit flushes the stale snapshot, re-adding the removed key.

Possible Explanations

(A) Shutdown flushes stale in-memory settings.json snapshot

Claude Code loads settings.json into memory at startup, merges internal transient fields (or not), and on /exit writes the full object back without re-reading disk. Any edit made to disk during the session is silently clobbered on shutdown. Most fits the observed timeline.

(B) --effort CLI flag / internal state persistence

If Claude Code persists the effective effort level back into settings.json.env.CLAUDE_CODE_EFFORT_LEVEL (e.g. for cross-session continuity), the persistence is asymmetric: persists value-set, doesn't persist user-deletion.

(C) Plugin-side /effort or update-config skill rewrites on session events

Some bundled skill reacts to Stop / SessionStart events and echoes in-memory state into settings. I grep'd my installed plugins; none match write logic for this specific key. Less likely given my environment but worth noting for users with many plugins.

(D) Symlink resolution / CloudStorage sync race

My ~/.claude/settings.json is a symlink into a Dropbox-synced git repo. Shouldn't matter (file hash stays consistent and stat -L resolves correctly), but mentioning in case Dropbox's CloudStorage virtual FS causes atomic-write replay that Claude's writer assumes will succeed unchanged.

Impact

  • git status in users' config repos flags settings.json after every Claude session — noise that masks real config drift.
  • Users can't delete an env override: remove → re-appears on exit → user thinks "my delete didn't work" → loses trust in own config.
  • Cross-session parallel work amplifies the problem: removing a key while 5 sessions are active means 5 restart-flushes to fully purge.

Suggested Fix Direction

  1. On flush, re-read settings.json from disk and merge with internal deltas, so user deletions are preserved.
  2. Never flush env block from in-memory cache — treat env as read-only user config.
  3. Provide a claude config purge <key> or claude --no-persist-env CLI to opt out.
  4. Print a warning if env block is rewritten by Claude itself (so users can trace it next time).

Workaround

Restart all concurrent Claude sessions after editing settings.json, then immediately git commit to pin the clean state; only then resume work.

Related reading

  • claude-hud plugin's /effort is a pure reader (execFileSync ps ... --effort), not the writer — just the user-facing command that surfaces the discrepancy.

---
Filed from a live debug session on 2026-04-22 after ~4 cycles of the symptom.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗