We tested a file-based handoff between two Claude Code sessions using 5 fictional support requests. One session wrote a file with a job id, a named owner and the expected result. The other did the work and wrote a receipt. Then a small Python script checked the result. The normal handoff passed 5 of 5 checks, a repeated job was refused, and a missing file stopped the job.

We run our own agents this way every day. Work orders go into a folder as files, and every finished job leaves a receipt. As of 23 October our dispatch folder holds 369 receipt files. The demo below is that same habit, cut down to something you can run in a few minutes.

How we set up the handoff

Two sessions, each with one job:

"Done" is decided before anything runs: every request id that went in has exactly one row coming out, every team is on the allowed list, and the receipt's job id and output ids match the files. A Python script checks all of it. The receiver writes a completion claim, and the script decides whether to accept it.

The handoff file

This is what the sender wrote for the first run, trimmed to 2 of its 5 items:

JSON
{
  "job_id": "JOB-001",
  "from": "intake",
  "to": "assigner",
  "task": "Assign each request to one team: orders, payments, bakery or store-hours.",
  "items": [
    {"id": "R1", "customer": "Aina", "summary": "Order KC-1042 came without the oat milk they paid for.", "order": "KC-1042"},
    {"id": "R3", "customer": "Priya", "summary": "Card machine charged twice for one latte; receipt attached.", "order": null}
  ]
}

The job id lets the script track the job and spot a repeat. The named sender and receiver mean nobody guesses who owns it. The task fits in one line. The id on every item lets code match each output back to its input.

Our real work orders have the same bones with a few more lines: who the job is for, which files it may write, and what proof counts as accepted. The "which files it may write" line matters most once you have more than two agents. It tells each agent which files it owns. To stop two agents writing the same file, you also need a lock or owner check in code.

Run it and collect the receipt

Each session is one Claude Code call in headless mode, started in the demo folder with only the Read and Write tools switched on. The prompts name the files to use. This setup doesn't sandbox the session to that folder.

BASH
claude -p "<sender or receiver instructions>" --model haiku \
  --tools "Read,Write" --allowedTools Read Write \
  --permission-mode acceptEdits --setting-sources "" \
  --strict-mcp-config --output-format json

The receiver wrote one row per request:

TEXT
id,team,next_step
R1,orders,Check order KC-1042 contents and confirm whether oat milk was packed; offer to add it or refund the item
R2,orders,Check order KC-1047 and whether a 3pm pickup tomorrow fits availability; draft confirmation of the time change
R3,payments,Review the duplicate card charge for the latte against the attached receipt and start a refund for the extra charge
R4,bakery,Check gluten-free cake options and lead time for next Saturday; draft a reply to Wei Jie with options
R5,store-hours,Verify posted opening hours against the actual opening time today and correct the website if needed; draft an apology to Daniel

And a receipt:

JSON
{"job_id": "JOB-001", "input_ids": ["R1","R2","R3","R4","R5"], "output_ids": ["R1","R2","R3","R4","R5"], "status": "done"}

Then the script checked it. All 5 checks passed: the handoff named the right job, all 5 ids went in, each had exactly one row out, every team was allowed, and the receipt's job id and output ids matched. This checker doesn't validate the receipt's input ids or status. Only then did the script add JOB-001 to a plain text list of finished jobs.

Nothing was sent to a customer. The next_step column is a draft for a person to act on.

What happens when something is missing

We ran two failures on purpose.

First, the same job twice. We asked for JOB-001 again. The script saw it in the finished list and refused before any model was called. Result: refused: job id already done, not run twice

Second, no handoff file. We started JOB-002 with the sender skipped, so there was no handoff.json. The script stopped the job. We still called the receiver to see what it would do, and its instructions say to stop if the file is missing. It replied "STOP no handoff.json." and wrote 0 files.

The two stops work differently. The duplicate check is in code, so it doesn't depend on what a model decides. The receiver's stop came from its instructions, and it held this time. Put the rules you can't afford to break in code.

The demo has limits. The script clears its finished list each time it starts. It doesn't protect against two runs at once, or a crash after the work is done but before the job is recorded. Within this run, the second JOB-001 was refused because the first was already on the list.

Stop loops before they start

Agents that message each other freely can bounce a task back and forth, or both decide to fix the same thing. These are the three rules we use in our own setup:

Anthropic's own guide to building effective agents makes the case for starting with the simplest setup that works and adding agents only when you need them. Our AI Work OS guide shows the full setup we run, with owners, work orders and receipts.

Paste this into your agent

TEXT
Set up a two-step handoff for [your task] using two separate Claude Code
headless runs. Step 1 reads [your input file] and writes handoff.json
with job_id, from, to, a one-line task and an id on every item. Step 2
reads handoff.json and writes [your output file] with one row per item
id, plus receipt.json with job_id, input_ids, output_ids and status.
Then write a Python script that runs both steps. If handoff.json is
missing after step 1, the script stops the job before step 2. Define the
allowed values for [your category column] and check every row uses one.
Check every input id has exactly one output row, and that the receipt's
job id and output ids match the actual files. Keep a done-jobs.txt list,
refuse any job id already on it, and add a job only after every check
passes. Separately, test step 2 in an empty folder and confirm it writes
nothing and replies STOP. Run everything and show me the results.