CLI reference
All commands assume a lazybox binary on your PATH — installed via Homebrew
(brew tap AntoineToussaint/lazybox && brew trust AntoineToussaint/lazybox && brew install lazybox) or a contributor source build (see the
Quickstart). From a source checkout without a
built binary on PATH, substitute cargo run -p lazybox-tui-boot --.
An lb binary ships alongside lazybox — a short alias with the identical
entrypoint, so every command below works as lb … too.
Make targets
Section titled “Make targets”For day-to-day development, the Makefile wraps the common flows.
| Target | What it does |
|---|---|
make setup |
One-time online prep: fetch the checksum-verified pinned Zig 0.16.0 to ~/.cache/lazybox/zig/, cache the Ghostty source and the locked Cargo graph, and prebuild the native terminal dependency so later builds run offline |
make run |
Build and run lazybox |
make build |
Build the workspace |
make release |
Build in release mode |
make test |
Run the test suite |
make lint |
Run clippy |
make fmt |
Format the workspace |
make dev |
Run a side-by-side instance against LAZYBOX_HOME=~/.lazybox-dev |
make run-fresh |
Run with state wiped and the setup wizard re-run |
make run-test |
Run the seeded throwaway instance |
Direct cargo
Section titled “Direct cargo”| Command | What it does |
|---|---|
cargo build |
Build |
cargo run -p lazybox-tui-boot |
Build and run the TUI binary |
cargo nextest run --workspace |
Run all tests with the repository’s per-test timeout policy |
cargo clippy --workspace --all-targets -- -D warnings |
Lint, warnings as errors |
If you build with cargo directly (no make), put Zig 0.16.0 on your PATH
first.
lazybox run modes
Section titled “lazybox run modes”| Command | What it does |
|---|---|
lazybox |
Default — in-process daemon plus TUI |
lazybox --help, -h |
Print the command overview and exit |
lazybox --version, -V |
Print the version and exit |
lazybox --fresh |
Wipe ~/.lazybox/v2/state.db and re-run the setup wizard |
lazybox --test |
Throwaway tempdir repo with one seeded workspace, no GitHub |
lazybox --connect [socket] |
Connect to a remote daemon over a Unix socket; with no path, defaults to ~/.lazybox/run/daemon.sock |
lazybox --connect-relay <box-id> --relay <addr> --box-key <hex> |
Attach a TUI to a box reached through a rendezvous relay (encrypted, ciphertext-only tunnel); see --connect-relay |
lazybox --workspace <key> |
Preselect a workspace on startup |
lazybox --session <id> |
Preselect a session on startup |
lazybox scan [ROOTS...] [--depth N] [--hidden] |
Find existing git clones and linked worktrees without modifying them |
lazybox worktree list |
Report every managed worktree with its size and orphan reasons, plus the on-disk and reclaimable totals |
lazybox worktree gc [--force] [--dry-run] |
Reclaim the safe orphaned worktrees (confirms first unless --force) |
lazybox hook-ingest --backend-key <key> |
Internal: forward an agent lifecycle hook payload (stdin JSON) to the daemon — lazybox injects this into spawned agents’ hook config; not typically run by hand |
lazybox scan
Section titled “lazybox scan”lazybox scan is a read-only inventory of git checkouts created outside
lazybox. Pass one or more roots directly, or configure scan.roots and run it
with no positional arguments:
lazybox scan ~/code ~/work --depth 5lazybox scan --hidden| Option | Effect |
|---|---|
[ROOTS...] |
Directories to walk; overrides scan.roots when present |
--depth N |
Maximum levels below each root; overrides scan.max_depth |
--hidden |
Include dot-directories, which are skipped by default |
Results are ordered by recent commit activity and identify the branch, path,
linked worktrees, dirty checkouts, and any checkout lazybox already tracks.
Lazybox’s own managed worktree directory is excluded. The command is read-only —
it only reports what it finds. To import a discovered checkout in place, use
the in-app import flow: press x i inside lazybox to scan and link a checkout
as a workspace without moving it.
lazybox worktree
Section titled “lazybox worktree”Every workspace opens an isolated git worktree. Over time some are left behind —
their PR merged, their branch gone, their session removed — and the disk they
hold adds up. lazybox worktree inspects and reclaims the worktrees lazybox
provisions, without touching checkouts you created yourself.
lazybox worktree list # read-only reportlazybox worktree gc --dry-run # show what gc would reclaimlazybox worktree gc # reclaim the safe orphans (confirms first)list prints every managed worktree with its branch, size, and orphan reasons
(tagged DIRTY / UNPUSHED / safe-reclaim), then the totals that make a leak
visible: bytes on disk, bytes auto-reclaimable
across the safe orphans, and bytes held by orphans that need a look first (no
backing bare clone, or uncommitted, unpushed, or locked). Bare clones under
repos/ are not counted.
| Command | What it does |
|---|---|
lazybox worktree list |
Read-only inventory of every managed worktree and the disk totals |
lazybox worktree gc |
Reclaim the safe orphaned worktrees, confirming first |
lazybox worktree gc --dry-run |
Report what gc would reclaim without deleting anything |
lazybox worktree gc --force |
Reclaim without the confirmation prompt |
gc only ever deletes a worktree that is flagged orphaned and passes the
safety gate — it never removes one with uncommitted or unpushed work, a lock,
or a live session. Orphans that need review are left for the in-app worktree
inspector — open the Settings palette (,) and choose Inspect worktrees…,
which can force a per-row reclaim. gc also refuses to run while lazybox is
open, since a standalone reap can’t see the daemon’s live sessions; quit lazybox
first, or reclaim from the inspector.
lazybox server
Section titled “lazybox server”| Command | What it does |
|---|---|
lazybox server start |
Start the daemon in the foreground (blocks until shutdown — run it in tmux, nohup, or a service unit) |
lazybox server stop |
Stop the daemon |
lazybox server status |
Report whether the daemon is running |
lazybox server api [addr:port] [--insecure-no-auth] |
Start the HTTP API gateway |
lazybox server api auth
Section titled “lazybox server api auth”The API gateway refuses to start without an auth decision: either set
LAZYBOX_API_TOKEN (clients then send Authorization: Bearer <token>), or
pass --insecure-no-auth to explicitly serve unauthenticated (a warning is
printed).
The listen address resolves in order: the [addr:port] argument, then the
LAZYBOX_API_ADDR environment variable, then the default 127.0.0.1:8787.
The gateway has no built-in TLS, so non-loopback binds are refused. Forward the
loopback port through an encrypted SSH tunnel for remote use. Direct routable
transport requires future encryption and principal-scoped authorization.
POST /v1/commands waits for the command handler to finish before returning
{"ok":true,"completed":true}. Provider/domain outcomes still arrive as
normal events. Streaming requests are connection- and command-bounded, so a
client must reconnect after the documented stream limit instead of growing an
unbounded daemon queue. GET /v1/workspaces includes a warnings array when
an unreadable row was preserved and omitted from the decoded workspace list.
lazybox serve
Section titled “lazybox serve”lazybox serve dials out to a rendezvous relay and holds the connection
open, so clients can reach this box’s daemon behind NAT — no inbound
ports, DNS, or TLS certs. The relay brokers a client to the box by box-id and
forwards opaque Noise ciphertext; it executes nothing. The box proves possession
of its persistent Ed25519 identity when registering with a hosted relay.
lazybox serve --relay relay.example.com:9443| Option | Effect |
|---|---|
--relay <host:port> |
Relay address to dial (or the LAZYBOX_RELAY env var). Required. |
--insecure-no-auth |
Disable the end-to-end Noise channel for loopback testing. Never use this with an internet-facing relay. |
--box-id <id> |
Override the box-id. By default a persistent id is generated on first run and stored at ~/.lazybox/v2/box-id. |
--socket <path> |
Local daemon socket to bridge to (defaults to the standard daemon socket). |
Run the daemon (lazybox server start) alongside serve: each brokered
client is bridged to that daemon over the local socket.
The encrypted channel uses a persistent X25519 box key that the connecting
client pins with --box-key; the relay only sees ciphertext. The separate
Ed25519 box identity is used to prove which subscription a hosted relay should
check. --insecure-no-auth bypasses the encrypted channel and is only intended
for loopback testing.
The relay itself is a separate deployable (lazybox-relay, in crates/relay)
and is not part of the client installers.
lazybox slack
Section titled “lazybox slack”| Command | What it does |
|---|---|
lazybox slack init |
Interactively validate and store bot/app tokens, then join the configured anchor channel |
lazybox slack doctor |
Diagnose token, scope, and connectivity issues |
lazybox slack prune |
Archive stale per-workspace channels (Slack has no channel delete) |
lazybox slack prune options
Section titled “lazybox slack prune options”The command computes a plan, prints it, and prompts before archiving anything.
| Option | Effect |
|---|---|
--dry-run |
List what would be archived without touching Slack |
--yes, -y |
Skip the confirmation prompt |
--older-than DUR |
Only archive channels stale for at least this long (e.g. 7d) |
--workspace KEY |
Restrict pruning to one workspace’s channels |
lazybox account
Section titled “lazybox account”Link a box to a lazybox-platform organization with a one-time claim code:
lazybox account claim ABCD1234 --name my-dev-boxlazybox account status| Command or option | Effect |
|---|---|
account claim <code> |
POST the one-time code, box public key, and display name to /v1/devices/claim, then cache the returned non-secret organization/plan state |
--platform-url <url> |
Override the platform (LAZYBOX_PLATFORM_URL, cached URL, then https://platform.lazybox.ai) |
--name <name> |
Box display name; defaults to the host name |
account status |
Show the linked organization, plan, and cached entitlement without requiring a platform credential |
Claim codes and the box private key are never written to config.yaml.
lazybox device mint still succeeds for local/self-hosted use when unlinked,
but warns that a hosted relay may refuse the credential.
lazybox --connect-relay
Section titled “lazybox --connect-relay”Attach a TUI to a box reached through a rendezvous relay, rather than a local
Unix socket. It connects through the relay, runs the end-to-end Noise handshake
pinned to the box’s channel key, and drives the daemon over the encrypted,
ciphertext-only tunnel. The box side is served by lazybox serve.
lazybox --connect-relay <box-id> --relay relay.example.com:9443 --box-key <hex>| Option | Effect |
|---|---|
<box-id> |
The box to reach (first positional argument). Required. |
--relay <host:port> |
Relay address to dial (or the LAZYBOX_RELAY env var). Required. |
--box-key <hex> |
The box’s channel (X25519) key to pin the encrypted channel to. Required. |
--smoke |
Run a one-shot encrypted round-trip check and exit instead of launching the TUI. |
lazybox sandbox
Section titled “lazybox sandbox”Drive a remote dev-box’s lifecycle from the CLI. The box, its placement, and the
socket the connect forward carries come from the sandbox
config block; each verb’s flags override what’s set there. The same box is what
the sidebar r-spawn brings up lazily.
| Command | What it does |
|---|---|
lazybox sandbox ensure |
Provision the box (Terraform / provider apply) if it doesn’t exist |
lazybox sandbox wake (alias start) |
Wake a stopped box |
lazybox sandbox sleep (alias stop) |
Stop the box to stop billing compute |
lazybox sandbox status |
Report the box’s current lifecycle state |
lazybox sandbox connect |
Bring up the supervised forward carrying the daemon socket + workload ports |
lazybox sandbox rebuild |
Rebuild the box’s lazybox daemon in place |
lazybox sandbox destroy |
Tear the box down (confirms first unless --yes) |
Every verb accepts --worktree <key> to address a specific per-key box (default:
the one shared box). Verbs also accept the placement/credential overrides that
mirror the config block (--provider, --project, --region, --zone,
--template, --timeout-seconds, --terraform-dir, --deployment, --user,
--remote-socket, --local-socket, --ports, --service-account-key,
--impersonate-service-account, --gcloud-config-dir).
lazybox auth
Section titled “lazybox auth”Manage provider login credentials stored by lazybox (independent of gh auth).
| Command | What it does |
|---|---|
lazybox auth login [github] |
Log in via the GitHub OAuth device flow and store the token |
lazybox auth status |
Show the stored login status |
lazybox auth logout [github] |
Remove the stored token |
The provider defaults to github. Linear login is not yet implemented — set
LINEAR_API_KEY or install the linear CLI for now.
lazybox device
Section titled “lazybox device”Manage this box’s pairing identity and the credentials paired devices use to reach it over a relay.
| Command | What it does |
|---|---|
lazybox device box [--format base64] |
Show this box’s pairing identity (box id + public key) |
lazybox device mint --name <name> |
Mint a credential for a new paired device |
lazybox device list |
List paired devices |
lazybox device revoke <id> |
Revoke one device |
lazybox device token <id> |
Reprint a device’s pairing token |
lazybox workspace
Section titled “lazybox workspace”The agent-facing surface over the running daemon: lets a spawned agent (or a script) create a workspace in lazybox itself, not just act on the repo.
lazybox workspace create --issue owner/repo#7 --agent claudelazybox workspace create --name "spike auth" --scratch --repo owner/repo| Command / option | Effect |
|---|---|
workspace create |
Attach to a tracker record’s workspace — or create a scratch one — by sending CreateWorkspace to the daemon |
--issue / --pr / --ticket <ref> |
The record to attach to: owner/repo#N, a GitHub issue/PR URL, #N beside --repo, or a Linear key like ENG-45. Three names for one flag, each reading better at its call site |
--name <name> |
Workspace display name, for repo-less scratch work. Not combinable with a record |
--scratch |
Allow a bare --name under a tracker-backed project — it says this really is scratch work with no record behind it |
--project <key> |
Target an existing project by key |
--repo <owner/repo> |
Target a repo (an alternative to --project) |
--agent <id> |
Spawn this agent into the resolved workspace so a live session lands in it |
--cwd <path> |
Directory used to infer the project when neither --project nor --repo is given (default: the process cwd) |
--socket <path> |
Daemon socket to send to (defaults to the standard socket) |
The project resolves from --project / --repo, else it is inferred from the
checkout at --cwd — so an agent running inside a worktree needs only the
record. Unlike fire-and-forget hooks, a failure exits non-zero: the caller
asked for a workspace and is told if the daemon was unreachable or the project
couldn’t be resolved. The message goes to stdout, because lazybox
redirects its own stderr into the log file.
This verb refuses an argument it does not know, rather than ignoring it —
including a flag that is given with no value, and the launch flags (--fresh,
--test, --demo, --workspace, --session) that belong to lazybox
itself. A dropped flag is indistinguishable from an honored one: --tier xhigh used to report success and start the agent on its default model. There
is no tier flag — put a model:<token> label on the record instead.
x x archives a workspace: the row is deleted and the key tombstoned, so the
next poll skips it and the record appears in no mailbox. These two verbs are
what reverse it (the TUI’s archive browser, x U, is the same surface).
lazybox workspace archivedlazybox workspace unarchive owner/repo#40| Command / option | Effect |
|---|---|
workspace archived |
List the archived keys, each followed by the keys its row absorbed |
workspace unarchive <ref> |
Drop that record’s tombstone: owner/repo#N, a GitHub URL, #N beside --repo, or a Linear key |
--key <workspace-key> |
Name a raw workspace key instead, as workspace archived prints it |
--repo <owner/repo> |
Repo used to resolve the bare #N / N forms |
--socket <path> |
Daemon socket to send to (defaults to the standard socket) |
A PR row stands in for the issues it closes, so archiving it tombstones them
too; restoring the PR key takes that whole set back out. The row itself returns
on the next poll — or immediately from a following workspace create --issue <ref>, which an archived record refuses outright.
lazybox work
Section titled “lazybox work”The task/plan store from a shell — the supported surface for an agent that
cannot receive MCP tools (Codex, Cursor, --strict-mcp-config). Every
subcommand reaches the same daemon code as the create_work / my_work /
update_work / work_status MCP tools, so a shell and an agent are never told
different things about the same row.
lazybox work mine # what you own, are owed, have queuedlazybox work new "Fix the flaky clone test" \ --brief "Objective: ... Done when: ..." --to github:acme/widget#42lazybox work set <id> underwaylazybox work set <id> held --detail "waiting on a base-branch decision"lazybox work done <id> --summary "Landed in #1931" --artifact findings.mdlazybox work status --json| Command / option | Effect |
|---|---|
work mine [--all] |
Three lists: work you own, work a sibling owes you, work you filed that nobody owns. --all includes finished work |
work new <title> |
Mint a unit of work and print its id |
--brief <text> |
Objective, done-criteria, boundaries, output shape. Delivered to the owner’s session |
--to <workspace> |
Assign it, and hand the brief over now |
--no-deliver |
Record the assignment without poking the owner (needs --to) |
--plan <id> / --parent <id> |
Put it on a plan, or nest it under another unit of work |
--link <ref> |
Repeatable. owner/repo#N or an issue URL for a tracker record, ws:<key> for a workspace, or an http(s) URL |
work set <id> <lifecycle> |
underway, awaiting-answer, held, failed or canceled |
--detail <text> |
The question for awaiting-answer, the blocker for held, the cause for failed. Required for the first two |
work done <id> --summary <text> |
Complete it. The summary is required: it is what the requester reads instead of your scrollback |
--artifact <name> |
Repeatable. A file you wrote into .lazybox/artifacts/, carried by reference |
work status [--plan <id>] |
A plan’s done/total roll-up, the workspaces it spans, and the open work on no plan |
--workspace <key> |
Act as this workspace instead of the current session |
--json / --socket <path> |
Structured output; daemon socket to use |
The workspace comes from LAZYBOX_SESSION_KEY, which lazybox injects into
every session’s PTY at spawn, so inside a session these commands need no
arguments. Pass --workspace to act on another row.
A unit of work is not a tracker record. It carries an immutable id for its
whole life and points at issues, PRs and workspaces through --link, so an
issue→PR fold rewrites a link and never an id. Assigning work also links its
owner’s workspace, which is what gives a plan spanning several repos its member
list.
Two behaviours worth knowing before scripting this. A terminal state refuses
every further move — a completed, failed or canceled unit of work cannot be
reopened or re-completed, so a replaced session reporting late cannot overwrite
a result that already landed; the attempt exits 2. And a refused delivery is
not a refused assignment: handing work to a workspace with no running agent
still records it, and the owner finds it in lazybox work mine when it starts.
Exit codes: 0 on success, 2 when the call was wrong (a bad id, a missing
--summary, a terminal row), 1 when the daemon could not read the store —
so a script can tell a mistake that will never work from one worth retrying.
lazybox task status
Section titled “lazybox task status”Answers “is anyone working on owner/repo#N?” — the supported lookup for a
person at a shell and for an agent that cannot receive MCP tools. Read-only: it
never spawns, resumes, claims or changes anything.
lazybox task status obin-ai/core-solutions#151lazybox task status https://github.com/obin-ai/core-solutions/issues/151 --jsonlazybox task status 187 --repo obin-ai/core-solutions| Command / option | Effect |
|---|---|
task status <ref> |
Look the record up: owner/repo#N, a GitHub issue/PR URL, a Linear key, or #N beside --repo |
--repo <owner/repo> |
Repo used to resolve the bare #N / N forms |
--issue / --pr / --ticket <ref> |
Aliases for the positional reference |
--json |
Emit the full structured report (versioned by schema_version) instead of the summary |
--socket <path> |
Daemon socket to query (defaults to the standard socket) |
The daemon answers from its own live state, so the record resolves through every id its workspace holds — an issue still answers after its PR has taken over the row, and every matching workspace is reported rather than an arbitrary first one.
The report keeps apart facts that are easy to conflate. A finished agent turn is
not task completion; an unexpired working claim is not proof of a
running process (it has a one-hour TTL a crashed worker stops renewing); a
retained session worktree is not an agent turn. Where the evidence is
missing or contradictory the verdict is unknown rather than a guess.
Resolving a claim’s holder is the one part of this report that may reach
GitHub. Presence is the working label, which the daemon already has; the
holder, agent, model and expiry live in lazybox’s own claim comment. So a
record carrying that label — and no lease this box is itself renewing — costs
one comment read, on the lowest-priority tier. A working label with no
lazybox-authored comment behind it is reported as exactly that (no holder, no
expiry) rather than resolved into one: the label may be a human’s own workflow
label, its comment may have been deleted, or the read may simply have been
refused, and guessing between those is how a caller either double-spawns or
blocks forever.
Exit codes: 0 when status was established (including “nobody is working on
it”, which is a real answer), 2 for a reference that cannot be resolved, 1
when the daemon is unreachable or could not read its state — so a script never
reads a failed lookup as “no worker”.
The same report is available to a wired agent as the task_status MCP tool.
lazybox gh
Section titled “lazybox gh”The gh shim lazybox installs at ~/.lazybox/shims/gh and puts on every
spawned session’s PATH. It runs real gh for everything; what it adds is
routing through the daemon, so a fleet of agents shares one GitHub budget
rather than racing each other for it:
- an identical read from another session is answered from the daemon’s cache, costing nothing upstream;
- each session is paced by its own quota, and reads stand down while the budget is into the reserve the poller depends on;
- a mutation tells the daemon what it changed, so an issue closed with
ghinside a workspace flips its row within seconds — no sweep, no budget.
You normally never type this. A session’s gh already is this.
Anything the shim does not recognise is passed through untouched, and it
degrades to plain gh whenever the daemon is unreachable or slow. Two escape
hatches take it out of the path entirely:
gh.real pr list # the real binary, beside the shimLAZYBOX_GH_SHIM=0 gh pr list # opt one command (or a whole session) outConfigure it under providers.github.gh_shim,
where enabled: false removes the shim instead of leaving it on PATH.
Environment variables
Section titled “Environment variables”| Variable | Effect |
|---|---|
GH_TOKEN, GITHUB_TOKEN |
GitHub credential (otherwise gh auth token is used) |
LINEAR_API_KEY |
Credential for the Linear provider |
SLACK_BOT_TOKEN |
Slack bot credential; overrides slack.bot_token |
SLACK_APP_TOKEN |
Slack Socket Mode app credential; overrides slack.app_token |
RUST_LOG |
Log filter, e.g. RUST_LOG=lazybox=debug for verbose logs |
LAZYBOX_GH_SHIM |
Set to 0 to bypass the gh shim and run the real binary directly |
LAZYBOX_GH_SHIM_DIR |
Directory holding the shim; set on every spawn so the shim can resolve past itself |
LAZYBOX_GH_SHIM_DEPTH |
How many shim invocations deep this process is. The shim stamps it on each gh it runs and refuses past 4, so a misidentified shim bounds instead of recursing |
LAZYBOX_HOME |
Overrides every path lazybox writes under ~/.lazybox: state, config, worktrees, runtime dir, tmux socket. Logs are separate — they default to /tmp/lazybox.log (override with ui.log_path) |
LAZYBOX_RUNTIME_DIR |
Overrides just the daemon runtime directory (daemon.sock / daemon.pid); wins over LAZYBOX_HOME’s default <home>/run/ |
LAZYBOX_API_TOKEN |
Bearer token for lazybox server api (required unless --insecure-no-auth) |
LAZYBOX_API_ADDR |
Listen address for lazybox server api when no [addr:port] argument is given |
LAZYBOX_RELAY |
Relay address for lazybox serve when no --relay is given |
LAZYBOX_PLATFORM_URL |
Platform base URL used by lazybox account claim and hosted relay configuration |
LAZYBOX_PLATFORM_API_KEY |
Platform bearer used by a configured lazybox-relay entitlement gate |
| Path | Contents |
|---|---|
~/.lazybox/v2/state.db |
Persistent state (read/unread, snooze, sessions) |
~/.lazybox/config.yaml |
Configuration (see Configuration) |
~/.lazybox/run/daemon.sock |
Daemon Unix socket (lazybox server start / --connect) |
~/.lazybox/snippets.yaml |
Global snippet library (client-wide launch-directory override: <launch-dir>/.lazybox/snippets.yaml) |
/tmp/lazybox.log |
Logs (override with ui.log_path) |
~/.cache/lazybox/zig/ |
Pinned Zig toolchain from make setup |
~/.cache/lazybox/ghostty/ |
Pinned Ghostty source and Zig package cache used by offline builds |