Sessions that share a machine take leases.
Two Claude Code sessions on one machine share its stack, its GPU, its working tree and each other's processes. Continuum's sessions take leases on what they hold, keep their own state where no other session writes, and hand off through a journal whose catch-up reached each new session as a 2,000-character preview for nine days, until the hook was given a budget.
House share
Two sessions on one machine share its stack, its GPU, its tree and its processes.
On 28 September Tristan left one engine session re-ingesting Continuum's documentation into its own memory, with Lightning extracting facts on the GPU, and opened a second to build the Dashboard's pipeline pages.
Tristan
"I stopped it since you need to recompile, you are free to start it back up and shutdown as needed. While you are re-ingesting, I will work in a new session to build out what we need for the Dashboard/UI."
Both sessions reach the same stack on the same pinned ports, 5150 to 5156, the same
card and whatever is loaded on it, the same Docker, the same working tree and each
other's processes. Either can break the other without meaning to: an AppHost restarted
under a benchmark, a model load that pushes another model's layers onto the CPU, a
build over DLLs a running host holds. Earlier that morning, while the stack ran, its window's
host process held the Terminal's binaries, and a solution build failed on
MSB3021.
Before leases, keeping out of each other's way was glue. A session announced a build window before building, named its processes by id so the other wouldn't stop them, and asked before touching the other's stack, all by message.
Terms and conditions
A lease is a Redis key whose TTL is its expiry.
Each lease records an owner, a purpose and an expiry, under
continuum:lease:<resource>. The expiry is the key's TTL, so a holder
that dies lets go when it runs out, and nothing has to notice the death. Taking and
releasing are Lua scripts, so the owner check and the write are one step, and of two
sessions taking at once only one wins.
-- Take: refuse while another owner holds it; set (or renew) it with the expiry as TTL.
local held = redis.call('GET', KEYS[1])
if held and cjson.decode(held).Owner ~= ARGV[1] then return held end
redis.call('SET', KEYS[1], ARGV[2], 'PX', ARGV[3])
return false
The owner is the Claude Code session. Claude Code sets
CLAUDE_CODE_SESSION_ID for every hook it runs and every command its shell
tools run, so continuum lease take typed in a session and a check made
on that session's behalf agree on who holds what. CONTINUUM_LEASE_OWNER
names any other owner, and without either it is the machine and the process.
A model load, unload or restart refuses while another owner holds gpu,
and names the holder. The check fails open: "A cold machine loads before Redis is up;
an unreadable lease never stops it." The trade is a card left unguarded while Redis is
down, for a machine that can always load its first model. The re-ingest took
the gpu and stack leases before starting the stack, so the
Dashboard session could see why the stack was up and why its builds failed, and
released both when it stopped.
Leases replaced the messages between sessions, which worked only while the holder's session was there to read them.
A room of one's own
State that no other session writes needs no lease.
- Cache files keyed by
session_id. Every hook keeps its state in.claude/.cache/under the session's id: the projects whose README a session has already seen, the messages saved before a compaction, the ledger of what instructions loaded. - One journal entry per feature per day. Two sessions working on two features append to two files.
- Tests on isolated stores. The integration suite runs on stores of its own, empty each run, so a test can't corrupt the dev data another session is using. The ports are pinned, so a test run still needs the stack stopped, and that is what the
stacklease is named for. - Builds to a scratch folder. When another session's processes run from
bin, a build goes elsewhere with-o. The 28 September lab ran from a Terminal built that way while the stack held the real one.
Return to sender
Hook output past 10,000 characters reaches the model as a preview.
The journal is the handoff between sessions. A session writes its entry as it goes, with the questions, the decisions and the next steps, and a hook blocks the end of a turn once three of the person's messages are unrecorded, and saves them to disk before a compaction. The next session receives the catch-up as it starts.
Until 28 September the catch-up hook printed the last three entries whole, and the entries kept growing once they recorded every question and answer. Claude Code caps a hook's context at 10,000 characters. Past that it saves the text to a file, hands the model the path and the first 2,000 characters, and, in its own documentation, "doesn't ask Claude to read the file." From 19 to 28 September that happened on 93 session starts, to catch-ups of 14 to 135 KB.
On the morning of 28 September the 2,000 characters held the instruction check, the catch-up's heading, the line
"This is your continuity context - do not re-read these files", and the start of the
first entry's title, cut off inside its date. CLAUDE.md said the same: "That is
your catch-up - do not re-read those files." It was written for a hook that worked,
and it kept sessions from reading the entries the hook had failed to deliver. Across
those nine days a session opened the saved copy three times.
The fix landed the same day. The hook now injects each of the last three entries'
header and its open questions and next steps, with a line index of its sections, cut to
an 8,000-byte budget, starting with the oldest entry's next steps. CLAUDE.md now says to read
the rest when the work touches it. Run against that day's journal, the old hook would
have injected 43,485 characters, and the new one injects 4,925.
An instruction that trusts a hook hides the hook's failure. So every hook that injects text has a budget, cuts to fit and says where the rest is, and in the kit below, the next session start reports any hook that had to cut.
The kit: leases, the lease gate, the brief, the budget
A lease in the kit is a file in one folder every session on the machine can see,
created exclusively so a take is atomic, with the session as its owner and an
expiry that frees it if the session dies. By default a lease lasts 30 minutes
unless --for says otherwise, and the folder is
~/.claude/harness/leases. build is one lease per
checkout, taken by dotnet build, npm test, cargo,
go, make and the like, but not by a build into its own
folder with -o. gpu (lms load,
ollama run, vllm serve) and stack
(docker compose up, docker stop) are one per machine. A
project's harness.json replaces that list with its own.
node .claude/hooks/harness/lease.mjs take build --for 2h --purpose "benchmark run"
node .claude/hooks/harness/lease.mjs list
node .claude/hooks/harness/lease.mjs release build
# A job outside Claude Code, such as a nightly benchmark, names itself as the owner
HARNESS_LEASE_OWNER=nightly-benchmark node .claude/hooks/harness/lease.mjs take gpu --for 3h --purpose "nightly benchmark"
lease-gate.mjs(PreToolUse on Bash and PowerShell) maps a command to the resources it touches and denies it when another session holds one. Its first real refusals went to a second session on 28 September: "build is held by session e31755b5 for "harness proof: session A holds the site build" in thread-unsafe, 36 min left. This command needs it, so it did not run." The second of them was a mistake, a build into a scratch folder with-o, which needs no lease, so a resource now lists the commands it lets through. At session start it lists the leases other sessions hold, and says nothing when there are none.session-brief.mjs(SessionStart, not on resume, which already has it) injects only the newest handoff's open questions and next steps, fromdocs/journalby default, in 4,000 characters at most, with its path.lib/emit.mjsis the only way a harness hook speaks. It holds each hook to its budget, 600 to 6,000 characters by hook and never above 9,000, cuts at a line, ends the cut with the path of the full text, and logs the size, socontext-budget.mjscan say at the next session start which hook was cut.
Also in this set: Don't pay a frontier model to be a shell script, and The codebase remembers itself.