Fleet org-tier policy mod: keeps user-tier mods from gating tool calls above the fleet's shell PreToolUse hooks.

<img src="assets/hero/hero-light.webp" width="838" alt="A 36-second looping 3D film. It opens on the words “~/.claude, deployed from git. The sessions run each other. You only decide.” beside a dark floor where a dozen Claude Code terminal windows stand in a fan, each at the head of a glowing line in its account’s colour; every line bends into one gate labelled land-lock, and a single green line, origin/main, leaves it and runs on to three racks of files, hooks/, commands/ and skills/, under ~/.claude. In front of one window hangs a card: “Blocked — need your call: May a session switch to the bigger model on its own? 72 % sure, says the session.” The camera glides down onto that window: the session runs /handoff, the window splits, and a new Claude Code session boots in the right-hand pane on its own branch, on account 3. Then the decision card rises out of the first pane. The camera follows the new session’s commit up over the fleet and through the gate, where commits cross one at a time and turn green, onto origin/main, “verified by content”, where the files it changed in ~/.claude turn amber until deploy-live.sh turns them green. It flies back as the new session reports “SAFE TO CLOSE”, pings the first session and closes its own pane, and the film returns to its first frame.">
<sub>Every string on screen is real: a session fires a peer, each works on its own branch and account, commits land one at a time through the land-lock, and only the decision reaches you. <a href="assets/hero/launch-film.mp4">The 48-second film</a> (3840×2160, 60 fps) · <a href="tools/hero-film/README.md">how it was made</a>.</sub>
~/.claude, deployed from git.<br>The sessions run each other.<br>You only decide.1 · Deployed from git · 2 · The sessions run each other · 3 · You only decide · Where it stops · Install · Map
Claude Code reads everything it does from ~/.claude, and it is built for one session that a person watches. Run thirty sessions at once and three jobs land on that person. They must keep ~/.claude working, though it is unversioned machine state that every session depends on. They must keep the sessions out of each other's way, though all of them share one git index and one trunk. And they must judge when each session is really done, and which of its questions really need an answer.
This repo takes those three jobs over. ~/.claude is deployed from this git repo. The sessions start, place, message and retire one another. A session reaches you only with a decision it cannot make. One job is still yours, noticing a session stuck on a permission prompt, and the last section measures how long that takes. The repo is 7,309 files and 6,038 commits since 2026-03-24, checked by 16,159 tests, running on one Mac across four Claude accounts. Install takes five commands.
| What the system does | The job it takes off you | |
|---|---|---|
| 1 | The whole system deploys from git. Every change to the running system, from a hook to the Claude Code binary itself, is tested, landed, proven live, and can be reverted. | Keeping a hand-edited config dir alive |
| 2 | The sessions run each other. A session opens new sessions in new terminal panes, gives each its own branch and account, and hears back when they finish. Hooks can refuse any single command, and files, plans and transcripts outlive the pane. | Starting, placing and supervising sessions |
| 3 | You only decide. A session cannot call itself done while git says otherwise, and cannot hand you a task it could do itself. What reaches you is one decision or one command. | Judging "done", and sorting real questions from parked work |
A system that edits its own controller has to be versioned, tested and revertible like any other software, down to the binary it runs on. This repo is the source of truth, and ~/.claude is its deployment.
<!-- Diagram source: assets/diagrams/deploy-model.mmd — edit it, run npm run diagrams, commit the regenerated SVGs. --> <img src="assets/diagrams/deploy-model-light.svg" alt="The claude-infrastructure repo — 7,309 files under one reviewable history — deploys three ways. install.sh symlinks hooks, commands and scripts into the primary ~/.claude, so editing the live hook is editing the repo. install.sh --config-dir installs the same system into the four billing-isolated account directories; as deployed those directories symlink the code surfaces back to the primary, and isolate only their own auth and settings. Global surfaces — ~/bin tools, 101 cc tools, 28 LaunchAgents and the statusline — are copied, and sync.sh pulls hand-edits back.">
<!-- mermaid-fence: assets/diagrams/deploy-model.mmd (auto-synced by npm run diagrams) -->
flowchart TB
Repo["claude-infrastructure<br/>7,309 files · one reviewable history"]
Repo <-->|"install.sh · SYMLINK<br/>editing the live hook IS editing the repo"| Prim["~/.claude<br/>hooks · commands · scripts"]
Repo -->|"--config-dir · code SYMLINKED<br/>auth + settings per-account"| Alt["~/.claude-secondary … 4<br/>4 billing-isolated accounts"]
Repo -->|"COPY<br/>sync.sh pulls hand-edits back"| Glob["~/bin · LaunchAgents<br/>101 cc-* tools · 28 daemons · statusline"]
classDef src fill:#2b2410,stroke:#d4af37,color:#e6edf3
classDef dep fill:#0d1d2e,stroke:#58a6ff,color:#e6edf3
class Repo src
class Prim,Alt,Glob dep
<sup><a href="assets/diagrams/deploy-model-dark.svg?raw=true">full-screen dark</a> · <a href="assets/diagrams/deploy-model-light.svg?raw=true">light</a> · <a href="assets/diagrams/deploy-model.mmd">source</a></sup>
~/.claude is a symlink deployment of this repoEditing a live hook edits this repo, because the live hook is a symlink into it. The rule came from a failure: on 2026-07-03 a copied handoff-fire.sh had drifted 198 lines from the repo and was one install.sh away from being overwritten. ~/.claude is the primary deployment. Each of the four accounts has its own config dir (~/.claude-next, -secondary, -tertiary, -quaternary), which keeps its own login and settings but links its hooks/, commands/ and scripts/ back into this checkout, so one land reaches every account. Changes to settings, launchd jobs or credentials go through numbered, idempotent migrations (migrations/, 44 so far). Almost all of them wait for you to run, because an agent must not rewrite its own permissions.
The full test corpus runs on every tree that reaches trunk, just not inside the land. There are 16,159 bats tests in 801 files. A land runs only the suites its change touches. The whole corpus belongs to one background verifier, postland-verify.sh, which runs each trunk tree in a fresh checkout. When a tree goes red it bisects down to the one failing test, and re-runs the suspect commit's own tree before reverting it, so a flaky test cannot get an innocent commit reverted. Diagrams have their own check: npm run diagrams:check fails if a rendered SVG or an embedded mermaid block has drifted from its .mmd source.
A commit on trunk changes nothing until the shared checkout moves, and deploy-live.sh moves it after every land. Before that trigger, the median time from commit to live was 2.31 hours, and 40% of advances were run by hand. It moves to the newest tree the verifier has passed. If none is recent, it takes the newest tree not known to be red, with a banner and a page. If trunk is red all the way down, it refuses. It then re-runs install.sh so new files get their links, runs the host test suites against the live layer, and checks what the running processes actually loaded. A long-lived daemon can no longer run day-old code while the deploy reports success. A dead verifier or a lagging deploy is raised on cc-blockers rather than assumed healthy.
Every Claude Code version installs into its own directory, and one atomic symlink picks the active one. npm overwrites the binary in place, which throws ENOTEMPTY while live sessions hold its files. A symlink swapped by rename(2) is atomic where ln -sfn is not, and the kernel keeps a running binary alive even after its directory is deleted. claude-latest wraps the install and the clean-up. Before a version or model is promoted, cc-upgrade-gate.sh runs 15 checks against the candidate: model registration, auto mode, effort levels, spawn-depth limits, teammate, workflow and subagent spawns, hooks, resume, MCP servers and credential safety. It returns one GREEN or RED verdict. Then cc-lr upgrade moves idle sessions onto the new binary and model in place, one at a time, and never touches a session that is mid-turn. The default today is Opus 5.5 (claude-opus-5-5) on Claude Code 2.1.280. A second, stable track stays pinned at 2.1.114 until three upstream issues that block its next version are fixed.
A session is an addressable process, not a terminal you watch. It can open another session in a new pane, brief it, hear back from it, and close its own pane when the work has moved on. That stays safe because no session can collide with another's work, run a refused command, or take its work down with its pane.
<img src="assets/demo/handoff-live.webp" width="838" alt="Screen recording of one real terminal window in three captioned beats, played at about 1.8 times real speed. One: a real Claude session runs handoff-fire.sh with --split-right --notify-back, and the pane splits. Two: a second Claude session boots in the new pane, reads its brief, gets origin/main = 04c549d2, and reports the back-channel ping as verdict=delivered reason=wake-path-armed before running self-close --terminal. Three: the ping arrives inside the originator's own chat as PING RECEIVED FROM PEER with the peer's message, and the peer has closed its own pane — the window is back to one.">
<sub><b>A recording of two real Claude sessions in one window, played at about 1.8× speed</b> (roughly 50 seconds of real time in 30). One session fires a peer and the pane <b>splits</b>. The peer answers <code>origin/main = 04c549d2</code> and <b>pings back</b>, and the ping arrives <b>in the first session's chat</b>. The peer then <b>closes its own pane</b>. The only human keystroke is the first prompt. Recorded 2026-07-28 on iTerm2; kitty has been the only terminal in use since 2026-08-03, and the same commands drive it. <a href="assets/demo/handoff-live.mp4">Full-resolution video</a> (1920×1144, 60 fps).</sub>
Five commands cover a session's whole life: open, restart, move, message, close. To fire is to launch a new, briefed session in a new pane.
| Touchpoint | Command | What happens |
|---|---|---|
| Fire | /handoff → handoff-fire.sh --split-right | Splits the firing pane (never whichever window has focus), picks one of the four accounts on live quota, creates a worktree, then types and submits the brief. It waits until the new session has provably started work, then arms a /goal that states the end state. |
| Recycle | handoff-fire.sh --recycle [--worktree B] [--account A] | Types /exit and relaunches this pane with a fresh context, optionally on a new worktree or account. It refuses (exit 4) while subagents are still running, and names each one's partial transcript. |
| Switch | cc-lr switch | Moves a live session to another account in place: same pane, same session id, full transcript. Use it when the context is worth keeping and only the account paying for it is wrong. |
| Message | cc-notify · --notify-back · cc-await-ping | A fired session reports completion, a question or a blocker to the session that fired it. The reply channel is on by default for every fire. |
| Retire | handoff-fire.sh self-close --successor <uuid> | Closes the pane once the work has moved on. The successor must be alive and have taken a real turn first. --terminal, for a session with no successor, refuses (exit 8) while it still holds unlanded commits or unfinished scope. |
The same commands, recorded at the command line:
<img src="assets/demo/handoff-real.webp" width="838" alt="Terminal recording in three scenes. Scene 1, self-open: handoff-fire.sh --dry-run ranks all four accounts by live quota headroom, resolves the split anchor to the firing pane's own session id, and prints the exact composed launch command. Scene 2, two-way: cc-notify --self prints the pane uuid, cc-notify writes a message that appears as one timestamped line in the mailbox file, mailbox-drain.sh emits it as UserPromptSubmit additionalContext, and a second drain returns zero bytes because the seen cursor already consumed it. Scene 3, self-close: a bare self-close is refused for having no succession statement, and self-close --terminal is refused because an origin session was never fired by an originator.">
<sub>Recorded with <a href="https://github.com/charmbracelet/vhs">VHS</a> from <a href="assets/demo/handoff-real.tape"><code>assets/demo/handoff-real.tape</code></a>, so it can be re-run and cannot drift from the scripts it shows. The fire and self-close scenes use <code>--dry-run</code>; the mailbox round trip is real, against a temporary inbox.</sub>
A message is a file, not a keystroke. cc-notify appends one line to the recipient's inbox. The recipient reads it only at a moment when nothing it is typing can be corrupted, and marks it read exactly once:
<!-- Diagram source: assets/diagrams/session-comms.mmd — edit it, run npm run diagrams, commit the regenerated SVGs. --> <img src="assets/diagrams/session-comms-light.svg" alt="A message sent with cc-notify to a role is resolved at send time: if the target is live it goes straight to its mailbox file; if the pane recycled it follows a .forward chain to the successor; if the target is gone entirely it is tee'd to the desk tagged with the original uuid. The mailbox holds one line per message. It is drained at a safe boundary — SessionStart, UserPromptSubmit, or after a tool call at most once every 20 seconds — arrives as context rather than keystrokes on a live input line, and is acked at Stop through an exactly-once cursor. An idle peer is woken by the same write, through mailbox-wake-arm or cc-await-ping.">
<!-- mermaid-fence: assets/diagrams/session-comms.mmd (auto-synced by npm run diagrams) -->
flowchart TB
Box[("mailbox/<uuid>.md<br/>one line per message")]
Send["any session<br/>cc-notify --role peer"] --> Res{"resolved at<br/>SEND time"}
Res -->|"live"| Box
Res -->|"pane recycled"| Fwd[".forward chain<br/>→ the successor"]
Res -->|"gone entirely"| Desk["tee'd to the desk<br/>tagged for:<uuid>"]
Fwd --> Box
Desk --> Box
Box --> Drain["drained at a SAFE boundary<br/>SessionStart · UserPromptSubmit<br/>· after a tool call (≤ 1 per 20 s)"]
Drain --> Ctx["arrives as CONTEXT —<br/>never keystrokes on a live input line"]
Ctx --> Ack["acked at Stop:<br/>exactly-once cursor"]
Wake["idle peer?<br/>mailbox-wake-arm · cc-await-ping"] -.->|"the write IS the wake"| Box
classDef k fill:#2b2410,stroke:#d4af37,color:#e6edf3
classDef b fill:#0d1d2e,stroke:#58a6ff,color:#e6edf3
classDef g fill:#12261a,stroke:#3fb950,color:#e6edf3
class Res,Box k
class Send,Fwd,Desk,Drain b
class Ctx,Ack,Wake g
<sup><a href="assets/diagrams/session-comms-dark.svg?raw=true">full-screen dark</a> · <a href="assets/diagrams/session-comms-light.svg?raw=true">light</a> · <a href="assets/diagrams/session-comms.mmd">source</a></sup>
Each rule below exists because a failure happened first, and each is now enforced in code:
desk, the standing orchestrator session), which is resolved when the message is sent. A .forward chain follows each succession, and a line nobody can receive is still recorded and copied to the desk.cc-notify reports "mailbox only", and cc-inbox-guard fails loud on mail that nothing will read..seen cursor moves when a message is delivered. The .acked cursor moves only at the end of a turn that provably carried it. A crash mid-turn therefore shows the message again rather than losing it.Sessions that share one checkout share its git index and its trunk, so each writer gets a worktree and an account of its own, and every land waits for one machine-wide lock.
<!-- Diagram source: assets/diagrams/parallel-lanes.mmd — edit it, run npm run diagrams, commit the regenerated SVGs. --> <img src="assets/diagrams/parallel-lanes-light.svg" alt="Sessions A, B and C each work in their own worktree on their own account. All three run an unlocked gate that takes minutes — statics, ratchets and a bounded smoke that sheds by skipping under load, never a test corpus — then funnel into a single machine-wide land-lock held a median of 3 seconds (90th percentile 8 seconds), covering only the compare-and-swap push window. The push is verified by content rather than by commit count before the work reaches origin/main. Behind the trunk, one background verifier runs the full corpus in a fresh cell with host suites partitioned out, and deploy-live, kicked by every land, advances the live ~/.claude layer and never onto a RED tree.">
<!-- mermaid-fence: assets/diagrams/parallel-lanes.mmd (auto-synced by npm run diagrams) -->
flowchart LR
A["session A<br/>own worktree · acct 1"] --> G
B["session B<br/>own worktree · acct 2"] --> G
C["session C<br/>own worktree · acct 3"] --> G
G["gate — UNLOCKED, minutes<br/>statics + ratchets + bounded smoke<br/>(sheds by SKIPPING under load; no corpus, ever)"]
G --> Lock{"land-lock<br/>held p50 3 s (p90 8 s):<br/>the CAS push window only"}
Lock --> Push["push → verify by CONTENT,<br/>not commit count"]
Push --> Trunk[("origin/main")]
Trunk --> V["verifier — ONE, background band<br/>full corpus, fresh cell,<br/>host suites partitioned out"]
V -->|"verdict stamp"| D["deploy-live<br/>kicked by every land · never a RED tree"]
D --> Live[("live ~/.claude")]
classDef w fill:#0d1d2e,stroke:#58a6ff,color:#e6edf3
classDef k fill:#2b2410,stroke:#d4af37,color:#e6edf3
classDef t fill:#12261a,stroke:#3fb950,color:#e6edf3
class A,B,C w
class G,Lock,Push,V,D k
class Trunk,Live t
<sup><a href="assets/diagrams/parallel-lanes-dark.svg?raw=true">full-screen dark</a> · <a href="assets/diagrams/parallel-lanes-light.svg?raw=true">light</a> · <a href="assets/diagrams/parallel-lanes.mmd">source</a></sup>
git commit in a shared checkout sweeps up another session's staged files. So every writer works in its own worktree. An app repo can keep a pool of pre-built worktrees (scripts/worktree-pool.sh) and hand one out in about three seconds, with dependencies, generated code, .env.local and a seeded database ready; this repo is light enough to create them fresh. A hook refuses git commit in this repo's shared checkout outright, because one stray commit there once stopped the live layer from updating on every account.claude-accounts therefore picks, among accounts whose 5-hour window can take one more busy session, the one whose weekly quota resets soonest, so quota is spent before it expires. A session counts as busy if its transcript changed in the last ten minutes, never merely because its pane is open, so a burst of fires spreads down the ranking instead of piling onto one account. The session you type in follows the same rule, but only moves when another account wins by 15%. Recovery work uses a stricter rule that skips any account likely to hit its limit within the hour. Each term fails soft and has its own off switch. Design record: ACCOUNT_ROUTING_V2.md §13–15 and R2-policy-design.md.LAND_PIPELINE_V2.md). ship-land.sh checks only what the change can break: static checks, rules that may only tighten, and the test suites the change touches (180 s each, 900 s in total), which it skips when the machine is loaded. land-lock.sh holds the lock only for the push itself: 3 s median, 8 s at the 90th percentile, over 938 holds in 14 days. A whole land takes longer, about 15 minutes median and 43 at the 90th percentile, and most of that is the checks. These figures drift, so re-derive them rather than quote them: gate-red-census.sh and land-speed-census.py print each one with its coverage.hooks/fleet-core.mjs 46 lines1// fleet-core: the fleet's org-tier mod, meant to sit in managed prependPlugins
2// ahead of sec-default@builtin. The fleet's ~98 shell PreToolUse hooks run in
3// core, below every mod, so a user-tier mod that answers tool.call or approves
4// at tool.check walks past them. This mod refuses such mods at load.
5
6// Events whose hooks can decide a tool call before the shell hooks see it.
7const GATE_EVENTS = ['tool.call', 'tool.check', 'classic.PreToolUse', 'classic.PermissionRequest']
8
9// Provenances (<name>@<marketplace>) allowed to gate anyway, after review.
10const ALLOWED = []
11
12// A registration pattern covers a gate event when it names it or is a
13// wildcard prefix of it ('*', 'tool.*', 'classic.*'). '!x' exclusions never add.
14function gates(pattern) {
15 if (pattern.startsWith('!')) return []
16 if (pattern.endsWith('*')) {
17 const prefix = pattern.slice(0, -1)
18 return GATE_EVENTS.filter((ev) => ev.startsWith(prefix))
19 }
20 return GATE_EVENTS.filter((ev) => ev === pattern)
21}
22
23async function judge($, e, next) {
24 if (e.tier !== 'user' || ALLOWED.includes(e.provenance)) return next(e)
25 const hit = [...new Set(e.uses.events.flatMap(gates))]
26 if (hit.length > 0) {
27 $.ui.log('fleet-core refused ' + JSON.stringify(e.provenance) + ' for ' + hit.join(','), { to: 'debug' })
28 return { refuse: 'fleet policy: user mods may not hook ' + hit.join(', ') + ' (they run above the fleet shell hooks)' }
29 }
30 return next(e)
31}
32
33export function register(on) {
34 // Fail closed: a judge that throws or times out refuses user mods.
35 on('plugin.register', judge).catch(async ($, e, next) => {
36 if (e.tier !== 'user') return next(e)
37 return { refuse: 'fleet policy check failed, so this mod was not loaded' }
38 })
39
40 // One debug line per session proves the org tier reached this process.
41 on('session.start', async ($, e, next) => {
42 $.ui.log('fleet-core seated cwd=' + JSON.stringify(e.cwd), { to: 'debug' })
43 return next(e)
44 })
45}
46