bg-spare daemon doesn't invalidate spares whose cwd is deleted, causing orphan session dirs and `/clear` errors

Resolved 💬 2 comments Opened May 23, 2026 by liyoungc Closed Jun 23, 2026

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:

  • /clear errors with Path "<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

  1. From the host, start any worktree-aware skill workflow (in our case /branch auto-EnterWorktree + /matt ulw).
  2. Skill enters a git worktree at <repo>/.claude/worktrees/<name>, opens a bg job inside.
  3. cc-daemon pre-allocates --bg-spare processes; some have cwd = <repo>/.claude/worktrees/<name> (visible via lsof -p <spare-pid>).
  4. Finish the work. /land apply runs ExitWorktree(action="remove") → worktree dir is unlinked.
  5. 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> shows cwd 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 /clear and resume picker subsequently fail with Path 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)

  1. 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.
  1. 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.
  1. Periodic spare-pool reaper. Background sweep every N minutes — stat(cwd) each spare, kill stale ones. Lossy but eventually-consistent.
  1. Invalidate spares on ExitWorktree(remove) and git 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)._

View original on GitHub ↗

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