Use case · Agent handoff
Python coordinator · single host · reserves nothingOne session drafts plan.md, another carries it on. Today the first session either keeps writing, or releases the file, and to the next writer a release looks exactly like a crash. A targeted grant handoff names who comes next: the coordinator records the handoff, refuses the giver's later writes to that file as handed_off, and the successor accepts or declines. No timer ends it, and nobody else is locked out.
The usual way to pass work between agents is a note: a summary in a room, a line in a shared scratch file, a name in a prompt. The receiving agent reads it and starts. Nothing stops the first agent from editing the same file again, and when it does, the second write erases the first with no error. A swarm of 1,200 agents reinvented exactly this: an owner named by message, a takeover by countdown, and a store that could not tell a crash from a sanctioned handover.
plan.md, or just stops. To the coordinator both are the same event. Session B picks the file up, and a bystander can too. When A comes back from a slow tool call and edits again, its write lands on top of B's. The run shows green.plan.md to B by name. The coordinator stores who gave, who receives and the version at transfer, and from that moment A's own writes to the file are refused with handed_off. B's next edit completes the handoff and lifts the fence. If a third session writes first, the record says overtaken, and by whom.The record is written by the coordinator, not narrated by either agent, which is the ledger the infrastructure can verify. Releases, reclaims and handoffs are three different events in the state log, under three different triggers.
| Property | What the coordinator does | What you see |
|---|---|---|
| It names the successor | Stores one transfer record per path: the giver, the successor, the version at transfer, the hold the giver gave up, and a status | plan.md: bd35b34c → 6e271ee6 at version 2 (pending) in agent-coherence-status, and a handoff key on every read of the path |
| The giver stops writing | Refuses the giver's session on every write route while the record is live, from every subagent and fresh attempt of that session | A Claude Code Edit denied before it lands, GiverFenced from a CoherentVolume, handed_off from the MCP server or HTTP |
| The successor answers | The successor's edit, compare-and-swap win or explicit accept marks the record completed; a decline ends it |
agent-coherence-accept / agent-coherence-decline, vol.accept() / vol.decline(), swg_accept / swg_decline |
| No timer ends it | The fence lifts when a commit moves the version, when the successor declines, or when the giver withdraws. Silence never counts as acceptance | The deny text names those three ways out, and nothing else |
| It reserves nothing | Any other session keeps reading, acquiring and writing the path. A bystander's acquire or commit marks the record overtaken and names the counterparty |
Nobody waits on a handoff, and the record tells you when it did not go to the session you named |
plan.md to session B.Two Claude Code sessions in one workspace, with the plugin's hooks and the Python coordinator. The output below is the coordinator's, not a mock.
# 1. In session B, find B's session-level agent id python3 -c 'import sys, uuid; print(uuid.uuid5(uuid.NAMESPACE_URL, "ccs-agent:claude-session-" + sys.argv[1]))' "$CLAUDE_CODE_SESSION_ID" 6e271ee6-5a71-5508-bedf-ffd46d8898ce # 2. In session A, right after its edit of plan.md agent-coherence-transfer --successor 6e271ee6-5a71-5508-bedf-ffd46d8898ce plan.md agent-coherence-transfer: acting as session agent bd35b34c-f785-51c6-a184-5922aee7cbea (session from CLAUDE_CODE_SESSION_ID) agent-coherence-transfer: transferred plan.md to 6e271ee6-5a71-5508-bedf-ffd46d8898ce at version 2 (gave up MODIFIED; status pending)
From here A is fenced. Its next Edit of plan.md is denied before it touches the file, and the deny says why and how it ends:
# 3. Session A tries to edit plan.md again
Edit denied: you handed plan.md to agent 6e271ee6 at v2, so this session can no longer write it. The handoff ends when agent 6e271ee6 writes plan.md, when agent 6e271ee6 declines it, or when you withdraw it on your user's or host's instruction. Stop and report to your user that plan.md was handed off. This denial is structural; retrying the same operation will produce the same denial.
B's next read of plan.md carries Handoff: agent bd35b34c handed plan.md to this session at v2. B's edit completes the handoff, its commit becomes version 3, and A's fence lifts. To undo instead, B runs agent-coherence-decline plan.md, or A runs agent-coherence-withdraw plan.md on its user's instruction.
Measured with Haiku 4.5, Sonnet 5.5 and Opus 5.5 on Claude Code 2.1.291, in warn and strict mode: every giver session that met the deny stopped after that one refusal, without retrying, and told its user the file had been handed off. The same deny answers a giver's shell write in the forms the Bash hook recognizes: a redirection or tee, an in-place sed -i, a cp or mv onto the path.
Every surface has matching accept, decline and withdraw verbs, and names the successor by its session-level agent id.
| Surface | Hand a path on | The giver's next write gets |
|---|---|---|
| Claude Code session | agent-coherence-transfer --successor ID path |
A deny on Edit, Write and recognized shell writes |
| CoherentVolume | vol.transfer(paths, successor=...) |
GiverFenced, never retried, which no reacquire clears |
| stale-write-guard-fs MCP server | swg_transfer |
handed_off, with recover: stop_and_report |
| HTTP | POST /handoff/transfer |
{"ok": false, "reason": "handed_off"} |
The routes are served by the Python coordinator only. The Claude Code plugin's built-in Node coordinator answers them 404, and the plugin does not ship the commands, so a plugin-only workspace runs the Python coordinator to use handoff. Install, upgrade notes and the full route contract are in the v0.15.0 release post.
Agent messaging rooms, Claude Code's own cross-session messaging, Slack, a shared scratch file: all of these carry the conversation half of a handoff, the context, the decisions, the follow-up questions. None of them can say who may write the file next, and most of them say so themselves. This is the other half. Post the context wherever your agents read it, then transfer the file, so the receiving agent gets a message it can act on and a grant it can refuse.
Stated plainly, before you ask.
cd in an earlier call the shell-write refusal can miss a relative path.CoherentVolume or MCP session's handoff stays live after the process exits, and nothing can withdraw it then; it ends when the version moves or the successor declines.handoff_ended until the version moves or the ended record is evicted, 24 hours later by default.Common questions about handing a file between agents.
In the giving session, right after its edit, run agent-coherence-transfer --successor <the other session's agent id> plan.md. The coordinator records the handoff and denies the giver's next edit of that file. The successor's next edit or agent-coherence-accept completes it, agent-coherence-decline refuses it, and agent-coherence-withdraw takes it back. The commands ship with the Python package and need the Python coordinator.
Its write is refused with the typed reason handed_off, and nothing lands. In Claude Code the Edit, Write and recognized shell writes are denied before they reach the file; a CoherentVolume raises GiverFenced; the MCP server answers handed_off with recover: stop_and_report. No retry or reacquire clears the fence. It lifts when a write moves the file's version, when the successor declines, or when the giver withdraws, never on a timer.
No. A handoff fences the giver and reserves nothing. Any other session can still read, acquire or write the file, and if one does, the record is marked overtaken and names who took it. The successor is granted nothing in advance: it reads or acquires the file by the ordinary rules, and its edit, compare-and-swap win or explicit accept completes the handoff.
Neither. Handoff is single host, for sessions that go through one Python coordinator. The Claude Code plugin's built-in Node coordinator answers the handoff routes with 404 and does not ship the commands, so a plugin-only workspace has to run the Python coordinator to use it. Cross-host coordination has not shipped.
15-minute call. Tell me how the work moves between them now, and I'll tell you whether a recorded handoff covers your shape, or whether you are in the cross-host case that has not shipped.