# Connect Steery

Steery lets your person answer questions and approve actions from their phone. A `steery`
binary on this machine sends the questions and checks the answers.

Server: `https://steery.dev`. Use `--url` or `STEERY_URL` if your person gives another.

## How to run this

You are the installer. Read this whole page, then do every step yourself, in order,
without stopping between them. Your person taps three times in the whole installation —
four on a machine with no Secure Enclave or TPM, where keeping the key in a file asks its
own consent (`steery setup --detect` says: `custody.file_consent_required`) — and you
set each one up so it is a tap and not a task:

1. **Pick the harnesses** — from a list you present. Buttons or numbers, never typing.
   Picking one that is not installed is also the yes to its vendor's installer, so it
   costs no tap of its own.
2. **Confirm a sign-in** in the browser tab that opens in front of them. One click.
   (First time only.)
3. **Confirm fingerprints** — from a list you present. (First time only.)

Everything else — installing, detecting, configuring, creating keys, pinning, starting
the runtime, checking it works — is yours, and none of it needs them. Setup ends once its
harnesses, runtime and signer are set up. Rules:

- **Never hand a command to the person to run.** If a step needs them, it needs a click or
  a choice, and you ask for exactly that.
- **Never end your turn with the installation unfinished.** Waiting is not the end. The
  commands below wait for the person themselves — you pass `--wait-secs` and let them
  block — so there is nothing to poll and no reason to come back without an answer. Exit
  code `2` of `steery setup` means *the person's move, then run me again*; it is never a
  reason to stop, and neither is a person taking their time. `install.sh` is not
  `steery setup`: it exits only `0` (done) or `1`, never `2`. `1` is a fault to fix
  (its stderr says why), except when the script ran `steery setup` itself and setup
  returned its `2`: setup's own sentence says whose move it is, the last line on stdout
  is `next: <the command to run again>`, and the exit is still `1`. You run
  `--install-only` and drive `steery setup` yourself, so you read setup's `2` directly.
- **Ask with options, never with an open question.** When your harness has a way to offer
  choices (buttons, a menu, a picker), use it. Otherwise list numbered options and ask for
  the number. Never ask them to type a name, a path, or a fingerprint.
- **Never work around a refusal.** Every `steery` error says how to fix it. Fix that.
- **File what goes wrong, at the moment it goes wrong.** A `steery` command that fails
  in a way its message did not predict, a step that stuck, an exit code that does not
  match this page: run `steery bugs create` right then — before fixing it, before moving
  on, and whether or not you get past it — with the command, its exit code and its
  output in `context`, secrets replaced with `[redacted]`. Then carry on. It works from
  the first step, before setup has run: it signs the person in if it has to. A bug you
  meant to file at the end is a bug nobody hears about.
- **Every step, every time.** A machine that already has Steery goes through the same
  page: the binary is replaced with the current one, the person chooses the harnesses
  again, and setup checks all of it. The sign-in and the fingerprints are skipped, because
  the machine already has its key and its pins and proved
  that path when it got them — so a re-run costs the person one tap, and you still finish
  by telling them what stands.

The whole thing takes two or three minutes when you keep moving.


## 1. Install the binary

macOS and Linux.

```sh
curl -fsSL https://steery.dev/install.sh | sh -s -- --install-only
steery --version
```

Run this **even if `steery` is already installed**, and do not compare versions first: it
installs the published build every time, replacing whatever is there, so the machine is
on the current one before anything else happens. Checks the SHA-256 first, never uses
`sudo`. `steery` goes into a folder of its own under `~/.local/bin/.steery/`, is run
(`steery --version`), and only then does `~/.local/bin/steery`, a link into it, move onto
it. A download that fails, or a binary that does not run here, leaves what was installed
as it was; a binary that does not run says why when the system said (a C library too old
for it, or none, as on Alpine). Then it adds the programs Steery reaches harnesses and
machines through - the drivers and the adapters - from their sources: `steery programs add
--yes`, which with no source adds Steery's own (https://github.com/steery-dev/drivers and
https://github.com/steery-dev/adapters), downloads each one's build for this computer,
checks its SHA-256, starts it once to check it, and records it under
`~/.config/steery/programs/`. `--install-only` stops after that; the rest of this page is
you.

**If it exits `1` after the binary went in, the programs were not added.** A source that
cannot be added is said with its cause (usually GitHub's release downloads cannot be
reached), the other is still tried, and the run ends with exit `1` naming the source and
printing no `next:` line. Fix the network and run the same command again, or add them
with `steery programs add --yes` (by the full path below). Do not go on to `steery
setup` with them missing: setup tries Steery's own two sources once more when it finds
them missing, but it only says what it could not add and then stops at the first step
that needs a driver, `no driver for it here`.

If `steery --version` fails, `~/.local/bin` is not on `PATH`: the script says so on stderr
(`… is not on your PATH`) and its last line on stdout, `next: <full path> setup`, names
the binary. Use that full path for the rest of this page in place of `steery`, and add
the directory to the person's shell profile (`~/.zshrc` or `~/.bashrc`) yourself.

## Where the work runs

Every machine in Steery is one kind of thing: it joins the same way, it is **Personal**
(its person's) or **Shared** (a workspace's), and both are switched on steery.dev's
**Machines** page for every machine, this one included. What differs is what runs its
work, and `steery setup --adapter <id>` says that:

- **This computer** (`--adapter local`, the default): agents run here, in the harnesses
  you set up below. This is what nearly every installation is.
- **An adapter's hosts** (`--adapter daytona`, `--adapter opensandbox`, or any adapter
  added with `steery programs add` that makes hosts): this computer only joins the
  machine to Steery. Every conversation runs in a host of its own that the adapter makes
  at the provider, and no agent runs on this computer: setup writes nothing into its
  harnesses and the runtime starts no turn here. The harnesses are set up inside each host, so `--harness`,
  `--install` and step 2 do not apply.

Ask your person only when they have not said: "Should agents run on this computer, or in
Daytona or OpenSandbox hosts?" as options. Without `--adapter` at a terminal, setup asks
the same question itself; run by you, with no terminal, say it. A machine set up before
keeps its choice unless `--adapter` names another.

An adapter's settings are its own, declared in its `initialize`, and given with
`--setting NAME=VALUE` or `--setting NAME`, which takes the value from your environment.
**Give a secret as `--setting NAME` with the variable exported**, never as `NAME=VALUE`:
then it is in no command line, no transcript and no log. Setup checks the settings with
the provider (it lists the hosts labelled with this machine's key) before it keeps
anything; a refusal is exit `4` with the provider's own line, which never holds the
secret. They are kept at 0600 in `~/.config/steery/machine.json` and never printed again.
`--source image:<ref>` or `--source snapshot:<name>` is what each host is made from: an
image or snapshot your person prepared, with their harness in it set up as they want it.
Absent, the adapter's own default (Daytona's default snapshot; OpenSandbox has none, so
give one).

The full commands, with nothing asked. The sign-in and fingerprints of step 3
are the same for every one of them; follow that step with these flags.

**This computer:**

```sh
curl -fsSL https://steery.dev/install.sh | sh -s -- --install-only
steery setup --adapter local --harness claude-code,pi,omp,codex --browser --wait-secs 900
```

**This computer, with the work in Daytona hosts** (settings: `DAYTONA_API_KEY`, required
and secret, from app.daytona.io/dashboard/keys; `DAYTONA_API_URL` for a self-hosted
Daytona; `DAYTONA_TARGET`, the region, `us` or `eu`; `DAYTONA_ENV`, secret, the environment
every sandbox gets, one `NAME=VALUE` a line). Harnesses and their sign-ins come the Daytona
way: in the snapshot (`--source snapshot:<name>`) and in `DAYTONA_ENV`. A machine whose
hosts run no harness says why for each, and Steery offers its person a conversation that
builds that snapshot:

```sh
curl -fsSL https://steery.dev/install.sh | sh -s -- --install-only
export DAYTONA_API_KEY=…   # from your person, never echoed back
steery setup --adapter daytona --setting DAYTONA_API_KEY --browser --wait-secs 900
# optional: --setting DAYTONA_TARGET=eu --source snapshot:<name>
```

**This computer, with the work in OpenSandbox hosts** (settings: `OPENSANDBOX_URL`,
required, the server's URL as its SDKs take it; `OPENSANDBOX_API_KEY`, secret, the
server's `server.api_key` where it has one):

```sh
curl -fsSL https://steery.dev/install.sh | sh -s -- --install-only
export OPENSANDBOX_API_KEY=…   # only where the server has one
steery setup --adapter opensandbox --setting OPENSANDBOX_URL=http://localhost:8080 \
  --setting OPENSANDBOX_API_KEY --source image:<ref> --browser --wait-secs 900
```

`install.sh` takes the same `--adapter`, `--setting` and `--source` and hands them to
setup, for a person who runs it at a terminal or a script that runs it whole.

**Daytona's or OpenSandbox's cloud, with no computer at all.** Your person adds that
machine on steery.dev's **Machines** page: **Add machine**, then *Daytona cloud* or
*OpenSandbox cloud*, the settings, a name, Personal or Shared. Steery's server runs it,
keeps its settings sealed and never hands them to a computer; nothing is installed and
this page does not apply. Tell your person that, and stop here, when that is what they
want.



## 2. Detect the harnesses and ask which to set up

```sh
steery setup --detect
```

Prints `{custody, harnesses, places, signers, this}` as JSON and writes nothing. The
harnesses are those this machine's drivers plan for, each reporting `harness`, `label`,
`short`, `tint`, `mark`, `binary`, `version`, `root`, `notes`, `refusal`, `usable`,
`configured`, `on` and `install`. `custody` reports `availability`, `backend`, `reason`
and `file_consent_required`; `signers` is `null` until this machine holds a sign-in.

Offer the usable harnesses, and beside them each one not installed here whose `install`
is set: `install.about` is its vendor's own installer, with the exact command it runs
(`install.argv`). With no harness at all, setup still enrolls the machine and pins
signers; attachment can follow.

**A harness older than its driver supports is not usable.** Each driver reads its
harness's version and says the oldest it works with; an older one reports `usable: false`,
its `refusal` the line to say to your person, for instance "omp 18.3.0 is older than
Steery needs (18.4.12): run `omp update`, then `steery setup`." Setup writes nothing for it. Naming it
(`steery setup --harness omp …`) fails with that line; a run that names no harness prints
it as a note and sets up the rest. Updating the harness and running setup again sets it up. A
version that did not answer claims nothing and is never refused for one. On a machine
already set up, a harness found older than its minimum reads Missing on every surface,
with the same line, until it is updated.

When hardware custody is unavailable, ask for explicit consent before adding
`--custody file`. A recorded file-custody choice needs no second consent.

Now ask your person — **always, including on a machine that is set up already**. Offer
every usable harness as an option, with the `configured` ones and the one you are
pre-selected, and say which one you are — the harness running this installation is the
one they most want Steery in. Offer each installable one too, not pre-selected, saying it
is installed first with its vendor's installer and the command that runs. Use your choice
UI if you have one; a numbered list otherwise. Example:

> Steery can be set up in these: **Claude Code** (that's me), **pi**, **omp**. **Codex**
> isn't installed: I can install it with its vendor's own installer, which runs
> `<install.argv, exactly as detect printed it>`. All of them? Or pick.

One answer. Do not ask anything else about harnesses. What they choose is exactly what
ends up set up: a harness that was set up before and is not chosen now is given back.
Each harness chosen receives its MCP entry, canonical skill, and supported hooks. Picking
one that is not installed is the person's yes to its installer: pass `--install` below,
and only then.

## 3. Sign in, set up, and pin — one command that waits

```sh
steery setup --harness claude-code,pi,omp,codex --browser --wait-secs 900
```

The harnesses they chose, and the flags that make this one command instead of a
polling loop:

- `--install` installs each chosen harness that is not installed here with its vendor's
  own installer, its output kept in `~/.config/steery/logs/install-<harness>.log`, before
  anything else. Pass it only when they picked one that is not installed; without it a
  harness named and not installed is refused, naming the installer it would run.

- `--browser` opens the sign-in page on this machine's display, so the tab lands in
  front of the person and all they do is confirm. Leave it off only where there is no
  display in front of them — a server, a container, CI — and deliver the link yourself.
- `--wait-secs 900` is how long setup will sit on each step that is the person's:
  confirming the sign-in, and enrolling a phone if they have none. It polls; you do not.
  Fifteen minutes is the right number for somebody who is right there. Without it, and
  with nobody at this terminal, setup waits 900 seconds anyway; `--wait-secs 0` hands
  every step back to you at once.

Setup does everything in order: puts this machine's reader key in the chosen custody,
signs the person in, creates this machine's key, pins the signers the person trusts, and
only then writes into each harness chosen (giving back any not chosen), starts the
runtime, and checks all of it. Nothing is written into a harness before this machine can
answer on the address it writes, so a setup that stops at the person's move leaves every
harness exactly as it was.

What goes into a harness is what its driver plans for it (`setup/plan`), and Steery writes
exactly that and records it: the **MCP entry** it reaches Steery
through, the **skill** that tells its agent what to do with it, and — where the harness
has them — **hooks** on its own lifecycle events, so the person's phone can say which of
their conversations is working, which is waiting on a call, which is asking them, and
which is finished. Claude Code and Codex have hooks; omp and pi load
theirs as a code module, so each gets Steery's one extension, `extensions/steery.ts`
under its agent directory, which in pi also carries the MCP entry. Codex runs a hook only once you have
trusted it in Codex itself, so the hooks setup writes there wait for you; until then its
conversations are seen from outside, from their transcripts.

The extension does more than a hook table can, and draws nothing: nothing of Steery
appears inside a harness, and your person answers on Steery's own surfaces. It holds one door to the runtime, a
WebSocket to `ws://127.0.0.1:<port>/door` on the runtime's own loopback listener. Over
it the extension says what the conversation is doing as it changes — its moments, the
tool call it is in, a dialog open, its model and context left — and the runtime hands
it words the person threw, which the harness takes as that person's next input: a new
turn when it is idle, a steer between tool calls while it works. A moment said while the
door is down — the runtime not started yet, or restarting — is kept in order on the
harness's process and sent once the door is back, and placed when it happened. The door
speaks one release: a copy of the extension from another Steery is turned away, and the
loader moves the conversation onto the copy this runtime wrote within seconds.

pi 1.0.0 loads MCP servers itself, and Steery's extension registers its entry, `steery`,
for each session with `pi.registerMcpServer`, so nothing is written into `<pi
root>/mcp.json`: `{"type": "http", "url": "http://127.0.0.1:<port>/mcp", "timeout":
<seconds>, "exposure": "direct"}`, with
headers naming the conversation (`Steery-Harness`, `Steery-External-Id`, and
`Steery-Sub-Agent` for a sub-agent), so every Steery call is filed under the conversation
pi says made it. `timeout` is the same bound every harness gets. `direct` gives
pi's model each of the six record tools by name with its whole schema,
`mcp__steery__<tool>`; pi's default would offer them only inside code-mode scripts. A
`steery` entry in your own `<pi root>/mcp.json`, or in a project's `.pi/mcp.json` once you
trusted that project in pi, wins over the registration: its calls carry no headers, and
setup says so when it finds one it did not write, in `<pi root>/mcp.json` or in the
`.pi/mcp.json` of a trusted folder pi's trust store names, a conversation worked in, or
setup runs in.

A new conversation picks the extension up, and with it the entry; one
already open moves onto a newer copy within seconds, and reaches Steery's tools at once,
with no `/reload`. A pi conversation left open since a Steery that gave it `steery_<tool>`
tools keeps those, dead, until its next `/reload`, `/new`, resume, fork or restart, beside
the working `mcp__steery__<tool>` ones. If the runtime is down when pi starts, pi waits at
most its own 10 s for the entry, the conversation runs without Steery, and the extension
connects the entry again once the door is back.
If pi and omp are both installed and `PI_CODING_AGENT_DIR` or `PI_CONFIG_DIR` points them
at one directory, setup refuses both and says so: the skill and the extension are one
file each and cannot belong to two harnesses. Unset the variable, or give each its own
directory, and run setup again.

Each of those hooks runs `steery hook <harness> <event>` (in omp and pi the extension
says the same moment over its door instead), which writes one small file under
`~/.config/steery/` and says what the conversation is doing. Where the moment is one the
harness stops at to ask its own person — a permission, a question, a plan — in a
conversation Steery started, it raises that as a request and, once a verified answer comes,
hands the harness that answer in its own words. A conversation you started is tracked only:
its harness asks you itself, and Steery raises no request for it. Anything else — no signer
pinned, the server unreachable, no answer in time — prints nothing and exits 0, and the
harness asks its person exactly as it would without
Steery: a hook never refuses a tool call on its own. `steery setup --disable --harness <harness>`
takes them out again along with everything else, leaving the person's own hooks on those
events exactly where they were.

Exit `0` means local setup completed. Exit `2` means sign-in, enrollment, or fingerprint
confirmation still needs the person.

**Say the one line that gets the click, then run it.** The command blocks, so the person
has to be told before it starts, not after:

> I'm opening a Steery sign-in page in your browser — confirm it there and I'll take it
> from there.

Then run it and let it block. Do not narrate, do not poll, do not ask them to tell you
when they are done: you will see it. If the fifteen minutes run out, setup prints the
link and the code as JSON and exits `2` — put them in front of the person again and run
the same command:

```json
{"sign_in": {"url": "https://steery.dev/oauth/device?user_code=…", "code": "2SZD-9TJ4", "expires_at": 1789310085}}
```

The code is good for 10 minutes; running setup again picks the same one back up, and
starts a fresh one once it has expired. Each attempt emits the same `sign_in` shape on
stderr before it waits, including resumed attempts. The secret polling credential is
never printed. The sign-in is used once, to create this
machine's key — after that the machine uses its own key and the person never signs in
for it again.

**No phone enrolled yet?** Setup waits for that too, on the same `--wait-secs`. It says
so and prints the link the person needs; tell them, in one line, and let it keep
waiting:

> Steery needs your phone: install the Steery app from https://testflight.apple.com/join/Px7xdt92 and
> sign in there. I'm watching for it.

If the wait runs out, it exits `2` with `{"signers": [], "enrol": "https://testflight.apple.com/join/Px7xdt92"}`
— same move, run it again.

### Exit 2 with `signers` — the person confirms fingerprints

```json
{"signers": [{"id": "…", "name": "Aram's iPhone", "kind": "phone", "fingerprint": "591990aef9cac1c4", "server_held": false, "pinned": false}]}
```

The one step that is genuinely a decision, and the step that protects your person from
Steery itself. An answer only counts if it was signed by a phone whose fingerprint they
checked, so the server can never invent one. You cannot check it for them — but you make
it one look and one tap.

Show them each signer that is not yet `pinned`, with its fingerprint **exactly as printed**
— the app shows the same 16 characters, so do not space or split it — and ask them to
compare it with what the Steery app shows under **Settings** on that phone. Offer the
signers as options, each pre-selected. Mark `server_held` signers as such:

> Trust these? Check each fingerprint against **Settings** in the Steery app on that phone.
> - **Aram's iPhone** — `591990aef9cac1c4`
>
> Matches?

A `server_held` signer (Telegram, Slack) has its key on the server, so it does not protect
against the server. List it, say so, and ask the person to untick it if they refuse it.

For each one they confirmed, add `--trust <fingerprint>` and run the same command again:

```sh
steery setup --harness claude-code,pi,omp,codex --browser --wait-secs 900 --trust 591990aef9cac1c4
```

Exit `0`: pinned, runtime started, done. Setup never pins anything you did not name, and
never pins a fingerprint that is not one of the workspace's live signers.

### Anything else

Any other non-zero exit is an error with the fix in its message. Read it, fix it, run
setup again. Common ones are at the bottom of this page.

## 4. What to tell them

Setup does not exit `0` on trust. Before it does, it reads every harness file back and
compares it with what it wrote, calls the local MCP service at the same URL it wrote into
those harnesses and asks it for its tools, and asks the supervisor whether `steery run` is
running. It prints what it found under `Checked:`, and any of those failing is exit `4`
with the reason — fix it and run setup again. Setup ends there: it raises nothing on the
person's phone.

Then tell your person, in a few lines: which harnesses are set up, which signer is
pinned, that the runtime is running, and that the local MCP service answered. On a Mac,
say that Steery is now in their menu bar and that opening it shows what is waiting.

**Tell them to start new sessions.** A harness reads its MCP entry when a session
starts, so every session already open in a harness you just set up — including this one,
if you are one of them — does not have Steery yet. Say which harnesses, and that the next
session in each will.

Open the installed skill and read it now — `skills/steery/SKILL.md` under your harness's
own directory — because it says how every conversation keeps its record on your
person's phone from here on: its title, workstream, chapters and lines. How to ask your
person, wait for their answer and park is told only to a conversation Steery's words
steer, by Steery itself, when they do.

## The runtime

`steery run` is the resident process, the same binary. It does five things.

**Connects your agents to your person.** The local MCP service your harnesses call runs
inside this process. It raises their requests and returns signed responses, checked
against the signers pinned on this machine before an agent receives them.

**Says what this machine is working on.** When it changes, and at least every five
seconds, it sends the conversations it can see, so your person sees them in the app. Each
harness's driver lists them and reads their transcripts (`sessions/list`, `sessions/read`):
what each conversation is doing, its turns, calls and tokens, and what its person typed. A
conversation nothing has touched for three days is left out.

**Starts a turn only for a throw.** A throw is words for a conversation, or for a new one,
that the server delivers to this machine: your person's from the app, or what they set up
to be sent. The runtime checks the throw's work with the server, then starts or continues
the turn through the harness's driver. Nothing else here starts a turn or writes into a
conversation. Your person answers the questions your agents raise, and the agent that
asked acts on the answer.

**Summarizes a conversation, if and only if your person says so.** Your person taps
**Share with others…** beside a conversation in the app, or a Steery-started agent runs
`steery shares create`. That queues a suggestion and nothing else. The runtime finds the
conversation on this machine, plans its read on the conversation's own harness, model and
thinking level, and raises an ordinary request naming the conversation, that reader and
where a draft would go —
then waits. Not one line of the transcript is read until your person's signed answer to
that request verifies against the signers pinned here. **Summarize and share** starts it;
**Don't read it** withdraws the whole thing.

What comes back is a redacted summary, published as a page with one unlisted link on the
answer that let this machine write it; deleting the page ends the link. Revoking the share
before then cancels a summary still being written, kills the summarizer, and throws away a
summary that was finished a moment too late. The transcript itself never leaves this machine.

A summary that was interrupted — the machine slept, the runtime restarted — is marked
failed and is never run again on its own. Asking for it again is your person's to do.

The summarizer is the conversation's own harness, on nothing but that one conversation,
started as its driver says, with what its driver can switch off switched off. It reads
on the model and thinking level your person picked in the Share sheet, else the ones the
conversation runs on now. The driver says what the start leaves open (MCP servers it
cannot cut off, for one); that is named in the question that asks you, beside the
harness, model and thinking level, rather than being a reason to refuse it. A harness
whose driver has no reader, or one turned off on this machine, cannot summarize its
conversations: `shares get` says why. If it does not answer, the share fails and says
so; no other harness is tried.

A harness is found where its driver looks for it, on the `PATH` the driver was started
with. The runtime asks your login shell for its `PATH` (`$SHELL -i -l`) in the
background when it starts and every 30 minutes after, never while a call waits, and starts
a driver again when it changes, so a harness installed somewhere your login shell reaches
is found without running setup again. `steery install` writes the supervised unit with
the `PATH` of the shell you ran it from.

**Keeps the pinned signers honest.** It refreshes them every 15 minutes. A refresh can
only remove trust, never add it.

**Uses at most a share of this machine's CPU.** Everything the runtime starts — the agents
it continues, forks or starts, and the builds and tests they run — shares one limit: 70%
of the CPU this machine is granted by default, counting only the cores it may run on and
any quota a container, pod or slice sets above it. On Linux that is a hard cap
(`CPUQuota` on its systemd slice), and they also yield to everything else the machine
serves. On a Mac, launchd has no cap, so they only run at a lower priority. Setup at a
terminal asks for the share each time, beside the harnesses, showing roughly how many
cores each choice comes to; anywhere else the share stands. `--cpu <PERCENT>` on
`steery setup` or `steery install` sets it without asking; it is kept in
`~/.config/steery/runtime.json`. A service that runs work for whoever asks — docker's
daemon, for one — runs it outside this limit.

**Shows what is waiting, in the Mac menu bar.** On a Mac with a desktop in front of it,
the same process puts a small menu up there under Steery's own face: how many requests are
waiting to be answered, how many conversations this machine can see, how many signers are
pinned, and when it last reported. Every count opens onto the things it counts — the
requests themselves, the conversations grouped by the harness running them, the signers —
and each one goes to the dashboard page for exactly that, already narrowed to it. There is
no command for it and nothing to launch: it is `steery run`, the one process you already
started, so there is nothing extra to start and nothing extra that can stop. A machine with
no desktop — a server, a container, a session you reached over ssh — runs the same runtime
with no menu. That is not a failure and there is nothing to fix.

The same menu is also where your person renames this machine and where they quit it.
Neither is a command you can run for them.

**The machine's name** is the line at the top of the menu — whatever `steery setup` found
this machine calling itself. Clicking it opens a box with that name already in it, and
what they type replaces it everywhere Steery shows this machine: the dashboard, their
phone, every request it raises. Only the name changes; the key this machine is known by
and everything pinned against it are untouched, so nothing has to be set up or compared
again. The menu says the new name as soon as the server has taken it. They can do the same
thing from the machine's page in the dashboard, which is the only other way — there is no
command for it, so do not go looking for one, and do not rename their machine for them.

**Quit Steery…** is more than this process stopping, so it asks first and says what it
means: quitting removes the launchd supervision as well, so Steery does not come back at
login. The local HTTP MCP service, reports, and signer refresh stop with it. Stdio
connections are separate processes started by their harnesses. If they quit and then want it back, they
open Steery from Applications and it offers to put the service back; opening it on a Mac
that has no service asks the same question, and installing it is the same thing step 3
did.

Calls the runtime has already accepted get a bounded chance to finish. An agent
waiting on a question receives its current state, which may still be open, rather
than waiting for the person before Steery can quit. No new calls are accepted while
it stops, and an in-progress summary or other model process is not kept running.

```sh
steery run                     # the foreground, when you want to watch it
steery uninstall               # stop the supervised one
steery install                 # put it back
steery install --cpu 50        # put it back, using at most half of this machine's CPU
steery setup ... --no-service  # set up without supervising, for a container that runs it
```

## Later

**A new phone.** Two ways, no others: run `steery setup --trust <fingerprint>` after the
person compared it (the same loop as above, with the same choices) — or the person answers
from a phone that is already pinned, Steery asks *"Trust a new signer?"* with the new
fingerprint, and **Trust** pins it.

**Another harness, or one fewer.** Step 1 again, all the way through: the person picks
from the list, and what they pick is what is set up. `steery setup --harness <ids> …`
names the same thing from the command line — exactly those, and any other given back;
with `--install`, one not installed is installed first with its vendor's installer.

**Undo.** `steery setup --disable --harness omp` removes what Steery wrote into that
harness and restores what it replaced.

**Upgrade, check, or repair.** A supervised machine keeps itself on the published
build: it looks every five minutes, installs what is newer — the way step 1 does, a new
`steery` beside the old one, run first, then the one link moved, so a failed update
leaves the old one running — and rewrites each harness's MCP entry, skill and hooks for
the binary it just installed. Each program added from a source that names its latest
release (a repository, or a manifest's URL) is brought up to that release the same way,
its settings kept; `steery programs update` does it now. The build it is offered is the
one the server it talks to offers: each release serves its own `latest.json`, so a new
build and its server go live in one switch, and the machine looks the moment its event
stream comes back onto that server. `steery version` says which build this is, which is
offered, and whether auto-update is on;
`steery version --auto off` stops it, and
`steery version --update` installs now without waiting. Each look also reads the server's
`runtime_minimum`: where it is newer than this binary, the server serves this machine
nothing — every surface reads it Missing, "Steery 6.0.6 on Kasras-Air is older than this
server needs: it is updating" — and the published build is installed at once, whatever
the auto-update setting.

Step 1 again does the same by hand. It installs the current build, the person
picks the harnesses, setup brings them up to that binary, restarts the runtime and checks
all of it. The old runtime stops accepting new calls and finishes accepted request
waits with an immediate read of the same request. Those reads share a 30-second
deadline, with five seconds for delivery and another five allowed by the supervisor
for cleanup. The new runtime then starts at the same local address. An agent whose
question is still open can continue waiting on the same id; it must not raise another
copy. New calls are unavailable during that handover. A machine that was fine ends
the same way, and costs the person the one tap that picks the harnesses.

## A cloud machine

A Daytona or OpenSandbox machine in the provider's cloud needs no computer: a member adds
it on steery.dev's **Machines** page (**Add machine** → *Daytona cloud* or *OpenSandbox
cloud*), and Steery's server runs it, with its settings sealed there. Each conversation
runs in a host of its own, made when the conversation starts, stopped after ten minutes
with no turn and no door, started again when the conversation is used; what Steery puts
in a host is `steery`, its programs and each harness's MCP entry, skill and hooks, so no
Steery key is ever in one. A host gone from the provider is the conversation's backing
lost: the person is asked on the phone what to do with it. A computer set up with
`--adapter` (above) runs its hosts the same way.

Every machine, computer or cloud, is renamed, made shared or personal, and removed on the
same page; a cloud machine's settings are changed there too, and removing it deletes its
hosts at the provider.

The image or snapshot is yours, with your harness in it set up as you set it up;
OpenSandbox has no default image, Daytona uses its default snapshot. What Steery adds to
each host is `steery`, the programs added on the computer or server that runs the
machine, and each harness's MCP entry, skill and hooks — nothing else — so no Steery key
is ever in a host. For each program it takes the build its source published for the
host's system and checks its SHA-256; a program with no such build is not carried, and its
harness reads Missing in that host, "no build of <id> for <target>". Where a host has
another set, the machine streams this one into it over the host's channel, runs it there
(`steery --version`, `steery programs check`) and only then moves `~/.local/bin/steery`
onto it. Then each harness found in the host is set up there, once per set, pointed at the
host's own `steery host serve`, whose connections the machine carries home. A provider
that cannot be reached is retried.

## Commands

Built in: `login logout setup run mcp install uninstall version events programs host
help`.

`steery programs` lists the drivers and adapters added here (JSON): each one's id, kind,
release, source, and whether it runs or why not. `steery programs add <source>…` adds
more: a GitHub repository (its latest release, followed from then on),
`<repository>@<tag>` (that release), a release manifest's URL, a program archive's URL or
path, or a built program's path or a folder of them. Nothing is ever built on this
computer. Each program is downloaded for this computer, its SHA-256 checked, started once
and checked (`steery programs test <command>` makes the same checks on a program you are
writing), and its plan printed; at a terminal you answer `y` to add it, and `--yes` adds it
unattended. `--only <id>,…` takes some of a source's programs. A driver's settings, an API
key for instance, are given with `--setting NAME=VALUE` or `--setting NAME` (from your
environment), or typed at the terminal where secret; Steery keeps them and starts the
driver with them, so it never needs your shell's environment. An adapter's settings are
given to `steery setup --adapter` (above), or on the web where a cloud machine is added.
`steery programs remove <id>` takes one away, and `steery programs update` brings the
followed ones to their latest release. A program that changed on disk after it was added
is not started until it is added again, which keeps its settings. `steery programs pack`
writes the release a program's author publishes (https://github.com/steery-dev/sdk says
how to write one).

Everything else comes from the server: `steery <group> <verb> --field value`. A group with
no verb lists its verbs. A wrong flag prints the right ones. `--json '{…}'` gives the whole
input. Results are JSON on stdout. A field of bytes takes `@path` and the client reads the
file, so a screenshot never passes through a model's context:
`steery artifacts create --bytes @/tmp/after.png` prints the URL a request shows it at.

Exit codes: `0` done, `1` the person rejected, `2` still open — someone else's move, run it
again — `4` error or unverified.

How a conversation keeps its record — its title, workstream, chapters and lines — is the
skill setup installs (`skills/steery/SKILL.md`), which every install and every
auto-update rewrites for the binary on disk. How to ask, build the form, read an answer,
park and work beside other conversations is told only to a Steery-started conversation:
its hook adds a short brief once, at its first turn and at every fresh context after, and
each part it names is printed by `steery help <part>`. A Steery-started conversation asks
with the CLI — `steery requests create`, then it ends its turn — and the answer arrives as
its next message. A conversation its person started is tracked only. Your person's
escalation policies — the conditions under which a Steery-started conversation asks
before it acts — are a list on the server, read with `steery policy get`. When your
person asks you to change them, `steery policy put` proposes the whole list; they approve
its `draft.request` on a signer.

## From an agent (MCP)

`steery run` serves the local HTTP MCP service. `steery mcp` serves the same tools over
stdin and stdout for a harness that starts its connection process. The runtime remains
responsible for session reports and revoked-pin refresh. Stdio ends when its input
closes; stdout contains only protocol messages. Every MCP — this machine's and the hosted
one — lists the six tools that keep a conversation's record and nothing else:
`sessions_describe`, `sessions_tag`, `tags_list`, `tags_put`, `usage_get`,
`sessions_query`. Everything else is the `steery` CLI.

Sharing a conversation is `steery shares create` and `steery shares get` and nothing
else; there is no local file to hand anybody. `shares create` returns immediately with a
`status` and an `id` — `queued` means your person has not been asked yet — and
`shares get --wait` on that id answers with a `url`, its page's link, once the summary is
published. A `failed` share carries the reason in `error`; a `revoked` one means they said no.

Publishing what an agent made — a report, a built folder, a PDF — is `steery pages publish
--slug <slug> --title "<title>" --bytes @/abs/folder-or-file` (a folder is zipped here); its
answer's `url` is the page's link, the same for every version. `steery pages get`, `list` and
`delete` read and end pages.

## From your own chat (Claude, ChatGPT)

A person's own door is `https://steery.dev/mcp/person`. Add it as a connector in Claude
or ChatGPT; it opens the Steery sign-in and lists four tools, the same for every person:

- `inbox_list`: your open requests, cut to what a chat reads.
- `throw`: say what you want; Steery picks the conversation and holds the words, showing
  the aim and the closest choices beside it. Nothing is sent.
- `throw_submit`: send the shown aim, or one of its choices.
- `sessions_list`: your conversations.

It is a person's grant, not an agent's: no key, nothing in `keys list`, ended from Agents.
It answers no request. A request a policy holds for your Go — a throw a policy asks
about, work on a machine, anything signed — is read in the chat and answered on your
phone alone, by your signer, which never leaves it. An agent harness given this URL is
refused: agents use `/mcp`.

## Without the binary (the HTTP API directly)

If your agent calls `https://steery.dev/v1` directly, the server could fake an answer.
Verify yourself, or use the binary.

1. Pin the phone's fingerprint (from the phone's Settings) in the agent's own config.
2. Rebuild the message from your own copy of the request:
   ```text
   steery.response.v1\n{workspace}\n{request}\n{signer}\n{sha256(content)}\n{sha256(response)}\n{at}\n
   content  = canonical {title, body, payload?, view, input}   (as you sent it)
   response = canonical {values, payload?}                     (payload = the person's edit)
   ```
   Canonical JSON is RFC 8785. Hashes are lowercase hex SHA-256. `at` is the exact string
   in the response.
3. Check `response.signature` (ECDSA P-256, DER, base64url) with the pinned public key.
4. Trust only your own copy of the request, not the one returned.

## Files

All in `~/.config/steery`, mode 0600: `key.json` (this machine's key), `bearer.json`
(sign-in, 10 minutes), `pending.json` (a sign-in waiting to be confirmed), `custody.json`
and the pinned signers inside custody, `harnesses.json` (what Steery wrote), `raised/`
(your copy of each request), `shares/` (this machine's own record of a summary it was
asked for: which conversation, which harness, model and thinking level read it, which request authorized it, and the
draft until the server has it), `runners.json` (the harnesses in your order, which are off,
the places a new conversation may start in, and whether it works in a worktree), `programs/` (each
program's record, `<id>.json`, and its executable under `<id>/`), `logs/` (each
program's standard error and each harness installer's output), `run.log`; and where the
work runs in an adapter's hosts, `machine.json` (the adapter, its settings, the source
its hosts are made from), `hosts.json` (the host of each conversation) and `hosts/` (what
was set up in each).

## If something goes wrong

Every error says how to fix it. None of them is a reason to work around Steery.

- **Steery itself is wrong** — a command that failed, an answer that would not verify, a
  step that stuck, an exit code this page did not predict: file it **then and there**.
  `steery bugs create --title "..." --body "..." --context '{...}'`, with the command,
  its exit code and its output in `context`, and secrets, tokens, keys, file contents and
  anything the person wrote replaced with `[redacted]` — the server refuses a report
  that still carries one. It needs no setup and works from the first step. Then fix what
  the error says and carry on; filing is never the end of the installation.
- **"this machine has pinned no signers"** — the runtime refuses to start, exit `4`, and
  says so on stderr — and in `run.log` when the supervisor started it. Go through step
  3's `signers` loop: `steery setup` prints them (or `enrol`, if there is no phone yet),
  the person compares, you `--trust`.
- **"this machine pinned its signers for `<other url>`"** — an earlier install pinned
  against a different server, so those pins say nothing about this one. Every other
  command still works; the runtime will not start until this server has pins. Same fix:
  the `signers` loop, and `steery setup … --trust` replaces the old pins with pins for
  this server.
- **"no signer … has the fingerprint"** — a typo, or a signer that was revoked. Run
  `steery signers list` and offer the live ones again.
- **"This response cannot be trusted on this machine"** — the signer is not pinned, or the
  signature does not match. Nothing ran. This is correct.
- **"this steery does not declare what … answers with"** — the server offers an operation
  this binary's verification contract does not cover. Nothing was sent. Install the
  current release using the procedure above; keep the existing key and pins.
- **"this machine will not serve the workspace's escalation policies"** — the
  workspace's own list could not be verified. Follow the bundled default and investigate
  the proof or the machine's trust configuration; never bypass the check or pin a signer
  blindly.
- **"… belongs to you now"** — a harness file has something Steery did not write. Show
  your person the file and the line; once they have moved it, run setup again.
- **"Nobody answered within … seconds"** — still open. Do something else. Do not retry.
- **Cannot reach Steery** — the call is refused. Nothing was authorized. Do not proceed.
- **"is not installed here"** — no executable found, and its driver names no installer
  here. `steery setup --detect` shows what was found; offer the `usable` ones and those
  with `install`.
- **"its driver is not installed here"** or **"no adapter for this computer's
  processes"** — the programs were not added, or a source could not be read when they
  were. Run `steery programs add --yes`; it says what it could not read.
- **"changed since it was added"** — a program's file is not what was checked when it was
  added. Add it again from its source with `steery programs add <source>`.
- **"not installed here: … add --install"** — a harness named is not installed. Ask your
  person whether to install it with the installer named; on a yes, run again with
  `--install`.
- **"its installer exited …"** — the vendor's installer failed; its whole output is in
  the log the line names. Say what it said, and set up the rest.
- **"sign-in failed: expired_token"** — the 10 minutes ran out. Run the same
  `steery setup … --browser --wait-secs 900` again: it starts a fresh code and opens it.
- **The app's World is empty** — the runtime is not running. `run.log` in
  `~/.config/steery` says why; `steery run` in the foreground says the same to you and
  exits non-zero. A machine only appears once it has reported.
