Lookup material, grounded in the current CLI. For an ordered first run, see Get started.

Reference

Reference

The supported torio command surface and the fixed boundaries of Torio.

Running the binary

Build with go build -o torio ./cmd/torio, then put the result on your PATH — for example sudo install -m 755 torio /usr/local/bin/torio. Every command below is written as torio … and assumes that.

A freshly built binary sits in the repository root and is not on your PATH, and your shell does not search the current directory, so torio yields command not found until you install it. Running it in place as ./torio … from the repository root also works, but then every command needs the prefix.

Command surface — torio ui

CommandWhat it does
torio uiOpen the interactive hub: setup, box status, projects, the Second Brain, the guest service, and the MCP boundary on one screen.

Running torio with no command on a terminal opens the same hub. torio ui names it, so a wrapper script or a keybinding can ask for it explicitly.

Setup derives the next step from the current box state and skips capabilities the selected backend does not declare. Actions use the same operation as their CLI counterpart. One operation runs at a time; long operations show elapsed time. Interactive sessions receive the terminal directly.

Keys are shown at the bottom of every screen. 1 to 6 and tab switch screens, enter runs the highlighted thing, r re-reads state, w jumps to the setup step you are on, b rebinds the hub to another backend, and q quits.

Command surface — torio vm

Controls the torio Lima VM. torio vm init creates it; every other subcommand operates on the existing instance. The parent takes no action itself; an absent or unknown subcommand is a usage error.

CommandWhat it does
torio vm initCreate the Torio VM from the embedded pinned template, or succeed idempotently when a compatible instance already exists. Incompatible instances fail closed — there is no --force. The global --backend NAME selects which box this is and records the agent it runs, fixed at creation. Sizing: --cpus N, --memory SIZE, --disk SIZE (defaults 4, 8GiB, 60GiB). They apply at creation only, since init never recreates.
torio vm statusReport the Torio VM state.
torio vm startStart the VM. Idempotent; confirms a Running post-state before reporting success.
torio vm stopStop the VM. Graceful and idempotent; never uses --force, never removes the VM or its data, and requires a Stopped post-state.
torio vm bootstrapReconcile and verify the existing target for the declared backend. Installs the backend at its pin when it is missing; verifies operator membership in torio-projects. Idempotent on a reconciled target. Accepts --timeout and --json.
torio vm ssh -- COMMAND…Run a command inside the VM. Does not open an interactive shell, forward stdin or a TTY, migrate data, start a chat, or copy credentials.
torio vm shellOpen the Lima login identity's own shell inside the box, also on s on the hub's dashboard. No SSH agent is forwarded and no multiplexed connection is reused, so the session cannot ride or become a push-capable one. Interactive; --json is a usage error.

Command surface — torio backend

Reports the agent backend this instance runs and, for a backend that holds a credential of its own, opens the session where you grant it one. The parent command takes no action itself; an absent or unknown subcommand is a usage error.

CommandWhat it does
torio backend statusReport the backend name and guest identity, the version installed on the guest, whether a credential is present, and which capabilities the backend declares — a project registry, a guest service, an interactive session. Reads the guest, changes nothing, and never reaches the network.
torio backend loginOpen an interactive terminal on the guest as the backend identity and start the backend so its own login flow runs. Interactive, so --json is a usage error.

Which backend an instance runs is fixed at torio vm init --backend NAME and recorded in that instance's config. A second backend means a second instance, never a second agent inside one VM.

You do not track which instance that is. --backend NAME is a global flag: it names the agent an invocation is about and selects the box that runs it — the default backend keeps torio, the rest are torio-<backend>. TORIO_INSTANCE still names a box directly, for a test VM or a second box running the same backend, and wins over the flag; given both, a disagreement between the flag and what the instance declares is a usage error rather than a guest built for one identity being driven as another.

login grants the box a credential, not you. It is issued to the guest identity and can be revoked without touching your own, and Torio never copies a credential in from the host: a shared identity would couple revocation to a machine you also work on and make the box's activity indistinguishable from yours. Torio sees none of the flow — it builds the transport and hands over the terminal. No SSH agent is forwarded, so a login session cannot reach a Git remote.

What the session then shows is the backend's own flow, and it differs. Claude Code starts and prompts. Codex asks for a device code, which is the flow that works on a box with no browser: it prints a code and a URL you open wherever you already are, and nothing has to reach the guest's loopback. An operator who would rather use an API key can run codex login --with-api-key in a torio vm shell session as the backend identity and paste the key on standard input; both routes leave the credential in the same place, and backend status reports it the same way.

status verifies and never repairs. It runs the same guest checks as vm bootstrap, with every repair turned off: a box missing something bootstrap would install — the pinned binary, the command-path link, the managed settings — fails the check and names torio vm bootstrap as the remedy, rather than being quietly rebuilt by a command you asked a question.

status answers four states for the credential, and only two of them are findings. present and absent are what a probe returned. not-applicable is for a backend that declares no credential check at all, and unknown is for one that declares a check whose result is missing from the report.

The distinctions are the point. A backend Torio cannot ask has not been found to be logged out, and a backend Torio could ask but did not hear back from has not been found to be unaskable. Each of the last two is one command's worth of difference for an operator, and collapsing them is how a box that holds a credential comes to report that its state is unknowable.

For a backend that is an MCP client, status also reports its configured server names. Where this comes from an agent-owned file it is a drift detector. Claude Code's released route is the root-owned managed MCP document; unmanaged native entries are excluded by the pinned managed setting. Codex keeps its declarations in a file the agent owns, and a root-owned allowlist decides which of them may run, matching each against the relay path and the single service argument it is permitted to carry. In every case tool permission comes from the separate root-owned broker policy, not from the backend report. See ADR-0013.

Command surface — torio brain

Manages the private Second Brain. There is one Brain. Its canonical vault is a Markdown corpus in a Git repository on the host, under ${XDG_DATA_HOME:-~/.local/share}/torio/brain/vault, and each backend's guest keeps a replica of it in that identity's own home, owned by that identity and, on a backend that keeps a project registry, registered as its own project so any session can search it. torio brain sync makes a replica and the host vault agree; brain status prints the path on the box you are talking to. The parent takes no action itself; an absent or unknown subcommand is a usage error.

CommandWhat it does
torio brain initCreate the canonical scaffold atomically through private guest staging, make the first local commit, Then install or refresh the global torio-brain retrieval skill so other projects can search the Brain. Idempotent for managed state; refuses to touch non-empty data it did not create. Configures no remote and pushes nothing.
torio brain statusReport state (initialized, uninitialized, or drift), the canonical path, native filesystem, ownership and mode, Git worktree state, aggregate counts, and skill state. Changes nothing.
torio brain import <host-directory>Import an existing Markdown vault through private host and guest staging, verified by checksum on the guest. Accepts --into SUBDIR to land the import as one new contained subtree, and --dry-run to preflight without transferring anything. The hub offers the same import on m on its Brain tab, and always preflights first: what would move is shown, in counts, before a second enter moves it.
torio brain syncReconcile this backend's replica with the vault on the host, both ways, by carrying Git bundles over the same one-shot transport brain import uses. Unsaved work in the guest vault is committed first. Neither vault gains a network remote. A merge that cannot be made automatically stops that direction, leaves it as it was, and names the host vault where you resolve it with Git. Counts are reported; note names and content are not. Rebinding the hub runs the same reconciliation on both sides of the move, and its note reports what each carried.

Output never contains note names or note content — not in success output, not in error.details. Every command reports bounded aggregate metadata only: file counts, total bytes, a manifest digest, and stable drift markers. This is the Brain's privacy boundary, not a matter of brevity.

import refuses or skips credential-shaped files, repository metadata, links, hardlinks, special files, and executables. Existing data is never overwritten — the single exception is an untouched scaffold that Torio itself created.

Sessions that were already open when init ran will not see the retrieval skill: A backend may cache a skill's prompt per process, so restart open sessions.

The vault on your host

torio brain sync puts the vault on your host, at ${XDG_DATA_HOME:-~/.local/share}/torio/brain/vault, and keeps it and this box's replica reconciled. It is a Git repository holding Markdown, so it is readable, greppable and backup-able with the tools you already have, and it is where a merge conflict is resolved.

There is still no torio brain export. What sync carries is a Git bundle read once and removed, in both directions, and neither vault ever gains a network remote. Backing the host vault up is your decision and your command; Torio reconciles the copies and makes no claim beyond that.

On a backend that keeps no registry or no skills

The vault, its git history and the import pipeline are the same on every backend: the vault lives in the backend identity's own home, owned by it, and brain import verifies and promotes exactly as it does elsewhere.

Two things are per-backend, and brain status reports each as a state rather than as a fault:

Command surface — torio project

Attaches repositories to the guest, inspects them, and forgets them. The parent takes no action itself; an absent or unknown subcommand is a usage error.

CommandWhat it does
torio project add <name> [remote]Clone the exact remote into the derived workspace path, or verify and adopt a checkout already there; give the operator and the backend identity shared access; register the project where the backend keeps a registry, before recording it. --id SLUG picks an id other than <name>; --use makes it active on success. With the id alone and no remote, materializes an already registered project in the selected backend's guest, using the remote on record. --local makes an empty repository instead, for a project that has no remote at all; --from-bundle FILE attaches from a Git bundle on this machine (git bundle create FILE --all), carried in over the same one-shot transport brain import uses. Neither reaches a network, and neither needs a deploy key.
torio project listList the registered projects. Reads config only, runs nothing on the guest, and works with the VM stopped.
torio project show <id>Report the shared entry, checkout state, and backend registry state where one is declared. Reports drift without repairing it, and returns no filenames, diffs, or raw Git output.
torio project use <id>Make a project active in the backend registry. A backend with no registry refuses the command.
torio project set-remote <id> <remote>Replace the remote of a project already on record. The registry is shared, so the correction applies to every backend. The checkout on the selected backend's guest is repointed when its origin still holds the remote being replaced; any other origin is reported and left alone. The id and display name do not change. It is also how a local project gets its first remote: the guest must be able to read it, so this is where a deploy key is provisioned for a private one. A remote cannot be removed — other guests' checkouts still point at it.
torio project sync <id>Reconcile a project that has no remote with the bare repository on your host that its boxes meet in, carrying branches and tags both ways as Git bundles over the same one-shot transport brain import uses. A ref is written only where what the other side holds is an ancestor of what is arriving; a ref that moved on both sides is named and left as it was. Uncommitted work is never carried. The branch the checkout stands on moves through the worktree, and where Git refuses that because work in the tree would be written over, the branch is held back and named rather than forced. A project that has a remote is refused: its boxes already meet there.
torio project remove <id>Archive the backend registry entry where declared, then drop the shared entry. The checkout and deploy key are retained and reported.
torio project enter <id>Open an ordinary interactive terminal in the checkout with SSH agent forwarding disabled. A registered project with no checkout on this backend's guest is materialized from the remote on record first. Interactive, so it does not support --json.
torio project agent <id>Start the configured backend inside the checkout, running as the backend identity rather than as you. A registered project with no checkout on this backend's guest is materialized from the remote on record first. No SSH agent is forwarded and the connection is never multiplexed, so it cannot inherit an operator write connection. The guest's own read route remains available. Interactive; --json is a usage error. A backend that declares no interactive session has nothing to open.
torio project shell <id>Open an ephemeral operator session in the checkout with your SSH agent forwarded. A registered project with no checkout on this backend's guest is materialized from the remote on record first. Interactive, so it does not support --json.

The workspace path is not an input. It is always derived as <backend workspace>/<id> — never taken from you, never stored in config. On the default backend that is /home/claude/projects/<id>. Without --id, the id is <name> itself, which must be a lowercase slug.

One registry, one checkout per backend. The registry is shared by every instance, so a project you attached while talking to one backend is on record for all of them and project list says the same thing whichever you select. The checkouts are not shared and cannot be: each is owned by one backend's guest identity. torio project add <id> --backend NAME clones it into that backend's guest from the remote already on record — a separate step rather than something project agent does for you, because cloning reaches a Git remote. The two checkouts are independent working trees; what passes between them is what you push.

A project needs no remote. --local records a project with none: it is an empty repository in the guest that made it, and it is on no forge. --from-bundle records the same kind of project from a repository that already exists on your machine. Both are listed on every backend, because the registry is shared. Where the local checkout has no origin, that is agreement; an origin appearing on one is ordinary drift.

A local project reaches your other boxes through your host. torio project sync <id> writes a bare repository at ${XDG_DATA_HOME:-~/.local/share}/torio/projects/<id>.git and reconciles this box's branches and tags with it, both ways. That path is derived from the id on the machine that needs it and is recorded nowhere, so every registry entry keeps meaning the same thing on every machine. Once a project has been reconciled once, opening it on another backend's guest materializes the checkout from there, the way a project with a remote is materialized from the remote. What arrives is the branch the host repository points at; the other branches come in at the first torio project sync on that box. Before that first reconciliation there is nothing to make it from, and opening it says so rather than guessing. The host repository holds what a sync carried; Torio does not schedule one and does not call the directory a backup.

Torio stores no host Git credential. A remote the guest cannot read without prompting fails closed. For an SSH remote, add generates a deploy key on the guest and prints the public half — as does set-remote when it gives a local project its first remote, which is the moment a key first has a remote to authorize against. Add it to that repository with write access off, then run the command again. Torio cannot verify the forge setting.

add resets, cleans, and deletes nothing on the guest, so a rerun after a failure finishes the work rather than starting over.

Session write paths

enter is an operator terminal with no forwarded agent. shell forwards the operator's agent. agent has no operator write route unless --push-grant is used with a pinned key; then each signature still waits for host approval.

Use torio project enter <id> for ordinary editing, checks, and local commits. The SSH transport disables agent forwarding and connection multiplexing, so it cannot reuse a push-capable operator connection.

Use torio project agent <id> to put the backend to work in the checkout. It runs as the backend's own guest identity, not as you, on the same transport as enter: no forwarding, no multiplexing. The agent owns the tree and can commit in it. A correctly authorized deploy key can fetch but not push; no operator write credential reaches the session.

Inside the box the backend runs without permission prompts, and that is not a weakening. A prompt is a control inside the agent's own process. The box replaced it with controls the agent cannot reach: an unprivileged identity with no sudo, a closed group set, no operator write credential, and the edge of the VM.

project shell forwards your SSH agent for exactly as long as the session lasts, and the capability leaves with you when you exit.

The session is preflighted first — the project registered, the VM bootstrap-verified, the checkout present with the registered origin and shared permissions, your local agent holding an identity to forward. Torio never test-pushes to prove any of it, and once you exit it makes no claim about what you pushed. Check the remote yourself.

The session is not bounded by --timeout; you end it.

Command surface — torio mcp

Installs and verifies the MCP custody boundary and carries traffic through it. The parent command takes no action itself; an absent or unknown subcommand is a usage error. The hub's MCP tab offers the same three operations: the status rendered, i for install, and l for login with the service picked from the verified grant.

CommandWhat it does
torio mcp installProvision the torio-mcp identity and private home, verify root-owned policy, atomically install the broker, relay and unit shipped with this release, and wire the selected backend to the relay. A settled rerun reports changed:false.
torio mcp login <service>Run interactive OAuth as torio-mcp through a loopback-only callback forward. No SSH agent is forwarded and --json is not supported. The unit starts after every policy service has logged in.
torio mcp statusVerify identity separation, private OAuth state, exact policy and backend configuration, and—when login is complete—the active unit, sockets and running policy digest. It repairs nothing.

Policy is written by the operator as root-owned /etc/torio-mcp/policy.d/<service>.json. Each document names one Streamable HTTP endpoint and an exact tool list; every tool is explicitly classified as writing or read-only. install does not generate or guess a grant, and an empty policy is an unmet precondition.

The release archive contains the host torio binary plus Linux guest binaries for torio-mcp-broker and torio-mcp-connect. Install accepts no secrets. It may report changed:true with an error when a durable mutation succeeded before a later verification failed; rerun it after fixing the named precondition.

login prints the provider authorization URL and waits for the callback. OAuth discovery, dynamic client registration, PKCE S256, exchange and refresh use the pinned official MCP Go SDK. Tokens stay below /home/torio-mcp/oauth as torio-mcp:torio-mcp 0600 files inside the private 0700 home. Torio accepts no token, client secret or credential file from the host.

Every backend launches the credential-free relay over stdio, and each one puts the control somewhere its own agent cannot reach. An agent-owned declaration is agent-writable, so its exact relay check is a drift detector, not a boundary. Claude Code uses root-owned /etc/claude-code/managed-mcp.json together with allowManagedMcpServersOnly: true; install removes native MCP entries from the agent-owned .claude.json, and status rejects their return. Codex declares its servers in a file it owns, written through its own codex mcp command, and root-owned /etc/codex/requirements.toml allows only the relay path carrying one named service; status reads what Codex resolved rather than what it declared, so a server the allowlist disabled is reported rather than counted.

Every report enumerates the verified grant by service and endpoint, including tool and write-tool counts plus the policy generation digest. While any service still requires login, a missing runtime is the valid dormant state. Once OAuth state is complete, successful status requires the trusted unit, the exact live socket set, and a running digest equal to the root-owned documents.

At startup the broker enumerates upstream tools and refuses readiness if a policy tool is absent. Each call is checked against the exact grant and audited with time, peer uid, service, tool, write classification and allow/deny result. Arguments, results, protocol bodies and credentials never enter the audit. A missing peer uid or unwritable audit fails the call closed.

The broker does not solve confused-deputy use of an explicitly granted tool and does not prevent exfiltration through unrestricted guest egress. Granting a write remains an explicit human decision in root-owned policy.

Command surface — torio status

The one command that answers across boxes. Every other command addresses the single instance the invocation selected; this one polls every box Torio owns.

CommandWhat it does
torio statusReport each box Torio owns: whether it is running, which backend it was provisioned for, what that backend has running, whether anything there is waiting on a human, and when it last provably did work. Accepts --json and --format.
torio status setup tmuxPrint the configuration that puts the one-line form on a tmux status bar. It prints; it writes no file.
torio status setup zshPrint the configuration that puts the one-line form on a zsh prompt. It prints; it writes no file. The hub shows the same recipes on t on its dashboard.

Every field is a proven value, ? for a question that was asked and could not be answered, or for one that backend does not answer at all. Absence is never rendered as a zero, because a surface that cannot distinguish "quiet" from "unreadable" is one you stop looking at.

It exits 0 whenever the poll completes. A box that could not be reached, a config document that could not be read, a fact that could not be proven — each costs one field and nothing else. Only failing to list the boxes at all is an error, because then there is nothing to report on.

The poll covers the default box, every box whose name Torio derived from a backend, and the box TORIO_INSTANCE names for this invocation. --config does not redirect it: each box's backend is read from the document that box owns.

--format tmux and --format prompt collapse the same report onto one line for a surface that is glanced at rather than read. Asking for a line and --json at once is a usage error — the envelope is the machine contract and a line is a rendering of it. A poll that could not complete prints torio: ? on the line and still exits non-zero, because something refreshing a bar on a timer shows whatever arrives, and an empty line there reads as a quiet host.

setup prints and nothing else, and no flag will make it write. A dotfile belongs to the operator; this is the same line torio vm bootstrap holds about a managed file it did not install. The snippet calls the binary by the path of the executable that printed it rather than by name, because an older torio earlier on PATH exits 2 and every such surface renders that as an empty line.

torio status does not say whether one box is healthy. torio backend status walks a box's bootstrap checks, and torio serve status proves whether its guest service is answering.

Command surface — torio version

CommandWhat it does
torio versionPrint the version, commit, build date, and Go toolchain of the binary you are running. Accepts --json.

This is the only place Torio names its own version. Nothing else in the CLI or in these docs is labelled by release, because the label would not tell you anything you could act on — and it would be wrong the moment the next one shipped.

A binary built straight from a checkout reports dev with an unknown commit; that is expected, not a fault.

Readiness and exit semantics

Global flags

Four flags are accepted by every command, before or after the subcommand. This is the whole list; an unknown flag is a usage error (exit 2), never silently ignored, and there is no global --force.

FlagWhat it does
--jsonEmit a single machine-readable JSON document on stdout, where the command has one to emit; human logs stay on stderr. --help is the one exception — it prints usage and exits 0 without an envelope.
--verboseRaise stderr diagnostics from warnings to debug. Stdout is untouched, so machine output is identical with and without it, and the extra lines are redacted like every other diagnostic.
--timeout DURATIONBound the operation. Default 30s, or default_timeout from the config file when set; an explicit flag always wins. Anything above the policy maximum 10m is rejected before any work starts.
--config PATHRead the non-secret config document from PATH instead of $XDG_CONFIG_HOME/torio/config.json. It bypasses XDG entirely, is resolved and validated rather than merely parsed, and applies to the project registry as well; a missing file or an invalid document is a usage error (exit 2).

Fixed boundaries

ThingValue
VMtorio-<backend> — a Lima instance Torio creates from a pinned template and never re-images or deletes
Guest identitythe backend's own: claude on Claude Code, codex on Codex
Profile / application state/home/<identity>/.<backend> (native Linux filesystem; not the Second Brain)
Second Brain vault/home/<identity>/brain (native Linux filesystem; separate from profile)
Workspace root/home/<identity>/projects (setgid torio-projects shared workspaces)
A project's checkout/home/<identity>/projects/<id> — always derived from the project id, never supplied

Rootful Docker for the agent's guest identity is forbidden. Bootstrap verifies it is not in the docker group.

Deployment

This site is served at torio.dev by Vercel, straight from the committed site/ output: a minimal vercel.json declares the output directory, and there is no build step, no framework, and no runtime dependency. Account, project, and DNS configuration live outside the repository, with the operator.

The pages under site/ are generated by scripts/build_docs.py from the Markdown sources in docs/content/. The generated files are committed, so a push to main is a deploy.