Guide

Claude Code persistent sessions and forking, explained

Two ways to fork a Claude Code session, one file on disk that does all the work, and the part most guides skip: the workspace is not forked, only the conversation. Here is the whole picture, from the CLI flag down to the JSONL on disk and the practical consequences when you actually run these sessions in production.

M
Matthew Diakonov
9 min

Direct answer, verified 2026-05-14

Two official ways to fork a Claude Code session:

  1. Inside an open session, type /branch. The current conversation forks at this turn; you keep going on the new branch without losing the old one.
  2. From a fresh terminal, run claude --resume <session-id> --fork-session. The named session is resumed into a brand new session id with a copy of its transcript. The original is untouched.

Both copy the JSONL transcript at ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl. They do not copy the working directory, background processes, or any external state. Source: docs.claude.com/en/docs/claude-code and the Agent Client Protocol RFD at agentclientprotocol.com/rfds/session-fork.

The three layers of persistence

Forking only makes sense once you see how a Claude Code session persists in the first place. There are three layers, each with its own lifetime, its own fork story, and its own way of getting out of sync with the other two.

What persists, where, and how long

1

Layer 1, transcript

One JSONL file at ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl. Survives reboots. Forked by /branch or --fork-session.

2

Layer 2, sandbox VM

Persistent disk (/app, /root) survives pause and resume. Tmpfs (/run) does not. Auto-resumes on Sandbox.connect.

3

Layer 3, workspace git

Per-session local git in /app. Each turn is a commit; a history stack and activeIndex pointer give you undo, redo, and jump.

Layer 1 is the only one Claude Code itself manages, and the only one a fork operates on directly. Layer 2 and Layer 3 are the host's problem. On a hosted product like ours, that means the E2B sandbox lifecycle and a per-session git index in /app. On a single laptop running the CLI, it is just your normal working directory. Either way, the fork only copies the transcript.

Where the JSONL actually lives

Claude Code stores every session as one JSONL file under ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl. The <encoded-cwd> is the absolute working directory with every non-alphanumeric character replaced by a dash. A session started in /Users/me/myproj lands under ~/.claude/projects/-Users-me-myproj/. Each event is one line of JSON.

The practical upshot of one-file-per-session: a fork is literally file copy plus new id. A session move between machines is scp. A long-term archive is git add. You can list everything Claude Code knows about for the current project with one line of shell, no API call:

ls ~/.claude/projects/$(pwd | sed 's:[^a-zA-Z0-9]:-:g')/

That is also why --fork-session feels so cheap. The whole operation is reading one JSONL file, assigning a new session id, and writing a copy alongside it. No network call, no service to coordinate, no migration. Every subsequent prompt on the new id appends to its own file; the original keeps appending to its own. They diverge from byte 0 of the next event.

What fork copies, and what it leaves shared

This is the part most guides skip and the part that bites in production. The fork is a conversational fork. It does not clone your filesystem.

What gets copied

  • Every prior user turn
  • Every prior assistant turn
  • Every tool call and tool result
  • Session metadata (cwd, mcpServers, systemPrompt)
  • New session id generated by the agent

What stays shared

  • The working directory and the files in it
  • Any background processes (Vite, browser, daemons)
  • Any database or external service state
  • Claude Code credentials at ~/.claude/.credentials.json
  • MCP server processes and their internal state

If you fork mid-edit and the agent on the new branch starts writing files, the agent on the original branch sees those files the next time it lists the directory. Two forks pointing at one workspace is one workspace, period. The remedies are either serialise (one active fork at a time per workspace), clone (each fork gets its own checkout), or virtualise (each fork gets its own sandbox). On mk0r we mostly serialise per session, and for the rare parallel case we clone into a fresh checkout from the SHA the fork was anchored to.

How the fork call flows through the Agent Client Protocol

When Claude Code runs as an embedded agent, every session lifecycle operation is a JSON-RPC method on the Agent Client Protocol. The fork is one of them. Below is the round trip when a host (say, a web app) asks the agent to fork an existing session.

Forking a session over ACP

HostACP bridgeAgent processJSONL on diskPOST /session/fork {sessionId}createSession({forkSession: <id>})read <id>.jsonltranscript eventswrite <new-id>.jsonl (copy){ sessionId: <new-id> }200 OK { sessionId: <new-id> }POST /session/prompt {sessionId: <new-id>}session/promptsession/update stream (new turn)

The interesting line is the second one. The ACP entry point calls a single method, createSession, and that one method handles all four lifecycle paths: newSession, resumeSession, loadSession, and forkSession. The agent disambiguates them with creationOpts.forkSession and creationOpts.resume. In our codebase, that disambiguation lives at docker/e2b/files/opt/patched-acp-entry.mjs, where we wrap ClaudeAcpAgent.prototype.createSession once and get hooks on all four lifecycle paths for free. That one-chokepoint design is why a forked session in our environment still streams compaction events, retry events, and cost metadata the same way a fresh one does.

The workspace side of the fork

When the transcript forks but the workspace does not, you get drift. A practical example, drawn from how our sandbox handles undo and redo: every assistant turn that mutates /app is committed to a local git repo with a generated message, and the resulting SHA is appended to a per-session historyStack with an activeIndex pointer. Undo walks the pointer back one SHA and reverts the tree; redo walks it forward.

Here is the part that mirrors the transcript fork story almost exactly:

// src/core/e2b.ts (around line 1930)
session.historyStack = session.historyStack ?? [];
if (
  typeof session.activeIndex === "number" &&
  session.activeIndex >= 0 &&
  session.activeIndex < session.historyStack.length - 1
) {
  // New commit arrived while we were "in the past"
  // Drop everything past the active index.
  session.historyStack = session.historyStack.slice(
    0,
    session.activeIndex + 1
  );
}
session.historyStack.push(sha);
session.activeIndex = session.historyStack.length - 1;

This is the destructive case. The user has undone three turns, then asks the agent to do something new. The new commit appends at the active index; everything past it is thrown away. The workspace has chosen one timeline and forgotten the other. That is exactly the situation --fork-session is designed to prevent on the transcript side. The way out is the same on both sides: before the destructive move, fork. On the transcript, call /branch. On the workspace, snapshot the SHA into a side reference before the truncate would happen. We pair those two operations behind a single user-visible action so they cannot drift.

What survives a paused sandbox

Persistent sessions sit between two extremes. On one end, a laptop running Claude Code locally is persistent in the boring sense: the disk does not go away unless you delete files. On the other end, a serverless agent that boots fresh on every request has no persistence at all. The interesting middle is a sandbox that pauses when idle and resumes on demand, which is what we run.

The rules are simple, but they bite if you skip them. The persistent disk ( /app, /root) survives pause and resume. /run is tmpfs and gets wiped, so any credentials or sockets you wrote there are gone. The Claude credentials at ~/.claude/.credentials.json survive, but their refresh tokens rotate, so blindly pushing a host-cached copy of the file on resume will force re-auth (the fix is to merge, not overwrite). The transcript JSONL at ~/.claude/projects/.../<id>.jsonl survives. Forking on the resume side of a pause works exactly the same as forking before it, because the file the fork operates on never went anywhere.

A short mental model

Treat the JSONL as the source of truth for the conversation, the working directory as the source of truth for the artefact, and the agent process as a stateless interpreter that loads one from the other at the start of each session. Forking is then a one-line operation on the conversation: copy the JSONL, hand the new id to a fresh interpreter. Anything you want to fork about the artefact is your problem, and the cheapest answer in practice is a git SHA you can checkout into a sibling directory.

Once the model clicks, the slash command and the CLI flag stop being two features and start being two ergonomics for the same file copy. Pick whichever one matches the moment.

Want this whole stack running for you?

If you are building on Claude Code and want the persistent-session and fork layers wired up the way mk0r does, book a 20-minute call and we will walk through the architecture together.

Frequently asked questions

Frequently asked questions

What is the actual command to fork a Claude Code session?

Two work. Inside an open session, type the /branch slash command and Claude Code creates a forked session that inherits the current transcript and lets the new one diverge. From a fresh terminal, run `claude --resume <session-id> --fork-session`; this resumes the named session into a brand-new session id with a copy of the transcript, leaving the original untouched. Both are documented in the Claude Code CLI reference at https://docs.claude.com/en/docs/claude-code/ and follow the lifecycle pattern that the Agent Client Protocol formalises as `session/fork` (RFD at https://agentclientprotocol.com/rfds/session-fork).

Where does Claude Code actually store a session on disk?

Under `~/.claude/projects/<encoded-cwd>/<session-id>.jsonl`. The `<encoded-cwd>` is the current working directory with every non-alphanumeric character replaced by a dash, so a session started in `/Users/me/myproj` lands under `~/.claude/projects/-Users-me-myproj/`. Each session is one JSONL file, one event per line. Forking copies that file into a new session id within the same project directory; resuming opens the existing file in place. You can list everything Claude Code knows about for a given project with `ls ~/.claude/projects/$(pwd | sed 's:[^a-zA-Z0-9]:-:g')/`.

What exactly does a fork copy and what does it leave shared?

It copies the transcript (every user turn, assistant turn, tool call, and tool result up to the fork point) and the lightweight session metadata (cwd, mcp servers, system prompt). It leaves shared: the working directory and any files in it, any background processes you started, any database the agent has been writing to, any API state the agent has been mutating, and Claude Code's own credentials at `~/.claude/.credentials.json`. Forking is a conversational fork, not a virtual-machine fork. If you want the workspace to fork with the conversation, you have to do that yourself (snapshot the directory, or, in our case, snapshot a git SHA in /app).

Why is the in-session /branch command useful if --fork-session does the same thing?

/branch is for the moment you realise mid-task that you want to try a different approach without throwing away what you already have. You run it, the new branch picks up from the current turn, and you can keep going without losing the old line of work. --fork-session is for the moment you want to take a finished session and revisit it cold from a specific checkpoint, often days later, in a fresh terminal. They are the same primitive served two different ways: keep flowing, or come back later. Boris Cherny on the Claude Code team has called both out as the supported paths.

How does the Agent Client Protocol relate to all of this?

The Agent Client Protocol (ACP) is the JSON-RPC protocol Claude Code speaks when it is embedded as an agent backend. It defines lifecycle methods for sessions: `session/new`, `session/load`, `session/prompt`, and a draft `session/fork` (the RFD is at https://agentclientprotocol.com/rfds/session-fork). When you spawn `@agentclientprotocol/claude-agent-acp` as a subprocess, every fork-equivalent operation, including the CLI's `--fork-session`, eventually calls `createSession` in the agent class with `creationOpts.forkSession = true`. That single chokepoint is the reason mk0r's patched entry at docker/e2b/files/opt/patched-acp-entry.mjs hooks `createSession` once and gets new, resumed, loaded, and forked sessions for free.

Can two forked sessions safely write to the same workspace?

Only if you accept the consequences. Both sessions see the same `/app`, the same git index, the same node_modules, the same dev server on the same port. If they edit the same file concurrently, last write wins and the loser's tool results lie about state. The practical pattern is: one active fork at a time per workspace, and if you really want parallel forks, give each fork its own working directory (mk0r does this by cloning the git history into a fresh checkout per fork) or its own VM (we do this for fully independent forks). The transcript JSONL is the easy part to fork; the filesystem is the hard part.

What breaks when a session is persistent but the VM gets paused?

Anything stored outside the persistent disk does. Concretely on a Cloud Run-fronted E2B sandbox: `/run` is tmpfs and is wiped on pause, so any credentials, sockets, or session state you wrote there is gone after resume. `/app` survives. `~/.claude/projects/...` survives. The credentials at `~/.claude/.credentials.json` survive, but their refresh token may have rotated under you between pause and resume, so blindly overwriting them with a stale host-pushed token forces re-auth (the production fix is to never overwrite a newer on-disk token, see the acp-bridge.js credential-merge logic). Forking on resume works the same as forking before pause, because the JSONL is on the persistent disk and survives the pause unchanged.

Is there a way to fork a session across machines?

Yes, but ad hoc. The transcript is a single JSONL file on disk, so the portable move is to copy `~/.claude/projects/<encoded-cwd>/<session-id>.jsonl` to the other machine into the matching project directory, then `claude --resume <session-id>` on that side. You may need to rename the project directory to match the new cwd encoding. There is no first-party sync today, only the file. Treat the JSONL as the artifact and you can move sessions between laptops, between a laptop and a remote sandbox, or into version control if you want a permanent record.

What does mk0r do differently for forks in its hosted environment?

Two things. First, every Claude Code turn that mutates `/app` is committed to a local git repo with a generated message; the SHA is appended to a per-session `historyStack` in `src/core/e2b.ts` and an `activeIndex` pointer marks the current head. Second, the `forkSession` notification flows through our patched ACP entry, which lets the host pair every transcript-level fork with a workspace-level snapshot (`git checkout <sha> -- .` into a fresh fork directory). The two layers are kept honest: the conversation forks and the working directory forks together, or the new conversation makes no sense.

Can I see a real session forking flow before I build one?

Yes. Open https://mk0r.com without signing up, describe an app, and as the agent works it streams updates from a Claude Code session running inside a paused-resumable E2B sandbox. Hit undo and you are jumping the workspace pointer over an immutable transcript; redo and you are walking it back. The same primitives this page describes are the ones you are using.

mk0r.AI app builder
© 2026 mk0r. All rights reserved.

How did this page land for you?

React to reveal totals

Comments ()

Leave a comment to see what others are saying.

Public and anonymous. No signup.