Get started is complete on this page — every command you need is here, in order. Links only ever take you to optional detail.

Tutorials

Tutorials

The complete first-run walkthrough.

Get started

By the end of this page you will have built the CLI, created the Linux VM, created your Second Brain, attached your first repository, and opened a real agent session in it. Git remote writes remain an operator action.

Work straight down. Nothing here sends you to another page to finish a step; the links are for going deeper afterwards.

Before you start

You need all of the following in place first:

Torio creates the VM itself, so there is nothing to provision by hand first.

Step 1 — Install the CLI

Every command in these docs is written as torio, so put the binary on your PATH once and the rest of the page just works.

Where a release exists, install its verified asset. From a checkout of the torio repository:

scripts/install.sh                       # latest stable release
scripts/install.sh --version X.Y.Z       # or a specific one, without the leading v

The installer resolves the release, verifies SHA256SUMS before the binary is copied anywhere — which is the whole reason to use it rather than untarring by hand — and installs into ~/.local/bin by default. It needs a published release to exist; before one does, build from source below.

install.sh authenticates to nothing and never will. Torio stores and transports no credentials, and an installer carrying a forwarded token would be the one exception that makes the claim untrue. So a repository it cannot read anonymously answers 404 from api.github.com, and gh — which does hold your credentials — closes that gap without Torio touching them:

gh release download vX.Y.Z -D /tmp/torio-rel
scripts/install.sh --version X.Y.Z --base-url file:///tmp/torio-rel

Either route verifies the same checksums. Set TORIO_REPO=owner/name if the assets live somewhere other than the default.

Installing a dev build

To try what is on main before it is released:

scripts/install.sh --channel dev

This installs the build of the last commit that reached main, into ~/.local/share/torio-dev/bin, and links it into ~/.local/bin under the name torio-dev. A stable install keeps its own directory and its own name, so both are available at once and neither overwrites the other's guest payloads. Rerun the same command to move to a newer build. --link-dir DIR links it somewhere else, --no-link skips the link.

A dev build is not a release. It carries whatever reached main and has passed the pull-request gate; the release gates that boot a guest and install the macOS archive on a Mac have not run against it. Checksums are verified exactly as they are for a release, and torio-dev version reports the commit it was built from.

The two installs are separate binaries, not separate states: torio-dev reads the same configuration and talks to the same boxes as torio. Where that matters, point it at a box of its own:

TORIO_INSTANCE=torio-devbox torio-dev vm status

Building from source instead

From a checkout, one command builds the working tree and installs it the same way, under a third name:

make local

It builds for this host only, installs into ~/.local/share/torio-local/bin, and links ~/.local/bin/torio-local. Nothing is published and no tag is touched. torio-local version reports the branch, the commit and whether the tree was dirty when it was built, so the binary names what you are testing. Run it again after a change to replace it, and delete the two paths above to be rid of it.

To place a binary yourself instead, with a Go toolchain:

go build -o torio ./cmd/torio
sudo install -m 755 torio /usr/local/bin/torio

Prefer not to install system-wide? Any directory already on your PATH works — for example install -m 755 torio ~/.local/bin/torio. If you skip this step entirely, torio will not resolve at all: a freshly built binary is not on your PATH and your shell does not search the current directory, so you would have to prefix every later command with ./ and run it from the repository root.

Confirm it resolves and runs:

which torio
torio version

version is the only place the operator reads which build they have:

torio dev (commit …, built …)
go1.26.5 darwin/arm64

A binary built straight from a checkout calls itself dev; the commit is the one you built.

torio vm status also works from here and answers torio: not_found until the next step creates the VM. That is the expected answer on a host that has never run Torio, not a failure — it exits 0.

Step 2 — Create and verify the VM

Create the VM from the trusted template, start it, then reconcile and verify it. Run all three in order: each is idempotent, so this is also the sequence you re-run later.

torio vm init
torio vm start
torio vm bootstrap --timeout 10m

init prints next: torio vm start whether it created the instance or found a compatible one, so there is no state in which you skip the second command.

init creates the pinned Lima instance (or succeeds idempotently when a compatible one already exists) and verifies the post-create list output before reporting success. start is idempotent and confirms a Running post-state. bootstrap operates only on the existing target after a verified Running precondition, through the typed Lima boundary. A backend install can be slow, so give it room — but 10m is the policy maximum for any single operation, and asking for more is refused before any work starts:

torio: timeout 15m0s exceeds policy maximum 10m0s

On a fully-reconciled target it mutates nothing; when the pinned binary is missing it installs the declared backend at its pin. It:

Any drift or unverifiable state fails closed (exit 6) with remediation. A rerun is success only when every postcondition is proven. Use --json for the machine-readable envelope (one document on stdout).

Persistent guest locations

The table below is the Claude Code layout; every backend has the same shape under its own identity.

WhatPath (on the VM's native Linux filesystem)
Guest userclaude
Home/home/claude
Profile / application state/home/claude/.claude
Second Brain vault/home/claude/brain
Workspace root/home/claude/projects

These are also emitted in the torio vm bootstrap output (human and --json).

init creates the VM; bootstrap then verifies the guest layout above on an already-running instance. Neither recreates or re-images it.

Step 3 — Create the Second Brain

The Brain is a private Markdown vault on the guest at /home/claude/brain, versioned by a local Git repository, with a retrieval skill installed where the backend discovers skills — so any session can search it without you opening it first.

torio brain init
torio brain status

init builds the scaffold atomically through private guest staging, makes the first local commit, and installs the global torio-brain retrieval skill. It is idempotent on state it manages, and it refuses to touch non-empty data it did not create — so a second run is safe and an existing vault is never silently absorbed.

It configures no remote and pushes nothing. The Brain stays on the VM.

A session that was already open will not see a skill installed after it started: a backend assembles its skills prompt once, per process. Open a new session. torio brain status reports the file it verified, which is the state Torio can prove.

Bring an existing vault in

If you already keep Markdown notes on your host — an Obsidian vault, say — import them once:

torio brain import ~/path/to/vault --dry-run   # preflight, transfers nothing
torio brain import ~/path/to/vault

Start with --dry-run: it runs the full preflight and reports what would move, without transferring a byte or touching Brain data.

The import is deliberately narrow. It carries Markdown, Canvas, and local attachments, and refuses or skips credential-shaped files, repository metadata, links, hardlinks, special files, and executables. Existing data is never overwritten — the one exception is a scaffold Torio itself created and you have not touched. To land the vault as one contained subtree instead of merging it into the root, pass --into notes.

Output is counts, bytes, and a manifest digest. No note name and no note content ever appears in it.

Getting it back out

Torio brings data in and does not take it out — there is no export command. Copying the Brain to your host is something you do explicitly:

limactl copy torio:/home/claude/brain/ ~/torio-brain-copy/

That is your command. Nothing verifies the result, and Torio does not call it a backup.

Step 4 — Attach your first 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.

Step 5 — 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.

Step 6 — Learn 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.

Step 7 — Leave a clean end state

Leave the VM running. No project remote was modified.

If a step failed, known first-run failures and fixes are in why something isn't running.

Where to go next