Loom keeps your agents on track.

Run a Kanban Fleet

A Fleet manages workers across several computers from one desired-state file stored on the board, instead of installing each worker by hand. Use it when you want a stated number of Codex, Claude, or OpenCode workers on named computers, kept converged automatically. For a single worker, use Connect another computer to a board.

Two small programs do the work. Both are run with uv from the installed runtime directory (~/.loom/cli on a standard install; call it <fleet-dir>; it contains kanban_fleet.py, fleet_residual.py, and requirements.txt). Do not copy them into a repository.

loom fleet --help
loom residual --help

loom fleet ... forwards to kanban_fleet.py (operator: desired state and the vault) and loom residual ... forwards to fleet_residual.py (per-computer reconciler).

Commands#

loom fleet (global options --store <board> default kanban, --stores-file <path>)#

SubcommandPurpose
key-create [--replace]Create the 32-byte Fleet vault key, once, on a trusted computer.
key-install [--replace]Install the vault key on a computer; the human pastes it at a hidden prompt.
profile-put-claude --profile-id IDStore the encrypted Claude worker token profile.
profile-put-git-ssh --profile-id IDStore an encrypted Git SSH profile for private repositories.
profile-put-loom-store --profile-id ID --store-name NAMEStore a profile for a worker on a different Loom store.
profile-put-app-secrets --profile-id IDStore application secrets (never materialised on disk).
config-publish --file PATHPublish a desired-state file.
show [--config-id ID]Show the published config's metadata (config_id, revision, config_sha256, created_at, credential_profiles), the profiles stored in the vault, and vault_key_installed, never secret values (config id default default; config is null when none was published). It does not print the [[workers]] entries; keep your fleet.toml.

loom residual (options --store, --config-id default default, --stores-file)#

SubcommandPurpose
preflightCheck this computer can run the residual. Changes nothing.
installInstall the residual schedule (cron, launchd, or Task Scheduler). This changes the computer.
statusShow its state.
onceRun one reconcile cycle now. Changes no schedule.
daemon [--max-cycles N]The reconciler loop the schedule starts (every 3 minutes).
removeRemove the residual. A local operator only.
prune-uv-cacheClean its cache. Needs uv on PATH.

preflight prints nothing and exits 0 when the computer is ready; it passed with ~/.loom/bin off PATH (uv was still found). The schedule runs the reconciler every 3 minutes, so a daemon is not always resident: after install, status showed a daemon pid once and then pid: null. While a daemon is running, once and daemon --max-cycles N print {"result":"already_running"} and do nothing; once runs a cycle only when no daemon is running. remove deletes the schedule and stops any daemon, but leaves ~/.local/state/loom-fleet/ and the computer's last fleet_host_status row in place. That directory holds the vault key, and, where present, decrypted credential files, cloned repositories, runtime, and log (none of the credential files or clones were present in the test, so confirm that on your computer). With no schedule installed, prune-uv-cache simply prunes (result: pruned, residual_stopped_pid: null). If uv is not on PATH it prints {"result":"aborted","reason":"uv executable not found on this host; nothing to prune"}.

Vault key and profile prompts#

key-create writes the key to ~/.local/state/loom-fleet/vault-key (mode 600) and prints that path; copy the file's contents out of band. key-install reads the key at a hidden prompt (Fleet vault key (base64; input hidden):), so run it in a terminal. Without a tty it still reads stdin but warns Password input may be echoed. If a key is already present, key-create and key-install print one line, <subcommand>: a key already exists at <path>; nothing was changed, and exit 1 (v0.674 or later; earlier releases printed a Python FileExistsError traceback). Pass --replace to overwrite it.

Each profile-put-* command reads its secret at a hidden prompt. Only the git-ssh, loom-store, and app-secrets prompts want base64; the Claude prompt is Claude setup token (input hidden): and wants the raw claude setup-token output.

CommandPaste
profile-put-claudeThe raw output of claude setup-token. It is verified with the claude binary, so claude must be installed on the computer you run this on. Without it, both inputs failed with Claude setup token does not authenticate: claude executable is not installed; token could not be verified.
profile-put-git-sshThe base64 of an OpenSSH private key file.
profile-put-loom-storeThe base64 of a stores.toml with a [<store-name>] section. From v0.674 the section may carry web_origin (as loom login --base writes it on a private server); the worker's materialized stores.toml keeps it. It must be a bare https:// origin with no credentials, path, query or spaces (plain http:// only for an IP address); anything else is rejected with Loom store web_origin must be an https origin such as https://memory.example.net. Earlier releases rejected the field (unsupported Loom store field: web_origin), so remove that line there.
profile-put-app-secretsThe base64 of NAME=value lines (UTF-8).

Not tested with a real claude install: profile-put-claude and its token verification.

Omit --store for one store-neutral residual that reconciles every store registered in ~/.loom/stores.toml; pass it to pin one store. Exactly one residual runs per computer either way. On a private server the store-neutral form reported every store as degraded (capability_unclassified ... no admin store is configured; capability catalog unavailable), while --store <board> converged, so pass --store on a private server.

Bring a new computer into a Fleet#

In order. "Manual" steps need a person.

  1. Manual. git and uv on PATH. The installer places uv at ~/.loom/bin/uv and, from v0.672, puts ~/.loom/bin on PATH for new terminals (Install the CLI); with an earlier installer add it yourself. The fleet scheduler also finds uv in ~/.loom/bin when it is not on PATH (v0.672 or later).
  2. Manual. Install the Loom runtime and at least one harness plugin (Install the CLI, Set up and log in). This creates <fleet-dir> (~/.loom/cli).
  3. Manual. Get the board's credentials into ~/.loom/stores.toml with loom login, or by hand (Connect another computer).
  4. Manual. Install each harness binary you want and sign in as the OS user that will run it.
  5. Manual. Git authentication for the target repositories (local, or a profile-put-git-ssh profile).
  6. Manual. Receive the vault key out of band and run loom fleet --store <board> key-install; paste it at the hidden prompt yourself, never through an agent.
  7. Manual. loom residual --store <board> preflight, then install, then status.
  8. Automatic. The computer mints its own host_id (a UUID) on first use, installs its schedule, and starts heartbeating.
  9. Manual (the step people miss). Read the new host_id and add a [[workers]] block for it:

    loom query --params '{"namespace":"<board>_user","rule_name":"fleet_host_status","args":[""]}'

    <board> is the store name from ~/.loom/stores.toml (for example kanban_user). The query returns [] until the residual completes its first cycle, up to about three minutes after install. Match the row by host_ip, hostname, and OS, copy its host_id, add it to your desired config, and publish with loom fleet --store <board> config-publish --file fleet.toml.

  10. Automatic. Within a few reconcile cycles (about three minutes each) the residual clones the repository, creates the worker sessions, and runs one proof turn per worker.

Desired-state file#

schema_version = 1
config_id = "default"

[credentials.claude-main]
revision = 1

[[workers]]
host_id = "<host uuid from fleet_host_status>"
harness = "codex"            # claude | codex | opencode
model = "<exact model key>"  # omit for Claude's own default
repo = "git@github.com:<org>/<repo>.git"
repo_name = "<repo>"
branch = "main"
count = 1

Within one board, each (host_id, repo_name, harness) is one worker identity and count expands it into numbered slots. credential_profile, store_profile, and application_secret_profile reference profiles you stored with the profile-put-* commands. Set count = 0, or remove the entry, and republish to remove a worker.

Every profile a worker uses must also be declared as [credentials.<profile_id>] with revision = N, and that profile at that revision must already be stored. Otherwise config-publish fails with Fleet credential profile is unavailable: <id>@<N> or Fleet worker 0 references undeclared credential <id>. The example declares claude-main, so run profile-put-claude --profile-id claude-main first, or delete that block.

Each config-publish creates a new revision, even if the file is unchanged. repo and branch are immutable for an existing (host_id, repo_name, harness); use a new repo_name to change them.

Allowed keys:

WhereKeys
Top levelschema_version (must be 1), config_id, credentials, workers
[credentials.<id>]revision only (integer, 1 or more)
[[workers]]host_id, harness, model, repo, repo_name, branch, count, credential_profile, store_profile, application_secret_profile

Required worker keys: host_id (a UUID), harness, repo, repo_name, count (0 to 100). model and branch are optional; branch defaults to main. Unknown keys are rejected, for example unsupported Fleet worker field: <key>. A repo URL with userinfo, query, or fragment is rejected; the scp-style git@host:org/repo.git is fine. config-publish does not validate the harness name (an unknown harness such as "cursor" was accepted); use exactly claude, codex or opencode.

Credentials: what is shared and what is not#

HarnessPer Fleet or per computer
ClaudeOne token for the whole Fleet. One browser approval (claude setup-token, then profile-put-claude); every computer holding the vault key decrypts the same profile.
CodexA human signs in on every new computer (codex login). Fleet never copies Codex credentials.
OpenCodeA human signs in on every new computer (opencode auth login). Fleet never copies auth.json. The worker fetches its plugin from the web_origin of its store section (see Join from another computer); from v0.674 a web_origin in the profile-put-loom-store profile reaches the worker's stores.toml.

Observe and recover#

  • fleet_host_status rows give seen_revision, applied_revision, state, last heartbeat, and redacted worker actions (the workers list). host_id is the stable target; an IP address may change.
  • loom fleet --store <board> show shows the published config's metadata and the stored profiles, not the workers.
  • If a worker is removed on a computer whose host_id is in the config, the residual rebuilds it within about one reconcile cycle. On a computer outside Fleet management there is no automatic recovery: a Claude worker needs a fresh token approval and Codex or OpenCode need the OS user to sign in again. Check whether the host_id is in the published config before running remove.
  • Windows: the residual and workers start only after the configured user signs in.

Not tested with real claude, codex and opencode harnesses: live worker creation, proof turns, and rebuilding a removed worker.

See also Tutorial 10C.