settings.json env block: removed key reappears on session exit (in-memory snapshot flush?)
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
- Edit
~/.claude/settings.json:
``jsonc``
{
"env": {
"SOME_VAR": "original_value"
}
}
- Start
claude. The process inheritsSOME_VAR=original_valuein env. - In a second terminal, edit
settings.jsonto remove"SOME_VAR". Save.catconfirms removed. - Back in Claude: run a few interactions (optional — may not even be needed), then
/exit. cat ~/.claude/settings.jsonagain →SOME_VAR: original_valueis 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
envblock 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
- 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.
- No filesystem watcher / plugin writes it (grep across
~/.claude/plugins, hooks, shell rc files, launchctl, LaunchAgents,.MacOSX/environment.plist,/etc/zshenv, VS Codeterminal.integrated.env.osx, shell-snapshots 84 files) — zero matches forCLAUDE_CODE_EFFORT_LEVEL.
- Child process env ≠ parent shell env:
ps eww -p <parent-zsh>has noCLAUDE_CODE_EFFORT_LEVEL, butenvinside Claude does. Injection happens insideclaudebinary based onsettings.json.env.
- Parallel-session pollution: when multiple Claude sessions run concurrently and one was started before the user's edit, its in-memory
envsnapshot 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 statusin users' config repos flagssettings.jsonafter 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
- On flush, re-read
settings.jsonfrom disk and merge with internal deltas, so user deletions are preserved. - Never flush
envblock from in-memory cache — treatenvas read-only user config. - Provide a
claude config purge <key>orclaude --no-persist-envCLI to opt out. - Print a warning if
envblock 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-hudplugin's/effortis 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.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗