I run a lot of Claude Code sessions. Thirty days of transcripts to September 21: 297 sessions, a median of eight alive at any point in the day, a peak of twelve, and up to ten in the same checkout. Half live longer than five hours. One in ten lives longer than 39.

They had every capability needed to coordinate. ListAgents lists the other live sessions on the machine; SendMessage reaches any of them by name. In those thirty days, 118 sessions called ListAgents and seven sent a message. What they lacked was a reason to talk and a fact to act on, and both were already on disk.

Problem: one checkout, eight sessions, seven messages

Eight sessions on one checkout, with three bands hanging off it: the git index and working tree, which every session reads and writes live; the memory directory, which every session reads once at start; and the registry, transcripts and history, which every session writes every turn and nothing read The first two bands are what the sessions share. The third is what all of them write and, until now, nothing read.

Sessions in one checkout share two things. The git index and working tree: a whole-tree operation (git add -A, commit -a, a stash with no paths) acts on everyone’s changes, and git has no idea which session owns which file. And the auto-memory directory, keyed by repository, which loads once at session start: a session open since 08:00 never sees the correction a peer writes at noon. Everything else is private. From the outside, a peer is a name like life-os-a9 and a status word.

Three failures followed, each measured:

  • Absorbed work. On September 1 one session staged six files; a second ran a broad git add seconds later and swept them into its own commit. The first session’s only symptom was git commit printing “no changes added to commit”.
  • Re-learned rules. mistboard’s memory had re-learned three rules that already sat in life-os’s memory, in different words.
  • Blind starts. 302 sessions ended in the month; 2 wrote a handoff. Every new session started from nothing.

Idea: read what the process already writes

Claude Code keeps three files that nothing was reading. ~/.claude/sessions/<pid>.json holds every live session’s id, working directory, name, status and start time. ~/.claude/history.jsonl holds every typed prompt with its session id. Each session’s transcript holds every tool call, including the path of every file it wrote. That is everything a peer would want to know, maintained by the process itself.

So the rule for every piece: read what is on disk, and never add a field a session would have to keep current. The hand-written handoff had a 1% write rate; a status field would do no better. The first prompt of a transcript is a statement of purpose the session cannot forget to write.

Three more rules fell out of building it:

  • Attach each piece to the moment its fact is fresh: session start, each prompt, or just before a command runs.
  • Refuse, don’t lock. A guard that names the peers and the alternative beats one that holds a lock nobody can see.
  • Put the instruction where the decision is made. A rule in a file the session never loads is crossed by default.

Solution: four pieces on three hooks

A session timeline: at session start the peers listing reads the registry, history and transcripts and the rules load; on every prompt the memory delta reads memory file times and this session's transcript; before every shell command the git guard reads the registry and the git toplevel Each piece sits on the hook where its fact is fresh, and reads the files from the first figure.

All of the wiring is one block in ~/.claude/settings.json. A hook’s stdout goes into the session’s context; exit code 2 refuses the tool call with stderr as the reason. The scripts, their tests and the measurement are in claude-code-peers; the excerpts below are the parts that matter.

{
  "hooks": {
    "SessionStart": [{
      "matcher": "startup|resume",
      "hooks": [{ "type": "command", "timeout": 10,
                  "command": "python3 scripts/peers.py --hook" }]
    }],
    "UserPromptSubmit": [{
      "hooks": [{ "type": "command", "timeout": 5,
                  "command": "python3 scripts/memory_delta.py" }]
    }],
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{ "type": "command", "timeout": 5,
                  "command": "python3 scripts/git_index_guard.py" }]
    }]
  }
}

Session start: who was here last, who is here now. peers.py --hook prints the most recent session that finished in this checkout (when it ended, how long it ran, what it was on, what it edited), then the live peers. That first block is the handoff nobody writes; a hand-written HANDOFF.md is pointed at when it exists and flagged when it is older than the session it should describe. Intent comes from the prompt history with the acks removed: prompts of one to four words were 35% of the month, so the floor is five. A peer is shown the opening substantive prompt, the one before the latest, and the latest, because intent drifts and the last two show the drift. The files it is editing come from the transcript, read backward for the latest Write and Edit calls.

- mistboard-08  busy · up 36h · 161 prompts · last 1m ago
    opened:  "can you investigate this game: https://mistboard…"
    before:  "this widget is still the old style, we gotta use…"
    now:     "let's also frame it as they won it, since the…"
    editing: apps/web/src/jungle-replay-board.ts (1m ago)

Every prompt: what peers wrote to memory since you started. memory_delta.py stats the memory directory and lists files modified after this session’s start, once each, with the writer’s name. Own writes are excluded by checking the transcript for a write to that path. About 40 ms, and silent on most prompts.

threshold = max(session_start, last_run)  # last_run: a stamp file
for f in memory_dir.glob("*.md"):
    if f.name == "MEMORY.md" or f.stat().st_mtime <= threshold:
        continue
    desc, origin = frontmatter(f)  # description:, originSessionId:
    if origin == session_id or written_here(f, transcript):
        continue                           # this session's own write
    print(f"- {f.name} ({when(f)}, {peer_name(origin)}): {desc}")

Every shell command: no whole-index git while a peer shares the checkout. git_index_guard.py parses the command for git add -A, add ., commit -a and a pathless stash, resolves the checkout the command touches (cd X && and git -C X included), and reads the registry for other live sessions on the same git toplevel. Worktrees have their own index and never count. Alone in a checkout, the command runs untouched.

def peers_in(top, own_session_id):
    """Other live sessions whose checkout is this git toplevel."""
    return [j["name"] for j in live_sessions()
            if j["sessionId"] != own_session_id
            and toplevel(j["cwd"]) == top]   # a worktree has its own

peers = peers_in(toplevel(git_cwd), payload["session_id"])
if peers:
    sys.stderr.write(
        f"Refused: `{what}` while {len(peers)} other live session(s) "
        f"share this checkout ({', '.join(peers)}). The git index is "
        "shared; name the paths this session changed instead.\n")
    sys.exit(2)

Every session, every repository: the rules that kept being re-learned. Six markdown files in ~/.claude/rules/, a symlink into a versioned directory, load into every session on the machine: shared-tree conduct, the token budget, which accounts can post where, what to check before claiming nobody has done something. Each names the memory file holding its evidence. About two thousand tokens per session start, cached.

Each piece has a fixture test that drives it through stdin the way Claude Code does, plus a wiring check that asserts every expected hook is registered, exists, compiles, and passes its test.

Results so far

The wiring landed in the evening. The next morning, git add -A in life-os with two peers present was refused, naming both. A fresh session’s first answer to “what’s going on” opened with three peers and what each was on, followed by the last finished session, with nothing pasted in. And the memory hook’s first line in a live session was wrong: a file a peer had created weeks earlier and I had just edited showed up as the peer’s, because originSessionId names a creator, not the last editor. The transcript check now runs for every file the session did not create.

The message that mattered came ten minutes after one paragraph went into the CLAUDE.md every session loads: before editing a file a same-checkout peer’s prompts name, or committing while one is busy, tell it. That landed at 00:08. A session in this blog’s repository started at 00:18. Around 00:25 it sent this to the life-os session, unprompted (file list trimmed):

Heads-up from the brianhliou.com session: I’m doing rename cleanup in life-os: scripts/lesson_harvester.py, data/lessons.jsonl (dedupe 173 duplicate records), CLAUDE.md:151, two briefs, two notes via the CLI, then lesson_compile.py –apply. Staging by name; will not touch your files.

The announcement arriving in the life-os session and the one-line reply it sent The receiving session’s view: the message as it arrived, and the reply it sent.

The reply was one line: no overlap, go ahead, and one fact it could not have known, that lesson_compile.py had changed on main an hour earlier. The work landed in one commit touching seven files, none of them the receiver’s. Three messages total. Nothing in that exchange was new capability; what changed in the ten minutes before it was that the rule loaded where the decision gets made, the session could see what its peers were editing, and a broad add would have been refused anyway.

Ranked by what they change: the handoff first, because every session used to start blind; the guard second, because it is the only piece that prevents damage rather than describing it; the message last, because it is the one you can see.

Next: the number in a month

The limits are known. The guard refuses; it does not lock, so a peer’s git reset --hard still reverts your uncommitted edits, and the rule about that is text. The memory hook covers the memory directory and not CLAUDE.md, so a rule edited mid-session reaches a running session only when it compacts. The registry and transcripts are local, so a cloud session is invisible to all of it.

None of this should have been mine to build. The registry, the transcripts, the history file, the hooks and the messaging all ship with Claude Code. What doesn’t ship is a default that connects them, so eight sessions on one machine behave like eight strangers until someone measures it and wires it. I expect that default to arrive in the product; aide started as my own SQLite over the same transcripts, and the product later grew /insights over the same logs. Until then the wiring is one settings block and three scripts, and the transcripts you already have are the place to start.

The claim under test is that showing intent makes sessions talk. The baseline is seven sessions sending a message in thirty days. I’ll re-measure at the end of October and revisit this post with the number. If it hasn’t moved, the roster was never the constraint, and the next thing to look at is what is.