[BUG] Daemon-hosted background worker deletes `~/.claude/settings.json` when it is a symlink into a read-only directory (nix/home-manager) — all user settings silently lost

Status Open
Reported on v2.1.219
Maintainer reply None cached
Activity 1 comment · opened Aug 20, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [ ] I am using the latest version of Claude Code — I am on 2.1.219, but I reviewed every changelog entry from 2.1.219 through 2.1.237 (380 entries across 18 releases; 2.1.230 was never published) and none of them addresses this, #78162 or #86935

What's Wrong?

A long-lived daemon-hosted background worker (claude bg-spare) calls unlinkat() on ~/.claude/settings.json and deletes the user's settings file. This is not a failed write leaving a stale file — the path is removed outright, and every session then silently falls back to built-in defaults: permission rules, hooks, sandbox configuration, model, env and status line all vanish at once with no error anywhere in the UI.

I caught this with a kernel audit rule after it happened twice without explanation. The attribution is unambiguous — exe is the Claude Code binary, the syscall is unlinkat, success=yes, and the audit PATH record shows mode=0120777 (S_IFLNK) with nametype=DELETE, proving it removed the symlink itself rather than writing through it to the target.

The trigger is the file being a symlink whose target lives in a read-only directory. On Nix with home-manager that is the default configuration — home-manager switch materialises ~/.claude/settings.json as a symlink into /nix/store, which is read-only — so this is not an exotic setup for that user population. Each home-manager switch recreates the symlink, and a background worker then deletes it again within seconds.

Over one evening on a single machine the same worker (pid 83263, alive and idle for two days) deleted the file four times in a row, each time within 7–69 seconds of it being recreated. Replacing the symlink with an ordinary file copy at the same path stopped it immediately — that file has been untouched since. Four symlinks deleted, one regular file survived: the symlink is the trigger.

I want to be precise about what I have and have not proven. Proven: Claude Code unlinked the symlink, four times, same pid, with kernel-level attribution. Not proven: that a failed write precedes the unlink. My audit rules watch ~/.claude/settings.json, but per #78162 the writer resolves the symlink one hop and aims its temp file at <target-dir>/settings.json.tmp.<pid> — a different path my rules don't cover — so the absence of a write record in my logs is a limitation of the rule scope, not evidence that no write was attempted.

What Should Happen?

Claude Code should never unlink the user's settings file. If it cannot persist settings, it should fail loudly and leave the existing configuration intact — losing the entire file is far worse than failing to save one key. Concretely: if the settings path is a symlink, either write through it to the resolved target or refuse and surface an error, but do not remove it. If an in-place fallback needs a regular file at that path, it must not delete what is already there until it has a successfully written replacement to put in its place.

Fully resolving the symlink chain (realpath) before choosing the temp-file directory would also fix the underlying write failure — that is the fix already proposed in #78162 — and would stop this destructive path being reached at all.

Error Messages/Logs

# Kernel audit, key=claude-settings. `ln` = home-manager recreating the link; `.claude-wrapped` = Claude Code deleting it.
# syscall 264=renameat 266=symlinkat 263=unlinkat 257=openat. Note the identical pid 83263 on all four deletions.

août 20 18:02:36  syscall=264  ppid=766221 pid=766265  tty=pts10   comm=ln                <- home-manager switch (gen 165)
août 20 18:03:19  syscall=263  ppid=83147  pid=83263   tty=(none)  comm=.claude-wrapped   <- DELETED after 43s
août 20 18:31:52  syscall=266  ppid=833534 pid=833581  tty=pts8    comm=ln                <- recreated
août 20 18:32:12  syscall=263  ppid=83147  pid=83263   tty=(none)  comm=.claude-wrapped   <- DELETED after 20s
août 20 18:35:11  syscall=266  ppid=847551 pid=850218  tty=pts8    comm=ln                <- recreated
août 20 18:35:18  syscall=263  ppid=83147  pid=83263   tty=(none)  comm=.claude-wrapped   <- DELETED after 7s
août 20 18:39:09  syscall=266  ppid=847551 pid=855488  tty=pts8    comm=ln                <- recreated
août 20 18:40:18  syscall=263  ppid=83147  pid=83263   tty=(none)  comm=.claude-wrapped   <- DELETED after 69s
août 20 18:41:46  syscall=257  ppid=847551 pid=863899  tty=pts8    comm=cp                <- replaced with a REGULAR FILE; never touched again

# Full record for one deletion:
type=SYSCALL msg=audit(...): arch=c000003e syscall=263 success=yes exit=0 items=2 ppid=83147 pid=83263 auid=1000 uid=1000 gid=100 euid=1000 tty=(none) ses=14 comm=".claude-wrapped" exe="/nix/store/5iw9nsds5803azy3fk1fqqd6ab62wyda-claude-code-2.1.219/bin/.claude-wrapped" key="claude-settings"
type=CWD  cwd="/home/ten/stockly/Main_PD859"
type=PATH item=0 name="/home/ten/.claude/" inode=30064771234 mode=040755 nametype=PARENT
type=PATH item=1 name="/home/ten/.claude/settings.json" inode=30068580160 mode=0120777 nametype=DELETE
type=PROCTITLE proctitle="claude bg-spare --bg-spare /tmp/cc-daemon-1000/e4c0235d/spare/90ce0c4b.claim.sock"

# The offending process, still alive and idle for two days at the time of the deletions:
$ tr '\0' ' ' < /proc/83263/cmdline
claude bg-spare --bg-spare /tmp/cc-daemon-1000/e4c0235d/spare/90ce0c4b.claim.sock
$ tr '\0' ' ' < /proc/83147/cmdline       # its parent
claude bg-pty-host --bg-pty-host /tmp/cc-daemon-1000/e4c0235d/spare/90ce0c4b.pty.sock 200 50 -- .../.claude-wrapped --bg-spare .../90ce0c4b.claim.sock

# The write failure on this configuration, for reference (this is #78162):
Failed to read raw settings from /home/ten/.claude/settings.json: Error: EROFS: read-only file system, open '/nix/store/<hash>-home-manager-files/.claude/settings.json.tmp.15.049e63847789'

Steps to Reproduce

  1. Arrange for ~/.claude/settings.json to be a symlink whose target sits in a read-only directory. On NixOS/home-manager this is automatic; otherwise: mkdir /tmp/ro && cp ~/.claude/settings.json /tmp/ro/ && chmod a-w /tmp/ro && ln -sfn /tmp/ro/settings.json ~/.claude/settings.json
  2. Arm an audit rule so the deletion is attributable rather than mysterious: sudo auditctl -w "$HOME/.claude/settings.json" -p wa -k claude-settings
  3. Start Claude Code and let the background daemon spawn its worker pool (claude agents, or any session that creates background agents). Leave it running for a while — see the note below on staleness.
  4. Recreate or repoint the symlink the way a dotfile manager would: ln -sfn /tmp/ro/settings.json ~/.claude/settings.json
  5. Wait up to ~2 minutes, then ls -la ~/.claude/settings.json — the path is gone.
  6. Confirm attribution: sudo ausearch -k claude-settings -i | tail -40 shows .claude-wrapped with syscall=unlinkat and nametype=DELETE on a mode=0120777 target.
  7. Control: put an ordinary file at the same path (cp /tmp/ro/settings.json ~/.claude/settings.json) and it is never deleted.

What did not reproduce it, which I think narrows things usefully for whoever picks this up:

  • A freshly spawned --bg-spare worker in an isolated HOME, with no credentials and an unclaimed socket, left the symlink alone for 120s — both when the link was left untouched and when it was repointed mid-run. So repointing the symlink alone is not sufficient; the worker appears to need to be a real daemon-attached one.
  • One-shot CLI settings writes do not trigger it. claude plugin marketplace add, claude plugin install, claude plugin marketplace update, claude plugin list, claude doctor, claude mcp add --scope user and a plain claude -p session all hit the EROFS failure and left the symlink intact.
  • The worker that did the damage had been alive and idle for two days. Every deletion came from that one long-lived pid, never from a freshly spawned one, so worker age or accumulated state looks relevant.

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

Unknown — see Additional Information. I cannot honestly pin a version boundary: both the build that deletes and an earlier build that did not are labelled 2.1.219.

Claude Code Version

2.1.219 (Claude Code)

Platform

Anthropic API

Operating System

Other Linux

Terminal/Shell

Other — zsh. Note the deleting process is not attached to any terminal (tty=(none)); it is a daemon-hosted background worker.

Additional Information

On the regression question. In July I tested this same class of operation on an earlier build and the symlink survived the EROFS failure every time. The build that deletes is a different derivation but reports the same version string, and the earlier one has since been garbage-collected, so I cannot diff them or bisect. I've marked this "I don't know" rather than guess. For what it's worth, #86935 (the settings-file watcher) is labelled regression and describes behaviour on this same configuration, which may share a cause.

Related issues. This looks like the destructive end of a path already reported from two other angles: #78162 (open, reproduced) is the same writer — it documents that the atomic save resolves the symlink one hop and puts its temp file next to the first target, failing EROFS/EACCES when that lands in a read-only directory; its stated impact stops at "the operation aborts partway", with no mention of the file being removed. #86935 (open, reproduced, regression) establishes that the settings file is watched via its parent directory, which on Nix is the store — that is plausibly what wakes a long-lived worker when home-manager switch swaps the file underneath it. Older, closed, same family: #67208, #55485, #52525.

Circumstantial evidence for the mechanism, offered as a lead rather than a claim — I could not decompile the Bun-compiled binary, so this is from recoverable string literals only, with no control flow. The write helper contains: Refusing to write through symlink: … Resolve the symlink and pass the real target path explicitly, … written via in-place fallback, (O_NOFOLLOW), and Write to … failed (…) after the target was truncated. The new content was preserved at …. An O_NOFOLLOW open cannot open a symlink at all (ELOOP), so an in-place fallback that must place a regular file at that path would have to unlink the symlink first. That would explain both the unlinkat and why nothing is written afterwards, but I want to be clear I have not proven the ordering.

Impact severity. This is silent user-configuration data loss, and it repeats: every time the dotfile manager restores the file, it is deleted again within seconds. Until I traced it, the only symptom was agents behaving strangely — no error, no log, nothing pointing at the settings file being gone. Users whose settings are managed declaratively will hit this on every home-manager switch.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗