Usage-limit monitor and automatic handoff for Claude Code, Codex, agy and Grok. leg claude runs the same agent with a board alongside; at the limit the next model or agent continues in the same terminal. Local-first, zero runtime deps, 14-day free trial.
See the codeType leg claude, leg codex, leg agy or leg grok instead of the bare command. You get the same interactive agent; Leg opens a board next to it, watches the usage limit, keeps a handoff bundle current, and when the limit hits it starts the next agent in the same terminal from that bundle.

Claude hits the five-hour wall. The terminal reads handing off to codex, and codex carries on there. Nothing is retyped. (the full 53-second run)

You keep using your coding agents exactly as you do today, in any terminal,
from your own config directory: Leg adds its hooks in a separate per-session
settings file and never edits yours (unless you turn on the portable
harness, which writes only marked, backed-up, Leg-owned
files and regions). leg claude --model opus is
claude --model opus with four things running alongside it:
waiting on you leg on main dirty 3 ahead 2 claude/fable. Under that come the prompt, the one thing worth
knowing, the files it is touching and the usage bucket that will stop it.
Capacity is one strip at the top rather than a region, with the login
panels and their gauges behind Capacity and models. Two sessions
editing the same file in one repo are flagged on both rows, and a second
session in a checkout that already has one gets its own worktree and a
Land button instead of writing over the first.StopFailure hook, Codex's
read-only app-server rate-limit read, agy's log, and Grok's billing proxy endpoint.claude/fable,
claude/opus, claude/sonnet, then each remaining installed agent. A
Fable limit walls Fable and not the login, so the first move is usually to
another model on the same subscription, and for claude that move is
claude --resume <id> --model opus, which keeps the conversation instead
of replaying the bundle. Only when every claude rung is out does it move to
another CLI. Settings holds the ladder for new terminals, leg ladder
edits it from a shell, and Details on a row edits the copy that
terminal is running. Nothing is retyped. When every rung is out, Leg tells
you which resets first and when, waits for that reset with a countdown, and
starts that agent from the bundle.Subscription logins only: Leg strips ANTHROPIC_API_KEY,
ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL, ANTHROPIC_CUSTOM_HEADERS,
OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_API_BASE, GEMINI_API_KEY,
GOOGLE_API_KEY, GOOGLE_GEMINI_BASE_URL, GOOGLE_GENAI_USE_VERTEXAI,
GOOGLE_GENAI_USE_ENTERPRISE, GOOGLE_CLOUD_PROJECT,
GOOGLE_CLOUD_LOCATION, GOOGLE_APPLICATION_CREDENTIALS, GROK_API_KEY,
XAI_API_KEY, CLAUDECODE, CLAUDE_CODE_*, CLAUDE_EFFORT, and
CLAUDE_PLUGIN_DATA before any agent starts. It then sets
CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0 for a detached
Claude print session. Leg never edits ~/.claude/settings.json or any other
settings file of yours; its hooks ride in a separate per-session --settings
file. The one thing it does write outside ~/.leg by default is the
folder-trust answer, below; the optional portable harness
is the other, and only after leg harness enable. leg uninstall removes only ~/.leg.
Each agent CLI asks once, the first time it runs in a directory, whether you
trust that folder, and Claude Code asks a second question when a CLAUDE.md
above the repo imports a file from outside it. A handoff fires when the limit
hits, which is usually when nobody is watching, so an agent that stopped on
that prompt would sit there until morning with the bundle already written.
Before starting an agent, Leg records the same answer you would have given,
for the repository you already chose by typing leg claude in it:
| agent | file | what is written |
|---|---|---|
| claude | ~/.claude.json (or $CLAUDE_CONFIG_DIR/.claude.json) | projects["<repo>"].hasTrustDialogAccepted: true |
| claude | the same entry, only when such an import exists | hasClaudeMdExternalIncludesApproved, hasClaudeMdExternalIncludesWarningShown |
| codex | ~/.codex/config.toml | [projects."<repo>"] trust_level = "trusted" |
| agy | ~/.gemini/antigravity-cli/settings.json | trustedWorkspaces: ["<repo>", "<worktree>"] |
For Claude Code this is the documented remedy: its permissions guide says to
set projects["<path>"].hasTrustDialogAccepted to true in ~/.claude.json,
where <path> is the repository root.
Leg never creates one of those files: if it is not there, that CLI has not
run as you yet and its own first-run flow is next, with you at the keyboard. It
never rewrites a file to say what it already says, and it never removes what is
already in one. When it approves an external CLAUDE.md import it prints the
full path of every file it approved, to the terminal and to the session
timeline on the board, so the approval is on the record rather than invisible.
Set LEG_TRUST=never to switch all of it off and answer the prompts
yourself.
Prerequisites: Node 22 or newer, git, Python 3 with pip, and at least one
logged-in agent CLI (claude, codex, agy or grok).
npm install -g @ucsandman/legcli
pip install -U context-handoff-bundle
cd <any repo>
leg claude
npm install -g leg-agents is the same release: it pins this version of
@ucsandman/legcli and exposes the same leg binary.
That is the whole setup. The first leg <agent> starts the board on
http://127.0.0.1:4747 and opens it; later sessions reuse it. Anything after the
agent name passes straight through (leg codex -m gpt-5.3-codex-spark,
leg claude --resume). The agent's own prompt and permission flags pass
through unchanged, and your settings file is never edited: Leg's hooks ride
in a separate per-session --settings file.
Leg is commercial, source-available software, and the source is on
GitHub: every .mjs file that runs is in the package you just installed, at
$(npm root -g)/legcli/src, and the license lets you read it and modify
your own copy. There is nothing compiled, minified or bundled to see through.
Nothing is guessed from screen scraping. Each tap was read from the CLI's source or documentation and then checked on a real machine (2026-09-11, Claude Code 2.1.268, codex-cli 0.153.4, agy 1.2.0); the rightmost column says which.
| agent | usage percentages | the wall (limit hit) | how Leg attaches | status |
|---|---|---|---|---|
| claude | GET api.anthropic.com/api/oauth/usage with the login Claude Code stored, the same data as /usage and the built-in status line (five_hour, seven_day, utilization, resets_at); polled every 60 s | StopFailure hook with error: rate_limit (docs) | one extra settings file per session via --settings, carrying only Leg's own hooks; autoContinueAtUsageLimit is set to false because Leg owns the handoff | observed live |
| codex | read-only account/rateLimits/read through the app-server, polled every 60 s by the board and active attach; windows are identified by duration (300 minutes = 5h, 10080 = 7d) | task_complete.error.codex_error_info: usage_limit_exceeded, message "You've hit your usage limit … try again at …" (codex-rs/protocol/src/error.rs) | no model turn and no hook are injected; the board reads the CLI backend and records only returned windows | verified by source and regression tests |
| agy | none exposed (agy's own status line fetches a quota summary that is written nowhere) | RESOURCE_EXHAUSTED, "it resets in …", "out of quota" in the log | --log-file per session; ~/.gemini/antigravity-cli/history.jsonl gives the prompts and conversation id | observed live (a real RESOURCE_EXHAUSTED with its reset was read from the log on 2026-09-11) |
| grok | GET cli-chat-proxy.grok.com/v1/billing?format=credits and GET cli-chat-proxy.grok.com/v1/user?include=subscription with OAuth token from ~/.grok/auth.json (creditUsagePercent, currentPeriod weekly reset); polled every 60 s | -32003 rate limit error, "You've hit the rate limit for your plan. Try again later.", "Rate limited (429)", StopFailureKind::RateLimit (xai-org/grok-build) | --debug-file grok.log per session; ~/.grok/sessions/<encoded-cwd>/prompt_history.jsonl gives prompts and session id | verified by source and proxy endpoint |
Why not Claude Code's status line JSON (rate_limits.five_hour.used_percentage):
on 2.1.268 the custom statusLine Leg passes through --settings did not
run, so the endpoint poll is the source. Leg still writes a statusLine
entry that records the same fields, so the moment a build honours it the poll
becomes a fallback.
For Codex, Leg does not infer availability from a lower percentage: only an
explicit available answer from the backend clears an earlier wall. The board
labels each bar as <n>% used and marks an old reading as stale rather than
presenting it as current.
LEG_WARN_PCT) the session card turns
amber, the event log names the next option, and the terminal bell rings once.StopFailure hook fires with rate_limit. codex: the
rollout reports usage_limit_exceeded. agy: the log says
RESOURCE_EXHAUSTED. The account is marked walled until the reset the CLI
reported (or the soonest known window reset).git diff --stat, dirty files, files edited this session, recent
commits, why it stopped) and runs context-handoff-bundle save --repo-local
with one slug per session, updated in place (--update <slug>) at every
checkpoint, about every two minutes while the session is active, and at
every warning, limit and hand-off. If the session maintained .leg/SYNTHESIS-<session-id>.md,
Leg inlines it into the resume file as a ## Synthesis section ahead of the raw dump.claude --resume <id> --model <alias>),
and another claude login you added with leg accounts add, which sees the
same transcript through the projects junction the account carries
(claude --resume <id> under that login's CLAUDE_CONFIG_DIR). A weekly
or Fable wall on one login then continues on the other with the
conversation it already had, and the timeline says kept the conversation. Every other rung starts in the same terminal with a short
pointer prompt:
read .leg/RESUME-<session-id>.md (the context-handoff-bundle load
output, the ## Synthesis section if present, and the reason for the switch),
check git status and git diff, continue, do not ask the human to restate
the task. The pointer prompt directs the next agent to read Synthesis first,
treat ruled-out approaches as settled, and start from the top-ranked next step.
The same text is copied to .leg/RESUME.md, the file people open by habit, and
both are stamped with the commit and the live terminals they describe.
claude "<prompt>", codex "<prompt>" and agy -i "<prompt>" all open the
normal interactive session with that first turn.leg ladder
to do it from a shell, or Settings to set the default copied by new
terminals. A rung whose CLI is missing, whose wall has not reset, or that
shares the window that is already out, is skipped with its reason.waiting for <agent> at <time>. Ctrl-C (or End on the row) quits with exit 3 instead.You can force a handoff any time: the Hand off now button on the row, or
leg sessions handoff <id>. Verified on this machine: leg claude opened
the real Claude Code TUI with Leg's hooks firing into the session log, the
usage poll recorded 36 % of the 5h window and 74 % of the 7d window, the
warning fired at 96 % of the 7d window and named codex as the next option, and
a limit saved the bundle, stopped claude and started codex in the same
terminal with the pointer prompt. A real StopFailure arrived on 2026-09-11
and is kept at fixtures/live/claude/limit-rate_limit.json; to drive the path
on demand, leg sessions simulate-limit <id> sends the same StopFailure
rate_limit payload Claude Code would send through Leg's hook: verified end
to end on a haiku session, the hook set the limit, the runner saved the
bundle, stopped claude and started codex, which read .leg/RESUME.md on its
first turn. The simulated wall clears after two minutes and is never kept as
evidence. The first real StopFailure was saved that way, with secrets
scrubbed, at fixtures/live/claude/limit-rate_limit.json, and the claude docs
row flipped to observed-live (node scripts/live-limits.mjs). The same
capture is wired for codex usage_limit_exceeded and agy RESOURCE_EXHAUSTED;
no payload for either has been kept yet.
Terminals started by this version can change their ladder while they run: Settings holds the default for new terminals, and Details on a row edits the copy that terminal is running. An older terminal stays on the order it started with; its row says a restart is needed and can save the desired default for the next launch. A normal agent exit ends the terminal. It does not trigger a handoff.
With the portable harness enabled (leg harness enable,
off by default), the hand-off also prepares the destination's environment
before it starts: the source agent's global rules, identity, hooks, skills,
subagents, slash commands, MCP servers and permissions, rendered into the
destination's own files as far as it can represent them. The terminal then
reads, for instance, codex harness partial · 8/8 components, 3 dropped · 1 file(s) written, and the terminal's details list what was dropped and why.
The source is fingerprinted so an unchanged environment costs a few stat
calls; a policy (warn, sync, strict) says whether a hand-off may write,
and strict refuses a destination it cannot make safe. Credentials never move
and which login runs stays the account layer's decision.
Two agents in one working tree write over each other's files. So when you
start leg codex in a checkout where leg claude is already live, the new
session gets its own git worktree, <repo>/.leg-worktrees/<session-id> on
branch leg/<session-id>, cut from the branch the checkout has out, and the
terminal prints one line saying where it is. The agent starts there; the card,
the usage tracking and the handoff work the same. --no-worktree shares the
checkout on purpose (Leg takes the flag out; the agent never sees it).
The card of a session with its own worktree has a Land button. It sends
the branch through the merge queue: whatever the agent left uncommitted is
committed on the branch, the branch is rebased onto its base, the repo's test
command runs (package.json test, pytest, or none with a warning), and the
base is fast-forwarded, never merged. When a step fails nothing lands and the
card says why: rebase-conflict with the files, tests-red with the end of
the output, dirty-trunk when the checkout has local changes the landing
would overwrite. Local changes it would not touch are left alone. The
landed-on-trunk list says which terminal landed each commit. Remove safely
prunes a finished session only when its worktree is clean and its branch is
already on the base. Remove record is a separate visible button with a
confirmation: it removes only Leg's saved session record and deliberately
keeps the worktree, branch, unmerged commits, and dirty files.
Verified live on 2026-09-11 with three haiku sessions in a throwaway repo. The
first stayed in the checkout; the second and third each got a worktree and
appended a line to README.md. Land on the second, clicked on the real board,
ran the repo's tests and fast-forwarded main; Land on the third bounced with
rebase-conflict on README.md and kept its commit on its branch; the
landed-on-trunk list named the second terminal (the screenshot above).
Off until you run it. leg share on binds the board to your Tailscale
address (or --bind lan, or an address you name) and gives every human their
own name and token; until then the board stays on 127.0.0.1 and there is no
token at all.
leg share on your own link, printed once
leg share add sam sam's link, printed once (a guest)
leg share add dana --role operator dana runs cards, not this machine
leg share who is on the board (never a token again)
leg share rotate sam sam's old link stops working
leg share off back to 127.0.0.1; every link stops working
There are three roles. owner is everything: the machine's settings, the harness, every terminal, the cards, the history index and the audit trail. operator is the pipeline board and their own terminals: they add, run, approve, reassign and kill cards, and they never see this machine's settings, its home path, its repository paths, its conversation index or the audit. guest is the terminals lane, read-only and redacted, with Request handoff as their only button.
A token is kept as a sha256 hash, so a lost link is re-issued, never re-read. The board takes the token out of the address bar and keeps it in the browser. Your own browser on this machine needs no token.
What another human sees is the Terminals region, read-only. Each panel says
whose terminal it is. On a panel that is not theirs there is no prompt, no file
name, no path, no bundle and no event log; what stays is the agent and session
tail, the status word, the sentence read-only: wes owns this terminal,
repo@branch, the worktree line, the elapsed clock, and one button,
Request handoff. The instrument head prints not shared in place of every
percentage. A request lands on the owner's panel as sam asked to take this terminal at 11:04 PM with Approve sam and Dismiss sam. The background
side of the board (cards, logs, the floor) stays the owner's alone. A terminal
belongs to the human who started it: LEG_PERSON=sam leg claude on the
same machine is sam's card, not yours.
The security pass that goes with it: every /api route needs a token, the
event stream included; twenty wrong tokens from one address and that address
waits a minute; one identity gets 600 requests a minute; a guest gets 403 on
everything that is not theirs; and the tests send a bad and a missing token to
every route. Verified live on 2026-09-11: two terminals on one machine, one wes's and
one sam's; sam's board showed wes's card with the prompt hidden and only
Request handoff, and sam's request reached wes's board (~/.leg/board.log:
"hand-off requested … by sam").
Off unless you hand it a certificate. Leg does not issue one: a self-signed pair teaches everyone on the board to click through a warning, which is worse than plaintext on a network that is already private. On Tailscale the pair is one command:
tailscale cert <machine>.<tailnet>.ts.net
leg share on --tls-cert <machine>.<tailnet>.ts.net.crt --tls-key <machine>.<tailnet>.ts.net.key
LEG_TLS_CERT and LEG_TLS_KEY do the same without writing the paths into
share.json, and they win over it. With a pair configured the shared address
serves https and every printed link says https://; the companion listener on
127.0.0.1, which exists so this machine's own browser needs no token, stays
plain http, because the certificate is for the shared name and loopback traffic
never leaves the machine. A pair that is half-configured, missing, unreadable
or empty stops the board with exit 3 rather than quietly serving plaintext.
Renew the pair, then leg down && leg up to pick it up.
Settings → Audit trail is one list across every terminal and every card,
newest first: hand-offs, landings, approvals, reassignments, kills, each with
the person or agent that did it, the repo and the time. Filter by person or by
kind. It reads what the ledger already recorded, so nothing new is stored, and
it prints how much it read beside the answer (14 terminals and 3 cards, 812 events read) so an empty trail cannot be mistaken for a quiet week. Owner
only: the trail names repositories and people.
leg digest (default window 8 hours; --since 2d, --since 30m, or an ISO
time; --json for the record; GET /api/digest?since= on the board, owner
only) is the trail read the other way round: grouped by repository, what
needs you first. The first line is the volume it was read from (3 terminals, 2 cards, 1 landing, 412 events read (66 sessions and 9 cards on disk)), then needs you: a live terminal waiting on a question, a card
parked for a human, a card that failed, a terminal that was lost, in that
order and each with how long ago. Then one block per repository: every
terminal that moved in the window with its login and model, state, turns,
files, commits ahead and the events worth a line (a wall, a hand-off and
whether it kept the conversation, an all-out wait, the end), every card with
its last event, every landing with who pressed Land, and last the walls
standing right now with their reset times. Nothing new is recorded; a window
with nothing in it says so with its counts.
leg <agent> opens it; leg open reopens it; leg down stops it.
claude 63% 6:01 PM, codex back Sat 10:11 PM, agy no figure, grok no reading. Capacity and models
opens a drawer holding the full login panels: the 5h and 7d rails, the reset
and how long that is, where and when Leg read the number, at the wall with
its back <day time>, and on the claude panel a rail of model chips
(fable 63%, opus 12%, a walled one saying out until 9:14 PM). The
sticky instrument head is still what /floor puts at the top.repo on branch, dirty <n> and
ahead <n>, agent/model, quiet <n>m, and own worktree, from main when
the session cut its own worktree; then the first prompt as a
button, exactly one sentence (the highest-ranked thing true about the
terminal), an also: disclosure naming the rest, the files as
comma-separated text, the binding bucket as 63% of the fable week, and an
elapsed clock with the session tail. Two live sessions in one repo touching
the same file print codex (codex-99ab) is changing src/server.mjs in another checkout; whoever lands second rebases on both rows. A Claude row
parked at a permission or idle prompt rises to the top, says waiting on you in place of its status word, prints the question verbatim, and puts a
count in the browser tab ((2) Leg, with a dot on the favicon).
After a Land the sentence is landed on <base>, <7-char sha>, <n> files, +<added>/-<removed>, or Land was attempted at <time> onto <base> and bounced: <first line of the reason>. The branch still holds every commit; nothing was lost.repo@branch chip, and
when plus who. A commit a Land put there says landed by <agent> (<id tail>).at its usage limit, back 11:40, shares the window that is out, buys nothing, not installed on this machine).
leg sessions handoff <id> --to claude/default/opus is the same choice from
a terminal. If the rung you picked walls between the click and the hand-off,
the work still continues down the ladder and the terminal says which one
took it instead.leg history continue command to copy where the agent
can resume by id. The same drawer lists every checkout Leg can see. Then
Settings. Background tasks are not down here: a live card is a row in
the Background panel directly under Terminals (see below), and only the
finished ones collapse into a ledger line beside these counts.The board reads ~/.leg/sessions/*/session.json over server-sent events; a
session whose runner process is gone is marked lost, never shown as live.
Claude Code, Codex, Grok, Antigravity and Copilot each keep their history in
their own place. leg history is one list over all of them, whether or not
Leg started the conversation, and leg worktrees is one list over every
checkout: git's, Leg's own, and the ones those conversations ran in.
leg history newest first, every agent; --provider, --repo, --search, --json
leg history show claude:0fc5 where it ran, its last messages, whether Leg can continue it
leg history continue claude:0fc5 leg claude --resume <id> in that folder, supervised like any session
leg worktrees path, repo, branch, exists, uncommitted, owner, conversations, stale
Nothing moves: each agent's store stays where it was, Leg writes only its own
~/.leg/history/index.json, reads transcripts from their head and tail, and
opens messages only when you open a conversation. A session Leg started and
the same conversation in the agent's store are one row, marked leg; the rest
are external. On a shared board the whole group is the owner's. The support
matrix (which agents list, show messages, continue) and every file read are in
docs/history.md.
Optional. leg accounts add claude work creates
~/.leg/accounts/claude/work, junctions your hooks, skills, agents,
commands, plugins, rules, scripts, output-styles, tools and
projects into it, copies settings.json, CLAUDE.md and the status-line
scripts (refreshed from your real ~/.claude before every launch), and prints
one line to paste:
$env:CLAUDE_CONFIG_DIR='C:\Users\you\.leg\accounts\claude\work'; claude auth login
projects is Claude Code's conversation store, so the second login sees the
same conversations and the same auto-memory as the first, and a hand-off from
one login to the other keeps the conversation: the terminal moves to work
with claude --resume <id> and no bundle prompt. That is the whole point of a
second 20x login when the Fable or the weekly window on the first one is out.
An account made by an older Leg gets the junction the next time it starts.
Claude Code writes that directory; Leg only reads it.
Same for codex (CODEX_HOME; config.toml, AGENTS.md, skills, prompts,
rules, plugins, agents, hooks, memories shared; a codex hand-off
still takes the bundle, because codex resume under a second CODEX_HOME
has not been observed). agy 1.2.0 has no config-directory override, so it
stays one account. Only the login lives in the account directory;
leg accounts rm removes the junctions and the directory and never touches
your real home.
The terms, as published (effective dates below):
Owning two paid subscriptions is not named as prohibited by either. Rotating to
a second account of the same vendor because the first one is rate-limited sits
close to OpenAI's "circumvent any rate limits" wording and Anthropic's
"circumvent product guardrails". Leg's default chain switches vendors
(claude → codex → agy), which is plainly fine. Same-vendor rotation only
happens after you run leg accounts add; that is your call.
~/.claude/settings.json, ~/.claude.json,
~/.codex/config.toml, agy's files, your repo's settings. Claude Code gets
hooks through a per-session --settings file under ~/.leg; codex and
agy get nothing injected.~/.claude/projects,
~/.codex/sessions, ~/.grok/sessions, ~/.gemini/antigravity-cli,
~/.copilot/session-state) for leg history; the index it builds lives
under ~/.leg/history/, and no SQLite file is ever opened.leg harness enable (the portable harness,
off by default): the destination client's global rules file
(~/.codex/AGENTS.md, ~/.gemini/GEMINI.md, ~/.claude/leg-rules.md plus
one @ line in CLAUDE.md), marked leg harness regions inside the
client's own config files, per-skill directory links, and one file per
subagent and slash command. Every file carries GENERATED by Leg harness,
every overwrite is backed up under ~/.leg/harness/backups, a hand-edited
file is skipped and named, the source client is never written, and no
credential ever moves (an MCP key becomes ${NAME}). leg harness disable
stops it and removes nothing..leg/ (session notes, RESUME.md and one
RESUME-<session-id>.md per hand-off),
.context-handoffs/ (the bundles) and .leg-worktrees/ (a second
session's worktree), all added to .git/info/exclude, plus the
leg/<session-id> branch of a session with its own worktree. Landing
fast-forwards your branch; nothing is ever pushed.ANTHROPIC_API_KEY,
ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL, ANTHROPIC_CUSTOM_HEADERS,
OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_API_BASE, GEMINI_API_KEY,
GOOGLE_API_KEY, GOOGLE_GEMINI_BASE_URL, GOOGLE_GENAI_USE_VERTEXAI,
GOOGLE_GENAI_USE_ENTERPRISE, GOOGLE_CLOUD_PROJECT,
GOOGLE_CLOUD_LOCATION, GOOGLE_APPLICATION_CREDENTIALS, CLAUDECODE,
CLAUDE_CODE_*, CLAUDE_EFFORT, and CLAUDE_PLUGIN_DATA. CLAUDE_CODE_*
does not include CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS: Leg sets that one
to 0 for a detached Claude print session.api.anthropic.com for the usage numbers. The ledger scrubs bearer tokens
and key shapes from every line regardless.leg uninstall --yes: removes ~/.leg (sessions, usage, extra
account directories with their junctions, v0.1 cards, the board pidfile) and
nothing else; then npm rm -g @ucsandman/legcli.leg claude|codex|agy|grok [agent args…] the interactive agent, board alongside, handoff on limit
[--no-worktree] share the checkout with a live session instead of a worktree
leg sessions ls [--json] every session and its usage
leg sessions show|events <id>
leg sessions handoff|end <id> same as the board buttons
leg sessions handoff <id> --to <agent>[/<account>] hand off to a destination you name
leg sessions rm <id> forget an ended session
leg sessions simulate-limit <id> the real limit path without a real wall (claude, agy, grok)
leg history [ls] [--provider p] [--repo r] [--search q] [--managed|--external] [--live] [--all] [--json]
every conversation on this machine, Leg's own and the agents' own (read only)
leg history show <id> [--messages n] [--json] | continue <id> [agent args…] | refresh [--full] | providers
leg worktrees [--repo <path>] [--no-dirty] [--json] every checkout: git's, Leg's, the conversations' (read only)
leg digest [--since 8h|2d|<iso>] [--json] what happened while you were away: needs-you first, then every
terminal, card, landing and wall in the window, by repository (read only)
leg accounts ls logins and their 5h/7d usage
leg accounts add <claude|codex|grok> <name> | rm <agent> <name> | terms
leg harness status|inspect|check|explain|history [--json] the portable harness, read-only
leg harness enable [--source claude|codex] [--policy warn|sync|strict] [--yes]
leg harness sync [--to codex,agy] [--force] [--dry-run] | diff <client> | doctor
leg harness capture [claude|codex] | source <client> | policy <mode> | disable
leg license the license on this machine, or where to buy one
leg license activate <key> | deactivate | refresh (refresh renews a Team key)
leg adapter list|show <n>|check <n> every adapter, built-in and custom
leg adapter template [--name n] a starter spec to fill in
leg adapter add <file.json> | rm <n> any CLI as a card agent, from JSON
leg share who is on the board (off by default; Team plan)
leg share on [--bind tailscale|lan|<addr>] [--port N] [--tls-cert <f> --tls-key <f>] | off
leg share add|rotate|rm <name> [--role owner|operator|guest] one link per human, printed once
leg open | down | status the board
leg uninstall [--yes]
Environment, all optional: LEG_HOME (default ~/.leg), LEG_PORT
(4747), LEG_ACCOUNT (start on a named login), LEG_WARN_PCT (85),
LEG_NO_HANDOFF=1 (warn and record, never switch), LEG_NO_OPEN=1 (do not
open the browser), LEG_USAGE_POLL_MS (60000), LEG_CLAUDE_ARGS /
LEG_CODEX_ARGS / LEG_AGY_ARGS / LEG_GROK_ARGS (extra args for a leg Leg starts after
a hand-off, e.g. -m gpt-5.3-codex-spark), LEG_CLAUDE_BIN,
LEG_CODEX_BIN, LEG_AGY_BIN, LEG_GROK_BIN, LEG_CHB_BIN, LEG_PERSON (whose
terminal this is when the board is shared), LEG_TLS_CERT / LEG_TLS_KEY
(serve the shared board over https; they win over share.json),
LEG_RATE_MAX (600 requests a minute per human) and LEG_RATE_MAX_FAILURES
(20 wrong tokens per address).
Version 0.1 was the other way round: you dropped a task card on the board and Leg ran the agents headless in a git worktree, one per card, with a fallback chain, path leases, a scheduler and a merge queue. All of that still runs, and a card is now a terminal you are not sitting at: same register, same one sentence, same ladder, same bundle. Live cards are rows in the Background panel directly under Terminals; finished ones fall into one ledger line that opens.
Starting one is a single field. Run in the background: takes the task, and
the sentence under it is inferred with its nouns as buttons: the repo of the
terminal you were last in, the saved ladder, and the build workflow, which
stops with its changes in the card's worktree and does not merge them.
More settings is the full form, with the Build, test, and merge and
Factory workflows that include an automatic land station, plus fallback
agents, permissions, approval gates, turn caps, leases, trunk, merge method,
tests, title, and scripted test and demo adapters. End, and keep going as a
card on a terminal's End row makes a card out of the work in front of you;
Take over on a card gives you the command that turns it back into a
terminal.
leg up boots the board with the scheduler and merge queue and streams
redacted logs; leg card add --repo <path> --task "<t>" --chain claude,codex --queue
creates a card; presets build, build-land, factory; station kinds
agent, test, land, human.claude -p --output-format json --permission-mode <m>, codex exec --json -s <m> -C <worktree>,
agy -p --output-format json --mode <m> --add-dir <worktree>,
grok --prompt-file <f> --output-format json --permission-mode <m> --cwd <worktree>; fake, fake-claude, fake-codex, fake-agy for tests and
demos.leg adapter template --name muse > muse.json, fill in the command and its
flags, leg adapter add muse.json, then --chain muse,claude. leg adapter check muse prints the exact command line a leg would run before one does.
See docs/adapters.md. A custom adapter
runs cards; it is not an interactive leg <agent> terminal, because that
needs a usage tap and a wall signal, which only the four above expose..leg/DONE hands off with a bundle to the next adapter in the same
worktree; a land station rebases, tests and fast-forwards trunk or bounces
the card with the failure in the bundle..env: OpenClaw Workboard
(LEG_SYNC_WORKBOARD=1) and DashClaw (LEG_SYNC_DASHCLAW=1).The full v0.1 story, with the fake-limit demo and the real claude→codex run, is in docs/concepts.md, docs/DEMO.md, docs/real-run.md and docs/board-guide.md.
Leg binds 127.0.0.1. leg share on is the supported way to listen
anywhere else: it binds your Tailscale or LAN address and every human gets
their own token (see More than one human). Without
share, setting LEG_BIND to a non-loopback address needs LEG_TOKEN too,
or the server refuses to start (exit 3), and requests then need
Authorization: Bearer <token>. Tokenless owner access also requires a
loopback hostname (127.0.0.1, localhost, or [::1]), which prevents a
DNS-rebound hostname from inheriting local access. Either way there is no TLS.
leg open, or visit http://127.0.0.1:4747.
~/.leg/board.log has the server's output.claude auth login), the stored token
expired (start claude once, it refreshes), or the usage endpoint answered
with something Leg does not recognise. The card says which. The wall is
still caught through the hook; only the percentages are missing.lost: the terminal that ran leg <agent> is gone
(closed, crashed, machine slept through a kill). Remove it from the board.leg claude typed inside a Claude Code shell works;
the parent's CLAUDECODE markers are stripped so the child starts.npm install dies with edgesOut (clone only): the global npm is older
than Node; run npx --yes npm@latest install once.More in docs/faq.md.
| guide | read it when |
|---|---|
| Getting started | you want leg claude running in five minutes |
| Concepts | sessions, accounts, bundles, and the v0.1 cards, stations, chains and leases |
| Board guide | every word, number and button on the board explained |
| Configuration | environment variables and options |
| Portable harness | carrying rules, hooks, skills, agents, commands and MCP servers to the agent a hand-off lands on: what moves, what does not, policies, ownership, secrets |
| History | leg history and leg worktrees: every conversation and checkout on this machine across agents, the support matrix, what is read and what is written |
| Adapters | what each CLI exposes and how Leg attaches to it |
| CLI contracts | exact argv per CLI and the limit-signal table with sources |
| FAQ | a question the others did not answer |
| Demo and real run | the v0.1 handoff, fake and real |
| Vocabulary | statuses, outcomes and event types |
| Roadmap v2 | where this is going |
| Reuse and deviations | what was ported and every place the plan changed |
| Website | the public page: static HTML in site/, preview with python -m http.server 4780 --directory site, deployed to Vercel from that directory; PRODUCT.md and DESIGN.md at the root carry its brief and tokens |
Issues and pull requests are welcome. Read CONTRIBUTING.md for the dev setup and the rules (zero runtime deps, argv spawns only, no bypass flags, a privacy check on every commit). Security reports go through SECURITY.md.
npm install
npm test # node --test + privacy check
npm run lint
Any real agent session started only to test Leg runs on the cheapest model
(leg claude --model haiku); the live checks in test/ never start one.
Maintainer releases use npm trusted publishing with no NPM_TOKEN. Bump the
package, lockfile, site metadata and release notes, then push main.
npm version (and npm run sync-alias) writes packages/leg-agents to the
same version and pins @ucsandman/legcli to it; npm test fails if they
drift. CI waits for the Ubuntu and Windows test matrix, validates both
@ucsandman/legcli and leg-agents against npm, and publishes each only
when that version is missing and newer than the stable latest. Existing
versions skip cleanly; older, prerelease, lockstep, and registry-error cases
fail the job. The npm trusted publisher is bound to ucsandman/legcli and
.github/workflows/ci.yml (bind leg-agents the same way). Publication
uses --provenance=false.
Parts of the runner, ledger and git snapshot were ported from a private
repository that was MIT licensed (see NOTICE and
docs/REUSE.md), with chat identifiers, machine paths and
personal names removed. The test suite runs a privacy check on every commit,
and the fixtures store home paths as ~.
Leg is commercial software under the Leg License Agreement. It ships as readable JavaScript so you can see what it does on your machine, and you may modify it for your own use, but not redistribute it or work around the license check. Versions 0.2.0 and 0.3.0 were published under MIT and remain available. The version in this source tree is 0.15.0; see npm for published versions and CHANGELOG.md for release notes.
The first 14 days are a free trial: install it, type leg claude, and
everything in both plans is on, with no key and no card. The clock starts on
your first session and leg license status shows what is left. After that
it needs a license: Personal, $79 once, one human on any number of
machines, every release for 12 months and the version you have keeps working
after that; Team, $12 per seat per month, Personal plus leg share for
more than one human on the board. Buy at the site, then
leg license activate <key>. There is also a 30-day money-back
guarantee after buying, no reason required. A key is a signed token
checked offline with the public key in src/license.mjs; only a Team key
renewal talks to the site. The bare agent CLIs are never affected by any of
this; only what Leg adds is licensed.
140 commits
JavaScript
78.6%
HTML
18.3%
CSS
3.1%
Usage-limit monitor and automatic handoff for Claude Code, Codex, agy and Grok. leg claude runs the same agent with a board alongside; at the limit the next model or agent continues in the same terminal. Local-first, zero runtime deps, 14-day free trial.
See the codeType leg claude, leg codex, leg agy or leg grok instead of the bare command. You get the same interactive agent; Leg opens a board next to it, watches the usage limit, keeps a handoff bundle current, and when the limit hits it starts the next agent in the same terminal from that bundle.

Claude hits the five-hour wall. The terminal reads handing off to codex, and codex carries on there. Nothing is retyped. (the full 53-second run)

You keep using your coding agents exactly as you do today, in any terminal,
from your own config directory: Leg adds its hooks in a separate per-session
settings file and never edits yours (unless you turn on the portable
harness, which writes only marked, backed-up, Leg-owned
files and regions). leg claude --model opus is
claude --model opus with four things running alongside it:
waiting on you leg on main dirty 3 ahead 2 claude/fable. Under that come the prompt, the one thing worth
knowing, the files it is touching and the usage bucket that will stop it.
Capacity is one strip at the top rather than a region, with the login
panels and their gauges behind Capacity and models. Two sessions
editing the same file in one repo are flagged on both rows, and a second
session in a checkout that already has one gets its own worktree and a
Land button instead of writing over the first.StopFailure hook, Codex's
read-only app-server rate-limit read, agy's log, and Grok's billing proxy endpoint.claude/fable,
claude/opus, claude/sonnet, then each remaining installed agent. A
Fable limit walls Fable and not the login, so the first move is usually to
another model on the same subscription, and for claude that move is
claude --resume <id> --model opus, which keeps the conversation instead
of replaying the bundle. Only when every claude rung is out does it move to
another CLI. Settings holds the ladder for new terminals, leg ladder
edits it from a shell, and Details on a row edits the copy that
terminal is running. Nothing is retyped. When every rung is out, Leg tells
you which resets first and when, waits for that reset with a countdown, and
starts that agent from the bundle.Subscription logins only: Leg strips ANTHROPIC_API_KEY,
ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL, ANTHROPIC_CUSTOM_HEADERS,
OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_API_BASE, GEMINI_API_KEY,
GOOGLE_API_KEY, GOOGLE_GEMINI_BASE_URL, GOOGLE_GENAI_USE_VERTEXAI,
GOOGLE_GENAI_USE_ENTERPRISE, GOOGLE_CLOUD_PROJECT,
GOOGLE_CLOUD_LOCATION, GOOGLE_APPLICATION_CREDENTIALS, GROK_API_KEY,
XAI_API_KEY, CLAUDECODE, CLAUDE_CODE_*, CLAUDE_EFFORT, and
CLAUDE_PLUGIN_DATA before any agent starts. It then sets
CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0 for a detached
Claude print session. Leg never edits ~/.claude/settings.json or any other
settings file of yours; its hooks ride in a separate per-session --settings
file. The one thing it does write outside ~/.leg by default is the
folder-trust answer, below; the optional portable harness
is the other, and only after leg harness enable. leg uninstall removes only ~/.leg.
Each agent CLI asks once, the first time it runs in a directory, whether you
trust that folder, and Claude Code asks a second question when a CLAUDE.md
above the repo imports a file from outside it. A handoff fires when the limit
hits, which is usually when nobody is watching, so an agent that stopped on
that prompt would sit there until morning with the bundle already written.
Before starting an agent, Leg records the same answer you would have given,
for the repository you already chose by typing leg claude in it:
| agent | file | what is written |
|---|---|---|
| claude | ~/.claude.json (or $CLAUDE_CONFIG_DIR/.claude.json) | projects["<repo>"].hasTrustDialogAccepted: true |
| claude | the same entry, only when such an import exists | hasClaudeMdExternalIncludesApproved, hasClaudeMdExternalIncludesWarningShown |
| codex | ~/.codex/config.toml | [projects."<repo>"] trust_level = "trusted" |
| agy | ~/.gemini/antigravity-cli/settings.json | trustedWorkspaces: ["<repo>", "<worktree>"] |
For Claude Code this is the documented remedy: its permissions guide says to
set projects["<path>"].hasTrustDialogAccepted to true in ~/.claude.json,
where <path> is the repository root.
Leg never creates one of those files: if it is not there, that CLI has not
run as you yet and its own first-run flow is next, with you at the keyboard. It
never rewrites a file to say what it already says, and it never removes what is
already in one. When it approves an external CLAUDE.md import it prints the
full path of every file it approved, to the terminal and to the session
timeline on the board, so the approval is on the record rather than invisible.
Set LEG_TRUST=never to switch all of it off and answer the prompts
yourself.
Prerequisites: Node 22 or newer, git, Python 3 with pip, and at least one
logged-in agent CLI (claude, codex, agy or grok).
npm install -g @ucsandman/legcli
pip install -U context-handoff-bundle
cd <any repo>
leg claude
npm install -g leg-agents is the same release: it pins this version of
@ucsandman/legcli and exposes the same leg binary.
That is the whole setup. The first leg <agent> starts the board on
http://127.0.0.1:4747 and opens it; later sessions reuse it. Anything after the
agent name passes straight through (leg codex -m gpt-5.3-codex-spark,
leg claude --resume). The agent's own prompt and permission flags pass
through unchanged, and your settings file is never edited: Leg's hooks ride
in a separate per-session --settings file.
Leg is commercial, source-available software, and the source is on
GitHub: every .mjs file that runs is in the package you just installed, at
$(npm root -g)/legcli/src, and the license lets you read it and modify
your own copy. There is nothing compiled, minified or bundled to see through.
Nothing is guessed from screen scraping. Each tap was read from the CLI's source or documentation and then checked on a real machine (2026-09-11, Claude Code 2.1.268, codex-cli 0.153.4, agy 1.2.0); the rightmost column says which.
| agent | usage percentages | the wall (limit hit) | how Leg attaches | status |
|---|---|---|---|---|
| claude | GET api.anthropic.com/api/oauth/usage with the login Claude Code stored, the same data as /usage and the built-in status line (five_hour, seven_day, utilization, resets_at); polled every 60 s | StopFailure hook with error: rate_limit (docs) | one extra settings file per session via --settings, carrying only Leg's own hooks; autoContinueAtUsageLimit is set to false because Leg owns the handoff | observed live |
| codex | read-only account/rateLimits/read through the app-server, polled every 60 s by the board and active attach; windows are identified by duration (300 minutes = 5h, 10080 = 7d) | task_complete.error.codex_error_info: usage_limit_exceeded, message "You've hit your usage limit … try again at …" (codex-rs/protocol/src/error.rs) | no model turn and no hook are injected; the board reads the CLI backend and records only returned windows | verified by source and regression tests |
| agy | none exposed (agy's own status line fetches a quota summary that is written nowhere) | RESOURCE_EXHAUSTED, "it resets in …", "out of quota" in the log | --log-file per session; ~/.gemini/antigravity-cli/history.jsonl gives the prompts and conversation id | observed live (a real RESOURCE_EXHAUSTED with its reset was read from the log on 2026-09-11) |
| grok | GET cli-chat-proxy.grok.com/v1/billing?format=credits and GET cli-chat-proxy.grok.com/v1/user?include=subscription with OAuth token from ~/.grok/auth.json (creditUsagePercent, currentPeriod weekly reset); polled every 60 s | -32003 rate limit error, "You've hit the rate limit for your plan. Try again later.", "Rate limited (429)", StopFailureKind::RateLimit (xai-org/grok-build) | --debug-file grok.log per session; ~/.grok/sessions/<encoded-cwd>/prompt_history.jsonl gives prompts and session id | verified by source and proxy endpoint |
Why not Claude Code's status line JSON (rate_limits.five_hour.used_percentage):
on 2.1.268 the custom statusLine Leg passes through --settings did not
run, so the endpoint poll is the source. Leg still writes a statusLine
entry that records the same fields, so the moment a build honours it the poll
becomes a fallback.
For Codex, Leg does not infer availability from a lower percentage: only an
explicit available answer from the backend clears an earlier wall. The board
labels each bar as <n>% used and marks an old reading as stale rather than
presenting it as current.
LEG_WARN_PCT) the session card turns
amber, the event log names the next option, and the terminal bell rings once.StopFailure hook fires with rate_limit. codex: the
rollout reports usage_limit_exceeded. agy: the log says
RESOURCE_EXHAUSTED. The account is marked walled until the reset the CLI
reported (or the soonest known window reset).git diff --stat, dirty files, files edited this session, recent
commits, why it stopped) and runs context-handoff-bundle save --repo-local
with one slug per session, updated in place (--update <slug>) at every
checkpoint, about every two minutes while the session is active, and at
every warning, limit and hand-off. If the session maintained .leg/SYNTHESIS-<session-id>.md,
Leg inlines it into the resume file as a ## Synthesis section ahead of the raw dump.claude --resume <id> --model <alias>),
and another claude login you added with leg accounts add, which sees the
same transcript through the projects junction the account carries
(claude --resume <id> under that login's CLAUDE_CONFIG_DIR). A weekly
or Fable wall on one login then continues on the other with the
conversation it already had, and the timeline says kept the conversation. Every other rung starts in the same terminal with a short
pointer prompt:
read .leg/RESUME-<session-id>.md (the context-handoff-bundle load
output, the ## Synthesis section if present, and the reason for the switch),
check git status and git diff, continue, do not ask the human to restate
the task. The pointer prompt directs the next agent to read Synthesis first,
treat ruled-out approaches as settled, and start from the top-ranked next step.
The same text is copied to .leg/RESUME.md, the file people open by habit, and
both are stamped with the commit and the live terminals they describe.
claude "<prompt>", codex "<prompt>" and agy -i "<prompt>" all open the
normal interactive session with that first turn.leg ladder
to do it from a shell, or Settings to set the default copied by new
terminals. A rung whose CLI is missing, whose wall has not reset, or that
shares the window that is already out, is skipped with its reason.waiting for <agent> at <time>. Ctrl-C (or End on the row) quits with exit 3 instead.You can force a handoff any time: the Hand off now button on the row, or
leg sessions handoff <id>. Verified on this machine: leg claude opened
the real Claude Code TUI with Leg's hooks firing into the session log, the
usage poll recorded 36 % of the 5h window and 74 % of the 7d window, the
warning fired at 96 % of the 7d window and named codex as the next option, and
a limit saved the bundle, stopped claude and started codex in the same
terminal with the pointer prompt. A real StopFailure arrived on 2026-09-11
and is kept at fixtures/live/claude/limit-rate_limit.json; to drive the path
on demand, leg sessions simulate-limit <id> sends the same StopFailure
rate_limit payload Claude Code would send through Leg's hook: verified end
to end on a haiku session, the hook set the limit, the runner saved the
bundle, stopped claude and started codex, which read .leg/RESUME.md on its
first turn. The simulated wall clears after two minutes and is never kept as
evidence. The first real StopFailure was saved that way, with secrets
scrubbed, at fixtures/live/claude/limit-rate_limit.json, and the claude docs
row flipped to observed-live (node scripts/live-limits.mjs). The same
capture is wired for codex usage_limit_exceeded and agy RESOURCE_EXHAUSTED;
no payload for either has been kept yet.
Terminals started by this version can change their ladder while they run: Settings holds the default for new terminals, and Details on a row edits the copy that terminal is running. An older terminal stays on the order it started with; its row says a restart is needed and can save the desired default for the next launch. A normal agent exit ends the terminal. It does not trigger a handoff.
With the portable harness enabled (leg harness enable,
off by default), the hand-off also prepares the destination's environment
before it starts: the source agent's global rules, identity, hooks, skills,
subagents, slash commands, MCP servers and permissions, rendered into the
destination's own files as far as it can represent them. The terminal then
reads, for instance, codex harness partial · 8/8 components, 3 dropped · 1 file(s) written, and the terminal's details list what was dropped and why.
The source is fingerprinted so an unchanged environment costs a few stat
calls; a policy (warn, sync, strict) says whether a hand-off may write,
and strict refuses a destination it cannot make safe. Credentials never move
and which login runs stays the account layer's decision.
Two agents in one working tree write over each other's files. So when you
start leg codex in a checkout where leg claude is already live, the new
session gets its own git worktree, <repo>/.leg-worktrees/<session-id> on
branch leg/<session-id>, cut from the branch the checkout has out, and the
terminal prints one line saying where it is. The agent starts there; the card,
the usage tracking and the handoff work the same. --no-worktree shares the
checkout on purpose (Leg takes the flag out; the agent never sees it).
The card of a session with its own worktree has a Land button. It sends
the branch through the merge queue: whatever the agent left uncommitted is
committed on the branch, the branch is rebased onto its base, the repo's test
command runs (package.json test, pytest, or none with a warning), and the
base is fast-forwarded, never merged. When a step fails nothing lands and the
card says why: rebase-conflict with the files, tests-red with the end of
the output, dirty-trunk when the checkout has local changes the landing
would overwrite. Local changes it would not touch are left alone. The
landed-on-trunk list says which terminal landed each commit. Remove safely
prunes a finished session only when its worktree is clean and its branch is
already on the base. Remove record is a separate visible button with a
confirmation: it removes only Leg's saved session record and deliberately
keeps the worktree, branch, unmerged commits, and dirty files.
Verified live on 2026-09-11 with three haiku sessions in a throwaway repo. The
first stayed in the checkout; the second and third each got a worktree and
appended a line to README.md. Land on the second, clicked on the real board,
ran the repo's tests and fast-forwarded main; Land on the third bounced with
rebase-conflict on README.md and kept its commit on its branch; the
landed-on-trunk list named the second terminal (the screenshot above).
Off until you run it. leg share on binds the board to your Tailscale
address (or --bind lan, or an address you name) and gives every human their
own name and token; until then the board stays on 127.0.0.1 and there is no
token at all.
leg share on your own link, printed once
leg share add sam sam's link, printed once (a guest)
leg share add dana --role operator dana runs cards, not this machine
leg share who is on the board (never a token again)
leg share rotate sam sam's old link stops working
leg share off back to 127.0.0.1; every link stops working
There are three roles. owner is everything: the machine's settings, the harness, every terminal, the cards, the history index and the audit trail. operator is the pipeline board and their own terminals: they add, run, approve, reassign and kill cards, and they never see this machine's settings, its home path, its repository paths, its conversation index or the audit. guest is the terminals lane, read-only and redacted, with Request handoff as their only button.
A token is kept as a sha256 hash, so a lost link is re-issued, never re-read. The board takes the token out of the address bar and keeps it in the browser. Your own browser on this machine needs no token.
What another human sees is the Terminals region, read-only. Each panel says
whose terminal it is. On a panel that is not theirs there is no prompt, no file
name, no path, no bundle and no event log; what stays is the agent and session
tail, the status word, the sentence read-only: wes owns this terminal,
repo@branch, the worktree line, the elapsed clock, and one button,
Request handoff. The instrument head prints not shared in place of every
percentage. A request lands on the owner's panel as sam asked to take this terminal at 11:04 PM with Approve sam and Dismiss sam. The background
side of the board (cards, logs, the floor) stays the owner's alone. A terminal
belongs to the human who started it: LEG_PERSON=sam leg claude on the
same machine is sam's card, not yours.
The security pass that goes with it: every /api route needs a token, the
event stream included; twenty wrong tokens from one address and that address
waits a minute; one identity gets 600 requests a minute; a guest gets 403 on
everything that is not theirs; and the tests send a bad and a missing token to
every route. Verified live on 2026-09-11: two terminals on one machine, one wes's and
one sam's; sam's board showed wes's card with the prompt hidden and only
Request handoff, and sam's request reached wes's board (~/.leg/board.log:
"hand-off requested … by sam").
Off unless you hand it a certificate. Leg does not issue one: a self-signed pair teaches everyone on the board to click through a warning, which is worse than plaintext on a network that is already private. On Tailscale the pair is one command:
tailscale cert <machine>.<tailnet>.ts.net
leg share on --tls-cert <machine>.<tailnet>.ts.net.crt --tls-key <machine>.<tailnet>.ts.net.key
LEG_TLS_CERT and LEG_TLS_KEY do the same without writing the paths into
share.json, and they win over it. With a pair configured the shared address
serves https and every printed link says https://; the companion listener on
127.0.0.1, which exists so this machine's own browser needs no token, stays
plain http, because the certificate is for the shared name and loopback traffic
never leaves the machine. A pair that is half-configured, missing, unreadable
or empty stops the board with exit 3 rather than quietly serving plaintext.
Renew the pair, then leg down && leg up to pick it up.
Settings → Audit trail is one list across every terminal and every card,
newest first: hand-offs, landings, approvals, reassignments, kills, each with
the person or agent that did it, the repo and the time. Filter by person or by
kind. It reads what the ledger already recorded, so nothing new is stored, and
it prints how much it read beside the answer (14 terminals and 3 cards, 812 events read) so an empty trail cannot be mistaken for a quiet week. Owner
only: the trail names repositories and people.
leg digest (default window 8 hours; --since 2d, --since 30m, or an ISO
time; --json for the record; GET /api/digest?since= on the board, owner
only) is the trail read the other way round: grouped by repository, what
needs you first. The first line is the volume it was read from (3 terminals, 2 cards, 1 landing, 412 events read (66 sessions and 9 cards on disk)), then needs you: a live terminal waiting on a question, a card
parked for a human, a card that failed, a terminal that was lost, in that
order and each with how long ago. Then one block per repository: every
terminal that moved in the window with its login and model, state, turns,
files, commits ahead and the events worth a line (a wall, a hand-off and
whether it kept the conversation, an all-out wait, the end), every card with
its last event, every landing with who pressed Land, and last the walls
standing right now with their reset times. Nothing new is recorded; a window
with nothing in it says so with its counts.
leg <agent> opens it; leg open reopens it; leg down stops it.
claude 63% 6:01 PM, codex back Sat 10:11 PM, agy no figure, grok no reading. Capacity and models
opens a drawer holding the full login panels: the 5h and 7d rails, the reset
and how long that is, where and when Leg read the number, at the wall with
its back <day time>, and on the claude panel a rail of model chips
(fable 63%, opus 12%, a walled one saying out until 9:14 PM). The
sticky instrument head is still what /floor puts at the top.repo on branch, dirty <n> and
ahead <n>, agent/model, quiet <n>m, and own worktree, from main when
the session cut its own worktree; then the first prompt as a
button, exactly one sentence (the highest-ranked thing true about the
terminal), an also: disclosure naming the rest, the files as
comma-separated text, the binding bucket as 63% of the fable week, and an
elapsed clock with the session tail. Two live sessions in one repo touching
the same file print codex (codex-99ab) is changing src/server.mjs in another checkout; whoever lands second rebases on both rows. A Claude row
parked at a permission or idle prompt rises to the top, says waiting on you in place of its status word, prints the question verbatim, and puts a
count in the browser tab ((2) Leg, with a dot on the favicon).
After a Land the sentence is landed on <base>, <7-char sha>, <n> files, +<added>/-<removed>, or Land was attempted at <time> onto <base> and bounced: <first line of the reason>. The branch still holds every commit; nothing was lost.repo@branch chip, and
when plus who. A commit a Land put there says landed by <agent> (<id tail>).at its usage limit, back 11:40, shares the window that is out, buys nothing, not installed on this machine).
leg sessions handoff <id> --to claude/default/opus is the same choice from
a terminal. If the rung you picked walls between the click and the hand-off,
the work still continues down the ladder and the terminal says which one
took it instead.leg history continue command to copy where the agent
can resume by id. The same drawer lists every checkout Leg can see. Then
Settings. Background tasks are not down here: a live card is a row in
the Background panel directly under Terminals (see below), and only the
finished ones collapse into a ledger line beside these counts.The board reads ~/.leg/sessions/*/session.json over server-sent events; a
session whose runner process is gone is marked lost, never shown as live.
Claude Code, Codex, Grok, Antigravity and Copilot each keep their history in
their own place. leg history is one list over all of them, whether or not
Leg started the conversation, and leg worktrees is one list over every
checkout: git's, Leg's own, and the ones those conversations ran in.
leg history newest first, every agent; --provider, --repo, --search, --json
leg history show claude:0fc5 where it ran, its last messages, whether Leg can continue it
leg history continue claude:0fc5 leg claude --resume <id> in that folder, supervised like any session
leg worktrees path, repo, branch, exists, uncommitted, owner, conversations, stale
Nothing moves: each agent's store stays where it was, Leg writes only its own
~/.leg/history/index.json, reads transcripts from their head and tail, and
opens messages only when you open a conversation. A session Leg started and
the same conversation in the agent's store are one row, marked leg; the rest
are external. On a shared board the whole group is the owner's. The support
matrix (which agents list, show messages, continue) and every file read are in
docs/history.md.
Optional. leg accounts add claude work creates
~/.leg/accounts/claude/work, junctions your hooks, skills, agents,
commands, plugins, rules, scripts, output-styles, tools and
projects into it, copies settings.json, CLAUDE.md and the status-line
scripts (refreshed from your real ~/.claude before every launch), and prints
one line to paste:
$env:CLAUDE_CONFIG_DIR='C:\Users\you\.leg\accounts\claude\work'; claude auth login
projects is Claude Code's conversation store, so the second login sees the
same conversations and the same auto-memory as the first, and a hand-off from
one login to the other keeps the conversation: the terminal moves to work
with claude --resume <id> and no bundle prompt. That is the whole point of a
second 20x login when the Fable or the weekly window on the first one is out.
An account made by an older Leg gets the junction the next time it starts.
Claude Code writes that directory; Leg only reads it.
Same for codex (CODEX_HOME; config.toml, AGENTS.md, skills, prompts,
rules, plugins, agents, hooks, memories shared; a codex hand-off
still takes the bundle, because codex resume under a second CODEX_HOME
has not been observed). agy 1.2.0 has no config-directory override, so it
stays one account. Only the login lives in the account directory;
leg accounts rm removes the junctions and the directory and never touches
your real home.
The terms, as published (effective dates below):
Owning two paid subscriptions is not named as prohibited by either. Rotating to
a second account of the same vendor because the first one is rate-limited sits
close to OpenAI's "circumvent any rate limits" wording and Anthropic's
"circumvent product guardrails". Leg's default chain switches vendors
(claude → codex → agy), which is plainly fine. Same-vendor rotation only
happens after you run leg accounts add; that is your call.
~/.claude/settings.json, ~/.claude.json,
~/.codex/config.toml, agy's files, your repo's settings. Claude Code gets
hooks through a per-session --settings file under ~/.leg; codex and
agy get nothing injected.~/.claude/projects,
~/.codex/sessions, ~/.grok/sessions, ~/.gemini/antigravity-cli,
~/.copilot/session-state) for leg history; the index it builds lives
under ~/.leg/history/, and no SQLite file is ever opened.leg harness enable (the portable harness,
off by default): the destination client's global rules file
(~/.codex/AGENTS.md, ~/.gemini/GEMINI.md, ~/.claude/leg-rules.md plus
one @ line in CLAUDE.md), marked leg harness regions inside the
client's own config files, per-skill directory links, and one file per
subagent and slash command. Every file carries GENERATED by Leg harness,
every overwrite is backed up under ~/.leg/harness/backups, a hand-edited
file is skipped and named, the source client is never written, and no
credential ever moves (an MCP key becomes ${NAME}). leg harness disable
stops it and removes nothing..leg/ (session notes, RESUME.md and one
RESUME-<session-id>.md per hand-off),
.context-handoffs/ (the bundles) and .leg-worktrees/ (a second
session's worktree), all added to .git/info/exclude, plus the
leg/<session-id> branch of a session with its own worktree. Landing
fast-forwards your branch; nothing is ever pushed.ANTHROPIC_API_KEY,
ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL, ANTHROPIC_CUSTOM_HEADERS,
OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_API_BASE, GEMINI_API_KEY,
GOOGLE_API_KEY, GOOGLE_GEMINI_BASE_URL, GOOGLE_GENAI_USE_VERTEXAI,
GOOGLE_GENAI_USE_ENTERPRISE, GOOGLE_CLOUD_PROJECT,
GOOGLE_CLOUD_LOCATION, GOOGLE_APPLICATION_CREDENTIALS, CLAUDECODE,
CLAUDE_CODE_*, CLAUDE_EFFORT, and CLAUDE_PLUGIN_DATA. CLAUDE_CODE_*
does not include CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS: Leg sets that one
to 0 for a detached Claude print session.api.anthropic.com for the usage numbers. The ledger scrubs bearer tokens
and key shapes from every line regardless.leg uninstall --yes: removes ~/.leg (sessions, usage, extra
account directories with their junctions, v0.1 cards, the board pidfile) and
nothing else; then npm rm -g @ucsandman/legcli.leg claude|codex|agy|grok [agent args…] the interactive agent, board alongside, handoff on limit
[--no-worktree] share the checkout with a live session instead of a worktree
leg sessions ls [--json] every session and its usage
leg sessions show|events <id>
leg sessions handoff|end <id> same as the board buttons
leg sessions handoff <id> --to <agent>[/<account>] hand off to a destination you name
leg sessions rm <id> forget an ended session
leg sessions simulate-limit <id> the real limit path without a real wall (claude, agy, grok)
leg history [ls] [--provider p] [--repo r] [--search q] [--managed|--external] [--live] [--all] [--json]
every conversation on this machine, Leg's own and the agents' own (read only)
leg history show <id> [--messages n] [--json] | continue <id> [agent args…] | refresh [--full] | providers
leg worktrees [--repo <path>] [--no-dirty] [--json] every checkout: git's, Leg's, the conversations' (read only)
leg digest [--since 8h|2d|<iso>] [--json] what happened while you were away: needs-you first, then every
terminal, card, landing and wall in the window, by repository (read only)
leg accounts ls logins and their 5h/7d usage
leg accounts add <claude|codex|grok> <name> | rm <agent> <name> | terms
leg harness status|inspect|check|explain|history [--json] the portable harness, read-only
leg harness enable [--source claude|codex] [--policy warn|sync|strict] [--yes]
leg harness sync [--to codex,agy] [--force] [--dry-run] | diff <client> | doctor
leg harness capture [claude|codex] | source <client> | policy <mode> | disable
leg license the license on this machine, or where to buy one
leg license activate <key> | deactivate | refresh (refresh renews a Team key)
leg adapter list|show <n>|check <n> every adapter, built-in and custom
leg adapter template [--name n] a starter spec to fill in
leg adapter add <file.json> | rm <n> any CLI as a card agent, from JSON
leg share who is on the board (off by default; Team plan)
leg share on [--bind tailscale|lan|<addr>] [--port N] [--tls-cert <f> --tls-key <f>] | off
leg share add|rotate|rm <name> [--role owner|operator|guest] one link per human, printed once
leg open | down | status the board
leg uninstall [--yes]
Environment, all optional: LEG_HOME (default ~/.leg), LEG_PORT
(4747), LEG_ACCOUNT (start on a named login), LEG_WARN_PCT (85),
LEG_NO_HANDOFF=1 (warn and record, never switch), LEG_NO_OPEN=1 (do not
open the browser), LEG_USAGE_POLL_MS (60000), LEG_CLAUDE_ARGS /
LEG_CODEX_ARGS / LEG_AGY_ARGS / LEG_GROK_ARGS (extra args for a leg Leg starts after
a hand-off, e.g. -m gpt-5.3-codex-spark), LEG_CLAUDE_BIN,
LEG_CODEX_BIN, LEG_AGY_BIN, LEG_GROK_BIN, LEG_CHB_BIN, LEG_PERSON (whose
terminal this is when the board is shared), LEG_TLS_CERT / LEG_TLS_KEY
(serve the shared board over https; they win over share.json),
LEG_RATE_MAX (600 requests a minute per human) and LEG_RATE_MAX_FAILURES
(20 wrong tokens per address).
Version 0.1 was the other way round: you dropped a task card on the board and Leg ran the agents headless in a git worktree, one per card, with a fallback chain, path leases, a scheduler and a merge queue. All of that still runs, and a card is now a terminal you are not sitting at: same register, same one sentence, same ladder, same bundle. Live cards are rows in the Background panel directly under Terminals; finished ones fall into one ledger line that opens.
Starting one is a single field. Run in the background: takes the task, and
the sentence under it is inferred with its nouns as buttons: the repo of the
terminal you were last in, the saved ladder, and the build workflow, which
stops with its changes in the card's worktree and does not merge them.
More settings is the full form, with the Build, test, and merge and
Factory workflows that include an automatic land station, plus fallback
agents, permissions, approval gates, turn caps, leases, trunk, merge method,
tests, title, and scripted test and demo adapters. End, and keep going as a
card on a terminal's End row makes a card out of the work in front of you;
Take over on a card gives you the command that turns it back into a
terminal.
leg up boots the board with the scheduler and merge queue and streams
redacted logs; leg card add --repo <path> --task "<t>" --chain claude,codex --queue
creates a card; presets build, build-land, factory; station kinds
agent, test, land, human.claude -p --output-format json --permission-mode <m>, codex exec --json -s <m> -C <worktree>,
agy -p --output-format json --mode <m> --add-dir <worktree>,
grok --prompt-file <f> --output-format json --permission-mode <m> --cwd <worktree>; fake, fake-claude, fake-codex, fake-agy for tests and
demos.leg adapter template --name muse > muse.json, fill in the command and its
flags, leg adapter add muse.json, then --chain muse,claude. leg adapter check muse prints the exact command line a leg would run before one does.
See docs/adapters.md. A custom adapter
runs cards; it is not an interactive leg <agent> terminal, because that
needs a usage tap and a wall signal, which only the four above expose..leg/DONE hands off with a bundle to the next adapter in the same
worktree; a land station rebases, tests and fast-forwards trunk or bounces
the card with the failure in the bundle..env: OpenClaw Workboard
(LEG_SYNC_WORKBOARD=1) and DashClaw (LEG_SYNC_DASHCLAW=1).The full v0.1 story, with the fake-limit demo and the real claude→codex run, is in docs/concepts.md, docs/DEMO.md, docs/real-run.md and docs/board-guide.md.
Leg binds 127.0.0.1. leg share on is the supported way to listen
anywhere else: it binds your Tailscale or LAN address and every human gets
their own token (see More than one human). Without
share, setting LEG_BIND to a non-loopback address needs LEG_TOKEN too,
or the server refuses to start (exit 3), and requests then need
Authorization: Bearer <token>. Tokenless owner access also requires a
loopback hostname (127.0.0.1, localhost, or [::1]), which prevents a
DNS-rebound hostname from inheriting local access. Either way there is no TLS.
leg open, or visit http://127.0.0.1:4747.
~/.leg/board.log has the server's output.claude auth login), the stored token
expired (start claude once, it refreshes), or the usage endpoint answered
with something Leg does not recognise. The card says which. The wall is
still caught through the hook; only the percentages are missing.lost: the terminal that ran leg <agent> is gone
(closed, crashed, machine slept through a kill). Remove it from the board.leg claude typed inside a Claude Code shell works;
the parent's CLAUDECODE markers are stripped so the child starts.npm install dies with edgesOut (clone only): the global npm is older
than Node; run npx --yes npm@latest install once.More in docs/faq.md.
| guide | read it when |
|---|---|
| Getting started | you want leg claude running in five minutes |
| Concepts | sessions, accounts, bundles, and the v0.1 cards, stations, chains and leases |
| Board guide | every word, number and button on the board explained |
| Configuration | environment variables and options |
| Portable harness | carrying rules, hooks, skills, agents, commands and MCP servers to the agent a hand-off lands on: what moves, what does not, policies, ownership, secrets |
| History | leg history and leg worktrees: every conversation and checkout on this machine across agents, the support matrix, what is read and what is written |
| Adapters | what each CLI exposes and how Leg attaches to it |
| CLI contracts | exact argv per CLI and the limit-signal table with sources |
| FAQ | a question the others did not answer |
| Demo and real run | the v0.1 handoff, fake and real |
| Vocabulary | statuses, outcomes and event types |
| Roadmap v2 | where this is going |
| Reuse and deviations | what was ported and every place the plan changed |
| Website | the public page: static HTML in site/, preview with python -m http.server 4780 --directory site, deployed to Vercel from that directory; PRODUCT.md and DESIGN.md at the root carry its brief and tokens |
Issues and pull requests are welcome. Read CONTRIBUTING.md for the dev setup and the rules (zero runtime deps, argv spawns only, no bypass flags, a privacy check on every commit). Security reports go through SECURITY.md.
npm install
npm test # node --test + privacy check
npm run lint
Any real agent session started only to test Leg runs on the cheapest model
(leg claude --model haiku); the live checks in test/ never start one.
Maintainer releases use npm trusted publishing with no NPM_TOKEN. Bump the
package, lockfile, site metadata and release notes, then push main.
npm version (and npm run sync-alias) writes packages/leg-agents to the
same version and pins @ucsandman/legcli to it; npm test fails if they
drift. CI waits for the Ubuntu and Windows test matrix, validates both
@ucsandman/legcli and leg-agents against npm, and publishes each only
when that version is missing and newer than the stable latest. Existing
versions skip cleanly; older, prerelease, lockstep, and registry-error cases
fail the job. The npm trusted publisher is bound to ucsandman/legcli and
.github/workflows/ci.yml (bind leg-agents the same way). Publication
uses --provenance=false.
Parts of the runner, ledger and git snapshot were ported from a private
repository that was MIT licensed (see NOTICE and
docs/REUSE.md), with chat identifiers, machine paths and
personal names removed. The test suite runs a privacy check on every commit,
and the fixtures store home paths as ~.
Leg is commercial software under the Leg License Agreement. It ships as readable JavaScript so you can see what it does on your machine, and you may modify it for your own use, but not redistribute it or work around the license check. Versions 0.2.0 and 0.3.0 were published under MIT and remain available. The version in this source tree is 0.15.0; see npm for published versions and CHANGELOG.md for release notes.
The first 14 days are a free trial: install it, type leg claude, and
everything in both plans is on, with no key and no card. The clock starts on
your first session and leg license status shows what is left. After that
it needs a license: Personal, $79 once, one human on any number of
machines, every release for 12 months and the version you have keeps working
after that; Team, $12 per seat per month, Personal plus leg share for
more than one human on the board. Buy at the site, then
leg license activate <key>. There is also a 30-day money-back
guarantee after buying, no reason required. A key is a signed token
checked offline with the public key in src/license.mjs; only a Team key
renewal talks to the site. The bare agent CLIs are never affected by any of
this; only what Leg adds is licensed.
140 commits
JavaScript
78.6%
HTML
18.3%
CSS
3.1%