Context
We share the claude CLI's session store at ~/.claude/projects/
between First Mate crewmate sessions and the Claude Desktop App — that's the
same store the Desktop App reads to populate its session list, and it's not
scoped per-tool. Every crewmate spawned into a disposable treehouse worktree
runs a claude CLI session that writes an entry there, keyed by the working
directory.
fm-teardown.sh removes the worktree once a crewmate is done, but it doesn't
touch that session-store entry. The entry has no other owner and nothing else
ever cleans it up, so it becomes a permanent ghost: a directory in
~/.claude/projects/ pointing at a path that no longer exists. Over time these
accumulate and clutter the Desktop App's session list with sessions that look
like live, orphaned, or unexplained activity — it initially looked
like First Mate was "spawning crewmates in the Desktop App," which it
wasn't; it was just this leftover visibility side effect.
We verified this, not just theorized it. We reproduced the mechanism
end-to-end in a throwaway worktree: ran a one-shot claude CLI session in a
scratch directory, confirmed a matching entry appeared under
~/.claude/projects/ (newest by mtime), deleted the scratch directory, and
confirmed the entry survived unchanged — the ghost, reproduced on demand.
Measured scale. A survey of one real ~/.claude/projects/ store (140
entries total) found roughly 121 pointing at directories that no longer exist
on disk. Of those, about 73 are dead worktrees from an automated no-mistakes
pipeline and about 18 are dead First Mate treehouse worktrees (the remainder
are older/unrelated dead paths). So this isn't a hypothetical edge case — it
was the majority of that store's entries, and it grows by one ghost per
teardown with no offsetting cleanup.
A detail worth flagging for whoever implements this. The store's directory
name is a dashed encoding of a path, but it's not reliably decodable back to
the original invocation path — we found real cases where it doesn't match the
literal path passed to the CLI. Concretely: running claude from /tmp/foo
on macOS produces a store entry named for /private/tmp/foo (the
symlink-resolved absolute path), not /tmp/foo. The reliable source of truth
is the cwd field recorded inside each session's .jsonl transcript (present
on real chat-turn lines, absent only on the first summary line), not the
dashed directory name. Since fm-teardown.sh already has the resolved
worktree path in hand at the moment it's about to rm the worktree, it's
well-positioned to match against that same resolved path rather than trying
to reconstruct it from the store's naming scheme.
Suggested fix
When fm-teardown.sh removes a crewmate's worktree, also look for and remove
the corresponding entry under ~/.claude/projects/ — matched by the
resolved absolute worktree path (matching against cwd in the entry's
transcript, or equivalently against however the resolved path maps to the
store's directory naming, since teardown already computes that resolved path
for the rm step anyway). This closes the leak at its source, at teardown
time, rather than relying on a separate downstream janitor to sweep for
ghosts after the fact.
Happy to share more detail on how this was measured or the reproduction
steps if useful. This felt like a good one to raise upstream since teardown
is the one place that reliably knows both "this worktree is going away" and
"here's its exact path" at the same moment.
Context
We share the
claudeCLI's session store at~/.claude/projects/between First Mate crewmate sessions and the Claude Desktop App — that's the
same store the Desktop App reads to populate its session list, and it's not
scoped per-tool. Every crewmate spawned into a disposable treehouse worktree
runs a
claudeCLI session that writes an entry there, keyed by the workingdirectory.
fm-teardown.shremoves the worktree once a crewmate is done, but it doesn'ttouch that session-store entry. The entry has no other owner and nothing else
ever cleans it up, so it becomes a permanent ghost: a directory in
~/.claude/projects/pointing at a path that no longer exists. Over time theseaccumulate and clutter the Desktop App's session list with sessions that look
like live, orphaned, or unexplained activity — it initially looked
like First Mate was "spawning crewmates in the Desktop App," which it
wasn't; it was just this leftover visibility side effect.
We verified this, not just theorized it. We reproduced the mechanism
end-to-end in a throwaway worktree: ran a one-shot
claudeCLI session in ascratch directory, confirmed a matching entry appeared under
~/.claude/projects/(newest by mtime), deleted the scratch directory, andconfirmed the entry survived unchanged — the ghost, reproduced on demand.
Measured scale. A survey of one real
~/.claude/projects/store (140entries total) found roughly 121 pointing at directories that no longer exist
on disk. Of those, about 73 are dead worktrees from an automated no-mistakes
pipeline and about 18 are dead First Mate treehouse worktrees (the remainder
are older/unrelated dead paths). So this isn't a hypothetical edge case — it
was the majority of that store's entries, and it grows by one ghost per
teardown with no offsetting cleanup.
A detail worth flagging for whoever implements this. The store's directory
name is a dashed encoding of a path, but it's not reliably decodable back to
the original invocation path — we found real cases where it doesn't match the
literal path passed to the CLI. Concretely: running
claudefrom/tmp/fooon macOS produces a store entry named for
/private/tmp/foo(thesymlink-resolved absolute path), not
/tmp/foo. The reliable source of truthis the
cwdfield recorded inside each session's.jsonltranscript (presenton real chat-turn lines, absent only on the first summary line), not the
dashed directory name. Since
fm-teardown.shalready has the resolvedworktree path in hand at the moment it's about to
rmthe worktree, it'swell-positioned to match against that same resolved path rather than trying
to reconstruct it from the store's naming scheme.
Suggested fix
When
fm-teardown.shremoves a crewmate's worktree, also look for and removethe corresponding entry under
~/.claude/projects/— matched by theresolved absolute worktree path (matching against
cwdin the entry'stranscript, or equivalently against however the resolved path maps to the
store's directory naming, since teardown already computes that resolved path
for the
rmstep anyway). This closes the leak at its source, at teardowntime, rather than relying on a separate downstream janitor to sweep for
ghosts after the fact.
Happy to share more detail on how this was measured or the reproduction
steps if useful. This felt like a good one to raise upstream since teardown
is the one place that reliably knows both "this worktree is going away" and
"here's its exact path" at the same moment.