how it works

Forks

In Claude Code, Leaf hands the user's thread messages to forks: copies of the session's conversation that run beside the session. A fork posts the agent's first words in the thread within a few seconds, whether the session is busy or idle, and a fork with the session's tools can do a small task and answer it while the session carries on with its own work.

Today the first words come from a fork in every Claude Code session, unless LEAF_FORK_REPLIES=off is set. A fork that does the work, and takes its thread's later messages, is built, and acts only with LEAF_FORK_WORK=on in Claude Code's environment; the rest of the design is planned. Each part below says which, and What's built and what's planned lists them.

This page is the design's reference, for people and for agents. It explains the mechanism. The exact rules live in the session lifetime contract and in the module headers listed under Where each rule is written. The research and measurements behind the design are in notes/fork-replies.md.

Names on this page

Main thread
The session's own conversation, the one the user talks to, as opposed to the forks and subagents made from it.
Line fork
A fork made with $.model.fork, which resends the main thread's last request with one question from Leaf and returns text in 2 to 3 s. It can't call tools. It writes the working message and sorts the message.
Working fork
A background subagent with the main thread's transcript, model and tools ($.agent.spawn with subagentType: "fork"), named leaf-<thread> after the thread it works on. It does that thread's work and answers it.
Working line
The agent's progress message in a thread, saying what it will do about the message, such as “reading the reconnect traces”. It stands until the agent's answer replaces it.
Waiting messages
The user's messages in a thread since the agent's last answer there. The newest one owes the thread's answer.
Kept back
A new thread message Leaf withholds from the main thread while a line fork writes its working message and sorts it, until 30 s after the thread's newest message, so a message that follows restarts the wait.
Holds
A turn holds a thread's messages from the moment it takes the thread, by picking up its messages or answering it, until another turn does. Who holds a message gives the exact rule.
Take
The line fork's judgment that a working fork should do the work (take), and the act that gives the thread to one: a turn Leaf opens for the fork picks the thread's messages up.
Hand over, hand back
Leaf hands a kept-back message over to the main thread, as the line fork's hand_over judgment asks or when keeping it back ends. A working fork hands its thread back to the main thread, with HAND BACK: or by ending unanswered (hand_back).
Pages follower
The background process Leaf's hooks module runs for a session that holds a page. It lists the messages Leaf keeps back, and the module asks a line fork about each. While it runs it holds the session's fork lane, a lock whose recorded start tells Leaf since when the follower has been running (forks.take_lane), so the code says “lane” where this page says the follower is running.
Turn
A span of an agent's work that Leaf's records name. The main thread works in its turns, and each run of a working fork is a turn of its own.
Delivery
How Leaf hands messages to a turn: their text and thread, and for the message that owes an answer, the reply reference its leaf thread reply names. A delivery too large for a hook goes by pointer, and the agent's leaf delivery ack records it.
Pickup
The event in the page log that records a turn taking messages in. It names the session and the turn.
Start
The record that the agent is at work on one message or to-do, written by leaf task start or by a progress reply the agent posts. It names its turn and stops counting when the turn ends.
Clerk's line
A line Leaf queues for the main thread, which it reads at its next tool boundary or prompt. A line telling it of a to-do a fork opened wakes an idle session.
To-do
Work the agent owes on a thread until it ends it, opened with leaf thread reply --todo. A to-do is the obligation a significant task becomes.

The path of one thread message

When the user writes in a thread, none of whose waiting messages has reached the main thread or been handed to it, all of them arrived while the session's pages follower was running, and no running fork's turn took the thread, Leaf keeps the new message back from the main thread and asks a line fork for the working message. From then until the agent answers, the message reads On it. The line fork reads the message against the conversation, writes the working message, and sorts the message by who should do the work. Leaf posts the line as the agent's and sends the message where the sort says. If no line comes within forks.BOUND_S, 30 s, Leaf gives the message to the main thread anyway, and it does so at once when the line fork fails, when the newest message owes no reply, or when the pages follower stops.

flowchart TD
  accTitle: The path of one thread message
  accDescr: The user writes in a thread. Leaf keeps the message back from the main thread, and it reads On it. A line fork writes the working message and sorts the message. A small task goes to a working fork, which answers in the thread, and the main thread hears what it did. A significant task becomes a to-do, which is planned. Input the main thread needs now goes to the main thread.
  msg["The user writes in a thread"]
  kept["Leaf keeps it back; it reads On it"]
  line["A line fork writes the working message and sorts the message"]
  fork["A working fork"]
  answer["It answers in the thread"]
  told["The main thread hears what it did"]
  todo["A to-do"]
  main["The main thread"]
  msg --> kept --> line
  line -- small --> fork --> answer --> told
  line -- significant --> todo
  line -- "needed now" --> main
  class todo planned
One message, three ways. The dashed box is planned: today significant work goes to the main thread, as input it needs now does. A message Leaf stops keeping back for any other reason goes to the main thread too. A working fork takes a message only with LEAF_FORK_WORK=on.

Keeping a message back is the one state of a thread that Leaf reads partly from outside the page log. A hand-over goes in the session's forks file (forks.HANDED), and whether the pages follower runs comes from its lease. Every reader asks one function which messages Leaf keeps back (forks.kept_back), so none disagrees. The log can't hold this state: keeping back also ends when the follower stops, which nothing writes, so a live lease decides it whatever the log says. A lost hand-over record only has the follower hand the message over again at its next look. Which turn holds a message is different: every reader has to agree on it, it outlives the fork, and it decides whose writes stand, so a take is a pickup in the log, and an answer is a reply there that names its turn. Moving hand-overs into the log too would add an event to nearly every thread message in a Claude Code session, and change the protocol, while the lease still decided when keeping back ends.

How the line fork sorts a message

The message Where it goes Status
Small: a question or task a fork can finish beside the main thread, such as a lookup, research that writes only its own scratch files, or a dispatch to another session A working fork, which answers it built judged take; acted on only with the flag
Significant: work on the session's files, branch, page or pull requests, or research long enough to need progress of its own A to-do on the thread, which the main thread schedules planned judged hand_over today, but research that writes only its own scratch files is judged take
Needed now: the user's answer to a question the main thread asked, or steering its current work The main thread, at its next tool boundary, or woken if idle built judged hand_over

Today the line fork judges two ways (forks.ANSWER). take means a fork with the session's tools could do the work start to finish beside the main thread: it needs nothing the main thread is changing or about to change (its files and branch, the page's source, its pull requests), settles no question the main thread is weighing, and waits on nothing the session is running. hand_over means the work belongs with the main thread. Leaf records each judgment in the page's diagnostics (fork_outcome). Without the flag, Leaf posts the line and gives every message to the main thread.

The planned sort splits hand_over in two. Input the main thread needs now can't wait as a to-do, since it changes the work the to-do would wait behind. Research sits on the line between small and significant: a fork can do it, but when it runs for minutes it needs progress lines of its own, so the line fork decides by the message. A working fork that finds a task bigger than it looked can still make it a to-do (To-dos).

A working fork takes a message

When the line fork judges take, Leaf posts the working message, then in one transaction under the page's lock opens a turn for a working fork and records that turn's pickup of the thread's waiting messages, with a delivery made for it (forks._take). The message goes straight from kept back to held by the fork's turn, with no moment in which neither has it. Leaf's hooks module then spawns the working fork with that delivery, or resumes the fork that answered the thread before (Follow-ups and flurries), and tells Leaf which agent runs the turn (forks.started), which until then no agent runs (Who a leaf command acts as). The fork works with the session's tools and answers with its own leaf thread reply, using the reply reference its delivery names. built only with the flag

sequenceDiagram
  accTitle: A working fork takes a message and answers it
  accDescr: The user writes. Leaf keeps the message back while a line fork writes the working message and judges take. Leaf posts the line, opens a turn for a working fork, records its pickup and spawns it, and tells the main thread a fork has the thread. A further message reaches the fork at its next tool boundary. The fork replies in the thread. When its run ends, Leaf tells the main thread what it did.
  participant U as User
  participant L as Leaf
  participant W as Working fork
  participant M as Main
  U->>L: message
  Note over L: line fork: the line, take
  L->>U: working message
  L->>W: turn, pickup, spawn
  L-->>M: a fork has it
  U->>L: another message
  L->>W: at a tool boundary
  W->>L: leaf thread reply
  L->>U: the answer
  W->>L: run ends
  L-->>M: what it did
A take, in order. Leaf here is its Python and its hooks module together, and Main is the main thread. Dashed arrows are the clerk's lines, which wait for the main thread's next tool boundary or prompt.

Who holds a message

Leaf ties work to the turn it runs in: a pickup, a start or an agent's reply names a turn, and a pickup or a start stops counting when that turn ends. The main thread works in its own turns, and Leaf keeps a turn for each run of a working fork in the session's agent-turns record (state.AGENT_TURNS). A fork's turn opens at the take, before Claude Code starts or resumes the fork, and closes when the hooks module reports that the fork's run ended. It also lapses when nothing has renewed it within the working grace, 15 minutes; the fork's tool boundaries renew it. built only with the flag

A turn holds a thread from the moment it takes it until another turn does, whether it is the main thread's turn or a fork's. A pickup takes a thread, and so does an answer:

  • Of the turns that picked up any of a thread's waiting messages, the one with the newest pickup holds all of them.
  • A message no turn has picked up yet is held by the turn that last took its thread, by a pickup or an answer, if that turn is a running fork's. So a running fork keeps its thread's further messages even after it has answered, and the main thread gets them once its own answer took the thread over.
  • An answered message is held by the turn that answered it, which the reply names.

Every reader of received input asks one reading which turn holds each thread, with its waiting messages (forks.page_holding): delivery, the hooks, the page's activity reading, keeping a thread back for a line fork, the end of a fork's run and every writer's command. A fork's turn keeps the main thread off the messages it holds only while it runs, that is while it is open and renewed. Once it closes or lapses, those messages are the main thread's to answer, though the closed turn still holds what it picked up until another turn picks it up.

leaf page state names a holder on each thread and each answer the agent owes: the main thread, a fork by the name the main thread reaches it by, or the line fork while Leaf keeps the thread back for it. A thread's holder is the turn that holds it, with whether that fork's turn is running, closed or lapsed. An answer's holder is who owes it now: its thread's holder while that is the line fork or a running fork, and otherwise the main thread (holding.owed_by). Delivery, the Stop hook and the watch read the same reading. A main thread recovering what it owes there, as after a compaction, leaves a running fork's answer to the fork, and a message a line fork is judging to it. built

flowchart TD
  accTitle: Who has a thread's waiting messages
  accDescr: A new message is kept back for the line fork. A take gives it to a fork's turn, through the fork's pickup; a hand-over, a failed fork or the 30 s bound gives it to the main thread. If the fork's turn closes or lapses with a message still waiting, the main thread owes it. The fork's reply answers it, and so does the main thread's, whether the main thread owes it or answers over the running fork.
  kept["Kept back for the line fork"]
  fork["A running fork turn holds it"]
  main["The main thread owes it"]
  done["Answered"]
  kept -- take --> fork
  kept -- "hand_over,<br/>failure, 30 s" --> main
  fork -- "turn closes<br/>or lapses" --> main
  fork -- "fork<br/>replies" --> done
  fork -- "main thread<br/>answers over it" --> done
  main -- "main thread<br/>replies" --> done
A thread's waiting messages, by who has them. A further message joins whichever state its thread is in, and goes to a running fork even once it has answered; a message in a thread the main thread already owes goes straight to it. Leaf also gives kept-back messages to the main thread when the newest owes no reply or the pages follower stops.

One rule for a fork's writes

A fork's turn may act only on the messages it holds or that no turn holds. One function checks each of its writes as the write is made (holding.refuse_unheld): its pickups, which are its take, its tool boundaries and its acknowledgement of a delivery handed to it by pointer; its replies, including a message in the thread that answers nothing, which counts against the thread's latest messages; and its starts. An answered message is held by the turn that answered it, so a fork may repeat its own answer, but may not answer again a message the main thread answered, even with the reply reference the fork's delivery gave it.

When the main thread takes a thread over

While a fork's turn runs, the main thread isn't given the messages the fork holds. Its deliveries leave them out (delivery.deliverable), its Stop hook and watch neither keep its turn from ending over them nor remind it of them (activity.agent_holds), and its own pickups leave them out (holding.pickable). While the fork's turn runs, the thread's further messages go to the fork, before and after it answers.

The main thread is sovereign all the same: it can answer a message the fork holds. Its answer takes the thread over at once, as a pickup would once the fork's turn ended, and the fork's later reply, start or message in that thread is refused. The thread's next message then goes to the main thread while the fork still runs.

Once the fork's turn closes or lapses, a message it left waiting is the main thread's input: the main thread's next delivery hands it over with a reply reference of its own. The main thread's pickup of it is then the newest, so it takes the thread over, and the fork's later pickup, reply or start on that thread is refused.

Who a leaf command acts as

A working fork's shell commands run in the session's environment, so a leaf command it runs looks like one of the main thread's. Leaf's PreToolUse hook in hooks.json, which Claude Code runs before every agent's Bash call that runs leaf, tells them apart by the agent id in the hook's payload. For an agent the session has run a turn for, open or ended (state.agent_known), it rewrites the command to put --agent-id and the agent's id after its leading leaf (hooks.agent_command). The command still begins with leaf, so a permission rule allowing leaf commands still matches it. The mark decides the writes that name a turn: a reply, a start, a pickup and an acknowledgement. Any other leaf command, such as a page check, runs as the session's, marked or not. built only with the flag

flowchart TD
  accTitle: Who a leaf command acts as
  accDescr: A Bash call that runs leaf, from an agent that runs no open turn while a turn Leaf opened awaits its agent, is denied with the instruction to run it again. Otherwise, from an agent the session ran no turn for, it acts as the main thread. From an agent the session ran a turn for, a call where leaf isn't the first word, or runs twice, is denied. Otherwise the hook marks it with the agent's id; it acts in the agent's turn while that turn is open, on what the turn holds, and is refused once the turn has ended.
  bash["A Bash call runs leaf"]
  waits("Does it run no turn, while a new turn awaits its agent?")
  again["Denied: run it again"]
  known("Did the session run a turn for this agent?")
  main["It acts as the main thread"]
  leads("Is leaf its first word, and its only leaf?")
  denied["Denied: run each leaf command alone"]
  open("Marked with --agent-id. Is the agent's turn open?")
  inturn["Its replies and starts act in that turn, on what it holds"]
  refused["Every command it runs is refused: the turn has ended"]
  bash --> waits
  waits -- yes --> again
  waits -- no --> known
  known -- yes --> leads
  known -- no --> main
  leads -- no --> denied
  leads -- yes --> open
  open -- yes --> inturn
  open -- no --> refused
The three actors a leaf command can run as, where the session holds the page.
Actor The command What it may do
The main thread names no agent act on anything the session owes, in the main thread's turn
An agent with an open turn is marked, and the agent's turn is open reply, start and acknowledge in its own turn, on the messages it holds or no turn holds (One rule for a fork's writes)
An agent whose turn ended is marked, and the agent has no open turn nothing: every command it runs is refused (state.acting_agent_turn)

A subagent the session ran no turn for, such as one of the main thread's own workers, isn't marked and acts as the main thread. The mark follows the agent rather than its turn, so every command of a fork still running after its turn ended is refused rather than taken for the main thread's.

A command that acts as the main thread on a thread a running fork or a line fork holds, whether the main thread's own, a worker's, a fork's script's or a terminal's, goes through with the main thread's authority, as you chose on 14 Nov over refusing it. It says beside its result which fork holds the thread, and for an answer, which takes the thread, that it took it and how to stop the fork (holding.authorize). built

A new fork runs before Leaf knows its agent id. The turn Leaf opens at the take stays unnamed until the hooks module, once Claude Code has spawned the fork, tells Leaf which agent runs it, and the hook's payload names neither the turn nor the name the fork was spawned with. So while a turn of the session awaits its agent (state.unnamed_agent_turns), the hook denies the leaf command of any agent that runs no open turn, with the instruction to run it again, since unmarked, a new fork's command would act as the main thread. A worker of the main thread that runs a leaf command in that second or so is asked to run it again too. A resumed fork's agent id Leaf already knows, so it runs its new turn from the moment Leaf opens it. Where any step of starting a fork fails, the module ends its turn; a turn left unnamed otherwise, as by a reload of the module mid-start, stops counting once it lapses.

A marked agent's Bash call with the word leaf anywhere but as its first word is denied with the instruction to run each leaf command on its own, as in cd x && leaf …, if …; then leaf …, "$(true; leaf …)" or bash -c 'leaf …'. Unmarked, such a command would act as the main thread, and a mark spliced into it could land inside quoted text. The rule reads words rather than Bash's grammar, so it also denies a call that only names leaf, as a search, an inline python -c or a reply's text naming a leaf command does; the denial says to write such text or code to a file with the Write tool and pass the file, and to search with the Grep tool. A script that runs leaf is out of its reach.

The marking lives in hooks.json rather than Leaf's hooks module, because Claude Code runs none of a plugin's tool.call hooks for an agent that plugin spawned (measured at Claude Code 2.1.322), so the module can't see a fork's tool calls. The PostToolBatch hook in hooks.json names the fork too: at each of the fork's tool boundaries it renews the fork's turn and hands it the thread's further messages (hook_transport.carry_agent_turn), and it hands the fork nothing else of the main thread's.

When a fork's run ends

As a working fork's run ends, the hooks module tells Leaf (forks.ended), which closes the fork's turn and judges each thread by the messages that turn picked up itself and by what the turn wrote: its replies name it, so Leaf never credits the fork with an answer or a to-do another turn wrote. The judgment says who owes the thread next. A message the user took back counts for nothing. Leaf's clerk tells the main thread of every outcome except taken_over, since the turn that took the thread over already has it. built only with the flag

flowchart TD
  accTitle: How Leaf judges a thread when a fork's run ends
  accDescr: When a fork's run ends, Leaf closes its turn. If a message it picked up still waits and its closed turn still holds the thread, the outcome is hand_back. If another turn holds one of them or answered it, taken_over. Otherwise the fork answered every one: todo where a reply of its own opened a to-do on the thread that is still open, else answer.
  ends["The run ends; Leaf closes the fork's turn"]
  q1("Is a message it picked up still waiting, held by its closed turn?")
  q2("Does another turn hold one of them, or did another turn answer one?")
  q3("Did a reply of its own open a to-do on the thread that is still open?")
  hand["hand_back"]
  taken["taken_over"]
  todo["todo"]
  answer["answer"]
  ends --> q1
  q1 -- yes --> hand
  q1 -- no --> q2
  q2 -- yes --> taken
  q2 -- no --> q3
  q3 -- yes --> todo
  q3 -- no --> answer
Each outcome forks.ended gives a thread, and what the main thread hears of it.
Outcome When The main thread hears
answer The fork's own replies answered every message it picked up. That the fork answered, so it can read the answer and correct it where what it knows shows it wrong; and that the thread owes nothing, where nothing waits in it and no to-do on it is open. A message the user wrote since, which the fork never picked up, Leaf passes on as any new message.
todo As for answer, and a reply of the fork's own opened a to-do on the thread that is still open, usually its answer. That the fork answered and opened the to-do, named with its text, which the main thread now owes and schedules. If the session is idle, Leaf wakes it.
hand_back A message it picked up still waits, and the closed turn still holds the thread: the fork gave it back with HAND BACK: and a reason, failed, or ended without answering. That the fork gave the thread back, and why, and the user's waiting messages themselves, as new input at its next tool boundary or prompt (delivery.deliverable). If the session is idle, Leaf wakes it.
taken_over Another turn picked the thread up since the fork did, or answered one of the messages the fork picked up, as the main thread can while the fork runs. Nothing: the turn that took the thread over has it. The page's diagnostics name that turn.

How a fork tells the main thread

Claude Code tells the main thread nothing of a fork a hooks module spawns: no row in its transcript, and no notification or wake when its first run ends. So Leaf tells it, in lines its clerk queues: one as a fork takes a thread, naming the fork so the main thread can SendMessage it, and one as the fork's run ends. A line waits for the main thread's next tool boundary or prompt; a take's line still waiting as the fork's run ends leaves the queue, since the end's line says where the thread stands. Where the run leaves the main thread owing something, the answer to a thread the fork gave back or a to-do the fork opened, Leaf also wakes an idle session for it. The end of a resumed run is different: Claude Code notifies the main thread of it, which starts a turn on an idle session, and that turn reads the queued lines. built only with the flag

The user chose the clerk's line on 2026-11-11, over the fork's own SendMessage to main. Leaf writes the line from the log, so it also covers a fork that failed or was stopped before it could write, and Leaf's own wake comes only when the main thread owes something, after a first run and a resumed one alike, though Claude Code still wakes it at every resumed run's end. A fork's SendMessage would name the fork as its sender and could say what the fork did beyond the outcomes above, but it would wake the main thread for every answer, and add a second report to a resumed run's notification.

In a probe, a fork's SendMessage arrived as an agent message and started a turn on an idle main session 60 ms later. Claude Code offers no third way: a hooks module can start a fork only with $.agent.spawn, whose first run never notifies the main thread, since the module's call to the Agent tool is refused (2.1.322). A resumed run's notification wakes an idle main thread whichever way tells it; in two live runs that woken turn took 23 s and 28 s over 4 and 5 model requests, and checked the forks' answers.

Follow-ups and flurries

Leaf gives a thread's work to one working fork where it can, so a flurry of messages in one thread usually makes one worker, and a thread never has two. While a fork's turn runs, the thread's further messages go to it, before and after it answers, and once its run has ended, a message the line fork judges take goes to the same fork, resumed. Otherwise a further message goes wherever its thread already is. built only with the flag

flowchart TD
  accTitle: Where a further message in a thread goes
  accDescr: If a running fork turn holds the thread, by its pickup or its answer, that fork takes the message at its next tool boundary. Otherwise, if the main thread owes the thread, or its answer took the thread from a fork that still runs, it takes the message as any input. Otherwise the message is kept back for the line fork, which sorts it. On a take, if a fork gave the thread's last answer and its turn has closed, that fork is resumed in a new turn; otherwise a new working fork takes it.
  more["A further message in a thread"]
  q1("Does a running fork turn hold the thread, by its pickup or its answer?")
  a1["That fork, at its next tool boundary"]
  q2("Does the main thread owe the thread, or did its answer take it from a fork that still runs?")
  a2["The main thread, as any input"]
  kept["Kept back for the line fork, which sorts it"]
  q3("On a take: did a fork whose turn has closed answer it last?")
  a3["That fork, resumed in a new turn"]
  a4["A new working fork"]
  more --> q1
  q1 -- yes --> a1
  q1 -- no --> q2
  q2 -- no --> kept --> q3
  q2 -- yes --> a2
  q3 -- yes --> a3
  q3 -- no --> a4
A further message follows its thread. A message the line fork judges hand_over goes to the main thread, as any new message does.
  • While a fork's turn runs, the user's further messages in its thread reach it at its next tool boundary, inline, or by pointer when they are too large for the hook, which the fork's own leaf delivery ack records into its turn. That holds after the fork has answered too: its turn holds the thread until another turn takes it, and Leaf keeps nothing back for a second fork in a thread a running fork took, by a pickup or an answer (holding.Holding.took). A message that arrives as the fork's run ends, after its last tool boundary, is kept back once the turn has closed, and goes to the same fork, resumed: no receipt passes a message no turn has taken in, so it is still waiting for its first delivery when the turn closes.
  • A message the line fork judges take, in a thread whose last answer names a fork's turn that has since closed in the session's present generation, goes to that fork again (forks._resumable). Leaf opens the new turn and records its pickup, as a first take does, and the hooks module resumes the fork by its agent id, then tells Leaf it runs the turn. Leaf's message resuming it (forks.resume_prompt) carries the thread's delivery and tells the fork to read leaf page state before it answers, since its transcript lacks what the rest of the session did on the page meanwhile. The fork can SendMessage the main thread to ask about it.
  • A thread the main thread answered last gets a new fork, once the fork it took the thread from has ended, and so does one whose fork ran in an earlier generation. A thread whose fork Claude Code can't resume goes to the main thread instead: the module ends the turn Leaf opened, which gives the thread back. A message the main thread writes in the thread unasked, such as a correction of the fork's answer, answers nothing, so the thread stays the fork's.
  • Messages in different threads start a fork each, side by side.
  • planned fallback If a fork per thread proves wasteful, one standing fork per session would take every small message in turn: cheaper, but slower when messages in several threads arrive together.

The main thread as coordinator

The main thread stays in charge of the session's work. It hears by name as each fork takes a thread, and can steer the fork, or ask it to stop, with SendMessage, or answer the thread itself, which takes the thread from the fork (When the main thread takes a thread over). It takes its to-dos in the order it judges, and runs work bigger than a message in subagents, as the shipped skill's Stay responsive already asks. A fork is to change neither the session's files nor its plan: the line fork gives a fork only work that needs nothing the main thread is changing, and the working fork's task tells it to change nothing the main thread is changing or about to change. Nothing else enforces it.

flowchart TD
  accTitle: The main thread coordinates forks, to-dos and subagents
  accDescr: Working forks tell the main thread what each did, and the main thread can steer them or ask them to stop. A fork can turn work that proved bigger than it looked into a to-do. The main thread schedules to-dos and runs work bigger than a message in subagents.
  forks["Working forks, one per thread"]
  main["The main thread"]
  todos["To-dos on threads"]
  subagents["Subagents"]
  forks -- "what each did" --> main
  main -- "steer, or<br/>ask to stop" --> forks
  forks -- "bigger than<br/>it looked" --> todos
  todos -- scheduled --> main
  main -- "work bigger<br/>than a message" --> subagents

To-dos

built only with the flag A working fork that finds the work bigger than a message, such as a change to the session's files, branch, page or pull requests, or research long enough to need progress of its own, does what it can without those changes, answers with what it found, and opens a to-do on the thread with leaf thread reply --todo, as the main thread does. Its task (forks.work_prompt) offers that beside HAND BACK:, which it keeps for a message the main thread has to act on now: an answer to a question the main thread asked, steering its current work, or a question it is weighing. The to-do is the session's and stays open after the fork's turn ends, while the fork's start on it stops counting with the turn. The main thread hears that the fork opened it, and an idle session is woken for it (the todo outcome above).

planned A message the line fork sorts as significant becomes a to-do on its thread that the agent owes until it ends it, so the main thread takes it in its own order instead of being interrupted by it. Such a message has no working fork, and who opens its to-do is still to be designed.

What's built and what's planned

Part Status
Keeping a message back, and the working message built in every Claude Code session
The line fork's judgment, take or hand_over built recorded, acted on only with the flag
A working fork in a turn of its own: the take, marking its commands, further messages at its tool boundaries, and its end built acts only with the flag
A to-do a working fork opens built with the flag
Telling the main thread in the clerk's lines built with the flag
The same fork for a thread's follow-ups built with the flag
Sorting three ways: small, significant, needed now planned
A to-do for significant work planned
One standing fork per session planned fallback if a fork per thread proves wasteful
Measuring forks before they become the default: decision, outcome and coherence evals planned

Where each rule is written

  • Session lifetime, “Turns and generations”: the main thread's turns and generations, the agent turns beside them, and who holds a message.
  • src/leaf/holding.py: which turn of a session holds each thread message, the main thread's or an agent's, and what a turn may do about one another turn holds; reading, refuse_unheld, authorize and pickable hold the rules.
  • src/leaf/forks.py: its header holds the states a thread moves through and why the line fork runs as it does; _take and ended hold the rules this page names for a working fork's take and end.
  • src/leaf/clerk.py: queue and waking, the clerk's lines and which of them wake an idle session; and src/leaf/session.py: watch_between_turns, what the watch wakes the session for.
  • src/leaf/state.py: AGENT_TURNS, agent_known, unnamed_agent_turns and acting_agent_turn, the record of agent turns and the actor a command runs as.
  • src/leaf/hooks.py: agent_command and leaf_words, the marking, the denial while a turn awaits its agent and the refusal of a compound command; and src/leaf/hook_transport.py: carry_agent_turn, a fork's tool boundary.
  • hooks/claude-code.ts: lineFork, workFork and the turn.complete hook, which make the forks and report a working fork's end.
  • notes/fork-replies.md: what Claude Code lets a hooks module fork, measured; how well the line fork judges; forks on real pages; and the build steps.