`--fork-session` leaves CLAUDE_CODE_SESSION_ID set to the parent session's id

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 3 comments · opened Jul 26, 2026

What happened

A session started with --fork-session exposes CLAUDE_CODE_SESSION_ID set to the parent session's id rather than its own.

Anything that reads that variable — hooks, Bash tool commands, helper scripts — therefore attributes the forked session's actions to the session it was forked from.

Environment

  • Claude Code 2.1.220
  • macOS 26.5.2 (Apple silicon)

Reproduction

  1. Run a session in some directory. Call it A.
  2. From a new terminal, fork it (--fork-session). Call it B.
  3. In B, run a Bash command that prints $CLAUDE_CODE_SESSION_ID.

Expected

B reports its own session id.

Actual

B reports A's session id.

Meanwhile ~/.claude/sessions/<pid>.json is correct for both sessions throughout — each file carries the right, distinct sessionId for its own PID. So the platform holds the correct value and only the exported environment variable disagrees with it.

Observed with two concurrently-live sessions in the same working directory: distinct PIDs, distinct session files, distinct sessionId values — but one shared CLAUDE_CODE_SESSION_ID.

Why this is more than a cosmetic mislabel

Any tool that treats CLAUDE_CODE_SESSION_ID as a unique per-session key gets two live sessions colliding on one identity, and every resulting failure is silent:

  • Per-session state collides. "What have I already seen" watermarks keyed on the session id are shared, so whichever session reads first consumes for both.
  • Self-filtering inverts. We use the session id to suppress a session's own messages in a shared message file. Two sessions sharing an id suppress each other's messages as their own — they become mutually invisible, with no error on either side. Each simply concludes the other is idle.
  • Writes are misattributed. Locks, claims and audit entries are recorded against the wrong session.

The variable is also inherited by child processes, which compounds it: consumers cannot distinguish an inherited value from a legitimate one.

Possible fixes

Listing these because the trade-offs differ and we do not have a preference:

  1. Set the variable to the new session's id before any user-reachable process can read it. The correct value already exists (it is written to the per-PID session file), so this looks like an ordering issue. Smallest change, but leaves the variable inheritable, so a session launched from another session's shell can still collide.
  2. Do not export it in forked sessions. Consumers fall back to the per-PID session file, which is authoritative. But absence is indistinguishable from an older client, so consumers that never learned about the file silently get no identity at all.
  3. Stop exporting it entirely and make the per-PID session file the only source. Fixes the whole class, at the cost of a breaking change for anything reading the variable today.
  4. At minimum, document that it is inheritable and not a reliable unique session key, so integrators do not build on it as we did.

Workaround

Resolve identity from ~/.claude/sessions/$CLAUDE_PID.json and treat CLAUDE_CODE_SESSION_ID as a hint only, warning when the two disagree — a mismatch means the reader is a child process and the variable is stale.

Not investigated

The precise ordering inside the binary. The shipped bundle is minified into very long single lines, so string-based inspection returns blobs rather than call-site context. The behaviour above is from direct observation instead.

View original on GitHub ↗

3 Comments

willmcginnis · 1 month ago

Correction to my own report, from a follow-up test.

The issue body says the variable "is also inherited by child processes, which compounds it." That is not supported for spawned sessions, and I should not have stated it.

Test: launched a child session from a shell with both variables deliberately poisoned —

CLAUDE_PID=999999 CLAUDE_CODE_SESSION_ID=deadbeef-0000-0000-0000-000000000000 claude -p "<print both>"

The child reported its own pid and its own session id, overriding both. A separately-forked session independently confirmed that CLAUDE_PID is its own pid rather than its parent's.

So a normally-spawned session resets these correctly. What remains reproducible is only the --fork-session case in the original report: the forked session carries the parent's CLAUDE_CODE_SESSION_ID while its own per-PID session file is correct.

The accurate statement is narrower: the variable is exported into subprocesses (so a helper script sees its own session's id, which is correct and intended), but a session started with --fork-session exposes the parent's id rather than its own. Everything else in the report stands.

willmcginnis · 1 month ago

Two further observations from continued use, plus a fix option I should have listed originally.

1. The divergence appears to be transient, not permanent

The two sessions in the report (b7828e74… and 6178dc65…) no longer share an id.

I could not measure the variable directly: on macOS ps eww cannot read another process's environment. I verified that instrument was useless before trusting it — it returned nothing for a variable I had just set on a probe process — and discarded my first attempt at this measurement rather than report it.

The usable evidence is indirect, but the chain is short. One of our hooks derives a per-session state filename directly from CLAUDE_CODE_SESSION_ID. Both sessions now have their own distinct state file. That is only possible if each session's copy of the variable currently reads its own id.

What this does not establish: when it corrects, or whether it corrects reliably. Those state files are rewritten in place, so their timestamps date the last write, not the first. The window is bounded; its size is unknown.

This narrows the impact from "two sessions permanently share an identity" to "two sessions share an identity for some initial window." That is still worth fixing, because the first thing a freshly forked session tends to do is announce itself — so the exposed window overlaps precisely with the writes that establish who a session is.

2. CLAUDE_PID is not affected

Independently confirmed on this machine, by two people working separately: CLAUDE_PID is the session's own pid, and it is not inherited by a child process one level down.

That is what makes the workaround in the original report sound. ~/.claude/sessions/$CLAUDE_PID.json cannot be inherited the way an exported variable can, so it stays correct in a context where the variable does not.

3. A fifth option — the hook payload already carries the right value

I listed four possible fixes. There is a fifth, which costs nothing and already ships: the JSON payload delivered to hooks on stdin contains session_id.

For hook authors this looks structurally better than either the variable or the PID file — it is per-invocation and passed in by the caller, so there is no mechanism by which it could be inherited from a parent process. We had avoided reading it on the assumption that a hook blocking on stdin could hang the prompt; that turned out not to be a real constraint, and we now have a hook reading the payload on both UserPromptSubmit and PostToolBatch without issue.

To be explicit about what I have and have not checked: I have not observed the stdin session_id during an active divergence window, because the divergence self-corrected before I could construct that test. So "cannot be inherited" here is an argument from how the value is delivered, not a measurement. If you already know the payload is populated from the same place as the environment variable, this option is worth nothing and I would rather you discard it than take my word for it.

It does not help non-hook consumers — a plain Bash command still only has the variable — so it mitigates one class of consumer rather than fixing the bug. But hooks are probably the largest group of things reading this variable, so it may be worth documenting alongside whichever fix you choose.

willmcginnis · 1 month ago

Correcting point 2 of my previous comment, which was wrong as written.

I said CLAUDE_PID "is not inherited by a child process one level down." That is false. A child shell and a grandchild shell both report the session's CLAUDE_PID; it is exported and inherits normally, like any other environment variable. I had generalised from a narrower check (that two concurrent sessions each carry their own pid, not each other's) and stated it far too broadly.

What I should have said, and what the workaround actually relies on:

CLAUDE_PID is the pid of the Claude Code process itself. So within any process descended from a given session — including a hook — it resolves to that session, which is the property that makes ~/.claude/sessions/$CLAUDE_PID.json usable where the session-id variable is not. A newly launched session gets its own value rather than the launching session's.

Two consequences worth being explicit about, since I got this wrong once already:

  • It is not a "which agent am I" discriminator. Because it inherits, a subagent's tooling sees the parent session's pid, exactly as it sees the parent's session id. If you are trying to tell a subagent's activity apart from its parent's, neither variable helps.
  • It is still sound for the case in this issue. A --fork-session session's own pid and its own entry under ~/.claude/sessions/ are correct, so resolving identity through the pid map returns the right answer where the environment variable returns the parent's.

Sorry for the churn on this thread.