One task per section, for when you already have a running setup. Doing this for the first time? Follow Get started instead — it covers all of this in order.

How-to guides

How-to guides

Short, task-oriented guides for the things an operator does by hand. Each section stands alone and reuses the operational blocks from Get started.

Configure a model provider

Until a provider is configured, starting a session fails at agent init — for example agent init failed: No credentials stored. Check what the guest currently has:

torio vm ssh -- sudo -u claude -- claude --version

torio vm ssh forwards no stdin or TTY by design, so the interactive picker cannot be driven through the control plane. Use an interactive shell on the guest instead. limactl shell logs you in as the Lima user, so become the agent identity explicitly:

limactl shell torio-claude-code     # interactive shell in the VM (Lima user)
sudo -iu claude                     # become the agent identity
claude                              # sign in through the agent's own flow

Which provider and credential to use is the operator's judgement. The secret never enters the repository, its evidence, or any pull request or comment.

Attach a repository

Torio manages only repositories in the registry. It discovers nothing from disk, and removing a registry entry leaves the checkout on the guest.

torio project add my-service https://github.com/you/my-service --use
torio project list

add clones the exact remote into the derived workspace path, gives you and the agent identity shared access to it, before it records anything in config. --use makes it the active project. If a valid checkout of that remote is already at the path, add verifies and adopts it rather than touching it.

You do not choose the path. It is always <workspace root>/<id>, derived from the project id — never taken from you, never stored in config. Without --id, the id is the name you gave, which must be a lowercase slug; pass --id to pick a different one.

A private repository takes the same command. Torio copies no host Git credential into the guest. A remote the guest cannot read still fails closed, at exit 7. For an SSH remote that failure comes with the way through: the guest generates its own deploy key and prints the public half.

The guest generated a deploy key for this project. Torio holds no copy of its private half.

ssh-ed25519 AAAA…

Add that key to the repository on github.com as a deploy key, with write access off,
then run the same command again. Adding it to your account instead would give
the guest write access to every repository that account can reach.
Private half, on the guest, owned by the backend identity: /home/claude/.ssh/torio/my-service

On GitHub that is the repository's own Settings → Deploy keys → Add deploy key, with Allow write access left unchecked. Then run the same add again and the second run clones. A key you authorized before the first run attaches in one command, and a rerun before you authorize it reports the same key rather than making another.

Where you paste the key is the whole of what keeps it read-only. Torio cannot check which you did, because proving a key cannot write would take a push and Torio runs none. A key added to your account rather than to the repository attaches the project equally well and leaves the guest able to write everywhere, which is the one way this path can widen what the VM can do.

The private half is generated on the guest, stays there, and is never read, copied, or stored by Torio. It is readable by the backend identity, which is the identity the model runs as, so treat it as a credential that lives where the agent lives. Push is unaffected: it still travels through the agent you forward with project shell.

A private HTTPS remote has no such path, because reading one takes a stored credential. Use the SSH remote. Do not work around any of this by copying a checkout from your host: a recursive copy drags host Git config, hooks, and keys across the VM boundary, which is exactly the thing this path exists to prevent.

Nothing on the guest is reset, cleaned, or deleted, so if add fails partway a rerun finishes the work instead of starting over.

Inspect and forget

torio project show my-service     # registry entry and checkout state
torio project use my-service      # switch the active project
torio project remove my-service   # forget it

show reports drift as stable markers rather than repairing it, and returns no file names, diffs, or raw Git output. list reads config only and runs nothing on the guest, so it works with the VM stopped.

remove drops the config entry. The checkout is never deleted — the output tells you where it still is. There is no --delete.

A deploy key is never deleted either, and unlike a checkout it is not inert. remove reports it as retained and touches nothing: the key stays on the guest and stays authorized on the forge until you withdraw it there. If you removed the project because the guest should no longer read that repository, deleting the deploy key on the forge is the step that makes it true. Deleting the guest file the output names is what makes the next add generate a fresh key, which is also how you rotate one.

Push, when you decide to

The persistent backend receives no operator write credential. A private repository's guest deploy key is read-only only if you authorized it that way; Torio cannot verify the forge setting.

When you want to write to a remote, open a session that carries your own capability:

torio project shell my-service

This forwards your host's SSH agent into an interactive session in the checkout. The capability lives exactly as long as the session does and leaves with you when you exit. Inside it you are the agent identity, in the project directory, with your agent available to Git:

git status
git diff
git commit -am 'the change you decided to make'
git push
exit

Torio preflights the session — the project registered, the VM bootstrap-verified, the checkout present with the registered origin and shared permissions, your local agent actually holding an identity to forward. It never test-pushes to prove any of that, because a test push is a write you did not ask for.

Once you exit, Torio makes no claim about what happened. It does not know whether you pushed, and it will not tell you that you did. Check the remote yourself.

Pin one key, and approve each signature

By default the session forwards your agent whole: every identity it holds is usable inside. To narrow that, set operator_key in the config document to a fingerprint or key comment naming the one identity a session may use. With the pin set, project shell forwards a mediated agent instead: it lists the pinned key alone, asks you on the host before every signature (a dialog naming the project, the remote, the branch and how far ahead it is; Deny is the default and the cancel), and records each decision to agent-audit.jsonl beside the config before acting on it. The dialog reports what the checkout held when the session opened; Torio still makes no claim about what a signature was used for.

Let an agent session ask to push

An agent session normally receives no operator write route. With a pinned operator_key,

torio project agent my-service --push-grant

opens one that may ask: the mediated agent is reachable inside the session, every signature waits for your confirmation on the host, and an unanswered dialog denies. The grant lasts one invocation; no config field turns it on. Without a pin the flag is refused outright, and a preflight refuses an origin the grant could not serve (an HTTPS push URL never consults an SSH agent, and a host key missing from the agent identity's known_hosts stops a push before it reaches the key), each with its remedy.

The session is not bounded by --timeout; you end it. It is interactive, so it does not support --json — there is no document to emit, and asking for one is a usage error rather than a silently ignored flag.

Your agent must be loaded before you start: ssh-add -l should list an identity. An empty agent fails the preflight rather than opening a session that cannot push.

Run a check without leaving the control plane

To run something inside a checkout without opening a session, torio vm ssh executes a fixed command as claude and returns its output. It forwards no stdin and no TTY, so it suits non-interactive checks and nothing else.

Pick a check from the repository's own contributor documentation — one that reads and reports rather than writing, installing, deploying, or pushing — and run it against the derived workspace path:

torio vm ssh -- sudo -u claude -- \
    python3 /home/claude/projects/my-service/scripts/some-check.py --check
torio vm ssh -- sudo -u claude -- \
    git -C /home/claude/projects/my-service status --porcelain

The second command must print nothing: a check that leaves the tree dirty was not the read-only check you thought you were running.

Two limits:

Edit a project with your own editor

Torio mounts no host directory into the VM. A project is an ordinary Git checkout at /home/claude/projects/<id>, owned by the claude guest identity. Editors reach that checkout inside the VM or over SSH. The optional Neovim panel under integrations/neovim uses the same routes.

The normal entrypoint opens the checkout without forwarding your SSH agent:

torio project enter my-service

Only open the push-capable operator session when you intentionally need it:

torio project shell my-service

The guest is deliberately minimal — Python is present, most other tools are not — so anything you want to run inside the VM you install in the VM yourself. That install is your setup, not part of the control plane, and it stays inside the VM: it must never add a Git remote, configure a credential helper, or grant push access.

ToolHow it reaches the checkout, and what to watch
Neovim, or any terminal editorRun it inside project enter for ordinary work. If it is not in the guest, install it there once (for example sudo apt-get install neovim). Your plugins, LSP servers, and config are the guest's, not your host's. The host-side :Torio panel lists projects, opens routine or push-capable terminals, reports health, and shows open sessions.
VS Code / Cursor, over Remote-SSHAdd Include ~/.lima/torio/ssh.config to your ~/.ssh/config so the lima-torio host resolves, then Connect to Host → lima-torio. Include the file rather than copying a port: Lima reassigns the SSH port across VM restarts. See the caveat below the table.
Claude Code, or another terminal AI agentRun the agent inside the VM as claude, pointed at the checkout, so its edits and commits land as claude. Leave pushes, remote changes, and credential setup to an operator session. Installing the agent and its runtime in the VM is outside the Torio control plane.

Caveat for Remote-SSH: it connects as the Lima user, not claude, and installs a server component into that user's guest home — so saving files in the claude-owned tree needs the remote window's integrated terminal running sudo -iu claude for Git and for checks. For a single-identity session, project enter is cleaner.

The everyday loop

Once the VM and backend are up, the day-to-day loop is the same whichever editor or interface you use:

  1. Work in a checkout — from a Desktop session, your own editor, or torio project enter <id>.
  2. Edit, or let your AI tool edit, files there.
  3. Run a check that reads rather than writes.
  4. Review what changed: git diff and git status.
  5. Decide whether any of it should leave the VM.
  6. If it should: torio project shell <id>, commit, push, exit.

Steps 5 and 6 are the split: Torio forwards operator write capability only inside a session you opened, and stops forwarding it when you exit.

Watch several agents at once

Running more than one box, the question stops being "what is this agent doing" and becomes "which of them needs me". torio status answers it in one row per box:

$ torio status
INSTANCE           BOX      BACKEND      SESSION  WAITING              PROGRESS
torio-codex        running  codex        —        ?                    14s
torio-claude-code  running  claude-code  2        yes 7m pid 11673 +1  —
torio-codex        running  codex        1        no                   —

Asking is still asking, though, and the answer is worth having without asking. Put it on your status bar:

$ torio status setup tmux >> ~/.tmux.conf
$ tmux source-file ~/.tmux.conf

The command prints the configuration to stdout and writes nothing; the redirect above is your decision. On a terminal with no multiplexer, use torio status setup zsh >> ~/.zshrc instead — the prompt is then the surface, and the snippet keeps the poll out of it so your shell never waits on a VM. Each shell writes to its own private temporary file and the prompt reads only a completed refresh. A very short command can leave the previous refresh visible; the next one catches up without ever placing a guest poll in prompt expansion.

One chip per box arrives, and exactly one state is loud: the box that wants you inverts, so it is found without reading. A live agent gets a dot and its count, a backend that keeps no session process gets how long ago it last did work, and a stopped box is barely there.

Work out why something isn't running

Most first-run failures are one of the cases below. On failure torio exits non-zero with a specific code and prints a diagnostic to stderr; match the symptom, then apply the fix.

What you seeWhat it means, and what to do
zsh: command not found: torioThe binary is not on your PATH. Build it and install it once — go build -o torio ./cmd/torio then sudo install -m 755 torio /usr/local/bin/torio — or run it in place as ./torio from the repository root.
torio: no command given; run 'torio --help' (exit 2)You ran torio with no subcommand somewhere that is not a terminal, such as a pipeline or a CI job, or you passed --json. Add a subcommand, for example torio vm status, or run torio --help to list the command surface. On a terminal the same invocation opens the hub instead.
torio: the hub requires a terminal … (exit 3)torio ui was run where standard input and standard output are not both a terminal. Run it in a terminal, or use an individual command.
A vm command fails mentioning limactl (exit 8)Lima is not installed, or limactl is not on your PATH. Install Lima and confirm limactl runs; Torio drives the VM through it.
torio: stopped, or a precondition error (exit 3)The VM is not running. Start it with torio vm start, then re-run your command.
torio: not_found from torio vm status (exit 0)No VM exists yet. This is the answer on a host that has never run Torio; create one with torio vm init.
torio: timeout … exceeds policy maximum 10m0s (exit 2)--timeout is capped at ten minutes for any single operation, and the check runs before any work. Ask for 10m or less.
torio mcp status reports that policy services require loginThe broker and policy are installed, but the unit is intentionally dormant. Run torio mcp login <service> for every reported policy service; the last successful login starts the unit.
torio mcp login <service> cannot open its callback listenerLocal port 43119 is already in use or the SSH callback forward could not bind. Stop the process using that loopback port and retry; do not widen the bind address.
torio project add prints a deploy key and exits 7Add the public key to that repository as a deploy key with write access off, then run the same command again. Do not add it to your account; Torio cannot verify the forge setting.
torio project shell refuses before opening a sessionA preflight did not hold — most often an empty SSH agent. Check ssh-add -l lists an identity; the other causes (project not registered, VM not bootstrap-verified, checkout missing) are named in the error.
agent init failed: No … credentials storedNo provider is configured on the guest. Run the interactive picker as the agent identity in a real shell — see Configure a model provider. torio vm ssh cannot do this; it forwards no TTY.
A file you piped through torio vm ssh is empty, but the command exited 0torio vm ssh forwards no stdin, so `… \torio vm ssh -- … tee file writes nothing and still reports success. Create the file in an interactive shell (limactl shell torio-claude-code, then sudo -iu claude`) instead.

Add --json to a non-interactive command for a single machine-readable envelope on stdout; human diagnostics stay on stderr.

The full exit-code table is in Reference.