[BUG] Forking reuses the parent's plan-file slug — all forks collide on the same ~/.claude/plans/<slug>.md (regression of #31677)

Open 💬 2 comments Opened Jul 10, 2026 by 100yenadmin

Severity: P0 — silent data loss that nullifies the core guarantee of /fork

Forking exists to let a user branch a conversation and explore multiple approaches in parallel without the branches affecting one another. The documented contract (how-claude-code-works.md: "The original session remains unchanged"; #31677: "each fork gets its own independent plan file") is isolation. That isolation is the entire value of the feature.

This bug breaks that guarantee at the plan-file layer: every fork of a session resolves to the same ~/.claude/plans/<slug>.md, so forks silently overwrite each other's plan. It is not cosmetic — it is silent data loss plus a correctness hazard (a fork can execute against a different fork's plan), and it fails with no error and no warning. It scales with the exact usage forking is meant to enable: the more branches a power user runs in parallel, the more they clobber each other.

This is also the failure mode #31677 states was already fixed ("Before the fix, forks shared the same plan file, causing plan edits in one fork to silently overwrite the other's plan"). It has regressed, or the fix never covered the Desktop /fork path.

User story

As a developer, I ask Claude Code to help me evaluate two competing implementation strategies for a module. At the planning point I fork the session into Fork A — "refactor in place" and Fork B — "rewrite the module", so I can develop both plans in parallel and compare them. Both forks are in Plan Mode. While Fork B refines its plan, its write to ~/.claude/plans/<slug>.md silently overwrites Fork A's plan file (same slug, same path). When I switch back to Fork A to execute, its plan now contains Fork B's rewrite plan. Fork A either executes the wrong plan against my repo, or my refactor plan is simply gone — with no error, no warning, no indication anything crossed over. I explicitly told the model in Fork A to "use a different plan file" — but that does nothing, because the model doesn't choose the plan path; the harness derives it from the inherited slug. The one workflow forking exists to enable — isolated parallel exploration — is the workflow it silently corrupts.

The failure is worse the more the feature is used as intended: N parallel forks all race on a single file, last-writer-wins, and any fork that later re-reads its plan gets whichever sibling wrote last.

Impact summary

  • Silent data loss — a fork's plan is overwritten with no error/warning.
  • Correctness hazard — a fork can execute (edit a real repo) against another fork's plan.
  • Breaks the feature's core invariant — fork isolation is the documented, load-bearing guarantee; this voids it.
  • Scales with intended usage — parallel/branching workflows (incl. multi-agent orchestration) are exactly where forks are load-bearing, and exactly where the collision compounds.
  • Regression — previously reported fixed (#31677).

Environment

  • Claude Code CLI: 2.1.186
  • Claude Desktop: 1.20186.0
  • Platform: macOS (Darwin arm64)
  • Reproduced via Desktop /fork and claude --continue --fork-session.

Repro

  1. Start a session and enter Plan Mode; a plan is written to ~/.claude/plans/<slug>.md (e.g. so-i-think-the-wiggly-newt.md).
  2. Fork the conversation one or more times (Desktop /fork, or claude --continue --fork-session).
  3. Enter/continue Plan Mode in each fork and edit the plan.

Expected: each fork gets its own isolated plan file (per #31677's documented post-fix behavior). Edits in one fork do not affect siblings or the original.

Actual: all forks resolve to the same <slug>.md; the last writer wins and the others' plans are silently overwritten. Instructing the model to use a different filename has no effect — the harness, not the model, derives the path from the inherited slug.

Evidence that the slug is re-read, not regenerated

The plan slug is stamped onto every message in the transcript ("slug":"…" on each row). On my machine a single slug so-i-think-the-wiggly-newt is carried across 193 distinct transcript files, and the on-disk plan files share the identical random suffix:

so-i-think-the-wiggly-newt.md
so-i-think-the-wiggly-newt-agent-a23f34039f0b36fdd.md   # sub-agents DO get unique ids (correct)
so-i-think-the-wiggly-newt-agent-a6cc2c8e0bba29bb1.md
...

The random component (wiggly-newt) being identical across every fork is the tell: regeneration would pick a new random word each time. Sub-agents (-agent-<id>) get unique suffixes and are unaffected — only the fork base slug collides.

Root cause (from the 2.1.186 binary)

The slug is generated once, then persisted on every message and restored from the transcript on resume/fork:

// slug extracted directly from inherited messages:
function i3l(e){ return e.messages.find(t => t.slug)?.slug }

// "copyPlanForResume": pins planSlugCache[sessionId] to the transcript slug:
function L2n(e,t){ let n = i3l(e); if(!n) return false; let r = t ?? xt(); aOo(r,n); /* … */ }

// resume path — L2n runs UNCONDITIONALLY; forkSession only gates the *next* call:
if (s) await L2n(r, NT(s)),
       I2n(r, !n.forkSession && s ? NT(s) : void 0),   // <- forkSession guard is here, not on L2n
       o = r.messages;

Because L2n (transcript slug-restore) has no forkSession guard, a fork inherits the parent's exact slug. The collision-avoidance loop in the slug generator (Vxe, which checks existsSync before minting a name) is bypassed for forks, since the cache is pre-populated from the transcript. Fork message processing (V7t) only neutralizes model_refusal_fallback markers and never touches the slug, and a search for forkSession interacting with the slug / planSlugCache finds nothing.

Suggested fix

When forkSession === true, skip the transcript slug-restore (L2n) and force a fresh slug so Vxe's existsSync loop mints a non-colliding <slug>.md for the fork (mirroring how sub-agents already get unique -agent-<id> files). Optionally strip the stale slug field from inherited messages on fork so re-derivation can't re-pin it.

Note

The only plan-related config (plansDirectory) relocates the directory but does not change the per-fork filename, so it is not a workaround for forks sharing a project root. Cross-reference: #31677 (docs issue describing the intended isolated behavior).

View original on GitHub ↗

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