bg-spare daemon doesn't invalidate spares whose cwd is deleted, causing orphan session dirs and `/clear` errors
bg-spare daemon doesn't invalidate spares whose cwd is deleted, causing orphan session dirs and /clear errors
Summary
The Claude Code bg-spare daemon (cc-daemon-*) pre-warms spare processes with concrete cwds. If a spare's cwd is later removed (e.g. when a git worktree is cleaned up), the spare keeps the dead inode and is still eligible for reuse. When such a spare is claimed for a new bg job, Claude Code derives the session-dir slug from the spare's stale cwd and writes JSONLs to ~/.claude/projects/-...<deleted-path-encoded>.../. The cwd path itself no longer exists on disk.
Downstream:
/clearerrors withPath "<deleted-path>" does not exist.- The resume picker cannot resolve the session's cwd.
~/.claude/projects/accumulates orphan dirs that no skill can prevent — even cleanup scripts re-create them as long as a stale-cwd spare exists.
Environment
- macOS Darwin 25.3.0 (Apple Silicon, M4 Pro)
- Claude Code 2.1.148 (
/Users/<user>/.local/share/claude/versions/2.1.148) - bg-spare pool socket:
/tmp/cc-daemon-501/<daemon-uuid>/spare/<spare-uuid>.{claim,pty}.sock
Reproduction
- From the host, start any worktree-aware skill workflow (in our case
/branchauto-EnterWorktree +/matt ulw). - Skill enters a git worktree at
<repo>/.claude/worktrees/<name>, opens a bg job inside. - cc-daemon pre-allocates
--bg-spareprocesses; some have cwd =<repo>/.claude/worktrees/<name>(visible vialsof -p <spare-pid>). - Finish the work.
/land applyrunsExitWorktree(action="remove")→ worktree dir is unlinked. - Days later, start a new bg job. cc-daemon picks an idle spare. If it picks one whose cwd was the now-deleted worktree:
lsof -p <spare-pid>showscwd DIR <dev> 64 <inode> <deleted-path>(kernel still resolves the inode; path is stale).- Claude Code derives session-dir slug from that cwd → writes to
~/.claude/projects/-<encoded-stale-path>/. - User's
/clearand resume picker subsequently fail withPath does not exist.
Evidence
# 1. Stale spare process
$ ps -o pid,ppid,etime,command -p 44099
PID PPID ELAPSED COMMAND
44099 44096 01-04:12:11 /Users/lyc/.local/share/claude/versions/2.1.148 \
--bg-spare /tmp/cc-daemon-501/14e2f272/spare/f0f46b90.claim.sock
# 2. Spare's cwd is a deleted worktree (path printed but dir gone from filesystem)
$ lsof -p 44099 | grep cwd
2.1.148 44099 lyc cwd DIR 1,15 64 17820416 \
/Users/lyc/Projects/dialedin-strength/.claude/worktrees/matt-ulw-246-237-186-187-199
$ ls /Users/lyc/Projects/dialedin-strength/.claude/worktrees/
(empty)
$ git -C /Users/lyc/Projects/dialedin-strength worktree list
/Users/lyc/Projects/dialedin-strength f681741 [main]
# ↑ no worktree entry for matt-ulw-246-237-186-187-199; gh worktree was removed days ago
# 3. New bg job assigned this spare (PPID matches)
$ ps -o pid,ppid -p 56274
PID PPID
56274 44099 # ← my new bg job's parent is the stale spare
# 4. Session JSONL writes to the orphan slug, recreating it on demand
$ ls ~/.claude/projects/ | grep matt-ulw
-Users-lyc-Projects-dialedin-strength--claude-worktrees-matt-ulw-246-237-186-187-199/
$ ls -la ~/.claude/projects/-Users-lyc-Projects-dialedin-strength--claude-worktrees-matt-ulw-246-237-186-187-199/
total ...
-rw------- 1 lyc staff 523213 May 23 17:07 c1bd3649-9933-4ff6-9c54-b63a346cc10a.jsonl
# ↑ actively growing, while the cwd path itself doesn't exist
Symptoms surfaced to user
$ # user runs /clear
local-command-stderr: Error: Path "/Users/lyc/Projects/dialedin-strength/.claude/worktrees/matt-ulw-246-237-186-187-199" does not exist
User-facing remediation today: manually trash the orphan dir under ~/.claude/projects/ — but a stale-cwd spare will recreate it on next bg job. The only durable fix without daemon changes is to restart the cc-daemon (which destroys all spares) and let it spawn fresh ones.
Suggested fixes (any one is sufficient)
- Validate spare cwd at claim time. Before handing a spare to a new bg job, the daemon checks
stat(cwd); if missing, kill the spare and pop the next one. Cheap, race-free, no protocol change.
- chdir the spare to the requesting job's CWD before opening session JSONL. Decouples spare allocation from cwd-based slug derivation. The slug should reflect the actual working directory of the new bg job, not the spare's birth cwd.
- Periodic spare-pool reaper. Background sweep every N minutes —
stat(cwd)each spare, kill stale ones. Lossy but eventually-consistent.
- Invalidate spares on
ExitWorktree(remove)andgit worktree remove. Harder — would need integration with git hooks or a daemon-side filesystem watch. Probably overkill compared to (1) or (2).
I think (1) is the right fix: smallest surface, deterministic, runs once per claim instead of every cwd-touch.
Why this is worth fixing
The trap is silent — there's no error at the time the worktree is removed, only days later when /clear or the resume picker is invoked. Skill-level cleanup (/land apply trashing the slug dir, /orchestrator scanning at boot) can't catch the underlying issue because the spare keeps recreating the dir on each bg-job claim. Multiple users have likely hit this without diagnosing further than "/clear sometimes complains about a weird path."
I'm happy to open a PR if (1) is the agreed direction.
---
_Discovered while debugging a /clear error on dialedin-strength repo, 2026-05-23. Workaround script: see attached cleanup-stale-bg-spares.sh (kills stale spares + trashes orphan session dirs)._
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗