Skip to content

Scripting sessions

Alto’s commands start, read, type into, wait for and end sessions from a script or an agent: real sessions you can also see in any terminal, the app or your phone.

Terminal window
id=$(alto new -- npm run dev) # start one; prints its id
alto wait "$id" --text 'ready on port \d+' --timeout 60
alto capture "$id" --last 20 # the last lines, as plain text
alto send -s "$id" --key C-c # press keys
alto kill "$id"
Terminal window
alto wait 3 --until needs-you # idle, working, needs-you, done, or prompt
alto wait 3 --text 'error|panic' --timeout 2m

Exits 0 when it happens (printing what it found), 124 after --timeout, 125 if the session ends. --text is a regular expression over what the program prints from now on; for what’s already on screen, use capture --screen.

Terminal window
alto run 3 -- cargo test # prints only its output, exits with its status
alto run 3 --json -- make lint # {output, exit, truncated, took, timed_out}

run types the command at the session’s prompt, where it shows, and waits for it by the shell’s own marks. See What run gives back.

Terminal window
alto send -s 3 'git status' --enter
alto send -s 3 --key Escape # leave insert mode
alto send -s 3 ':wq' --enter

Keys go by name: Enter, Escape, Tab, Up, PageDown, F5, q, with C-, M- or S- before them (C-c, M-x, S-Tab). Repeat --key for several, pressed in order.

Name a session on another machine as machine:id, with any command that takes one; --on MACHINE does the same, and is how new and ls are pointed there:

Terminal window
alto --on studio new -- ./train.sh
alto capture studio:4 --last 50
alto run win:2 -- cargo build # PowerShell on Windows gives run its exit status too
alto new --in 'os:linux gpu' -- ./train.sh # or let Alto pick the machine

See Every session, every machine and Remote access with Tailscale.

Terminal window
alto events # a line each, as it happens, until stopped
alto events --json --type needs-you # {"v":1,"type","ts","device","text","session","address"}
alto events --all # every device you reach
alto events -s studio:4 --since 10m --once # what happened in one session lately, then stop

Sessions starting, ending, needing you and done; devices connecting, pairing, being forgotten or blocked. Each device keeps its last thousand. With --json, a {"type":"stream.ready"} line comes first. Lines capture gives back are whole, however the terminal wrapped them.

Terminal window
alto describe 3 # its kind (agent, shell or job), its state, and its methods
alto describe studio:3 --json # each method with what it takes and gives, as JSON Schema
alto call 3 capture --args '{"last":20}'
alto call studio:3 run --args '{"command":"cargo test","timeout":"10m"}' --json
echo '{"text":"y","enter":true}' | alto call 3 send --args -

Every session has the methods the session commands are: send, capture, wait, run, pause, resume and kill, each taking what its command takes, by the same names, and running the same code. Arguments are checked before anything happens, so a call refused changes nothing. With --json, a call prints {"ok": …} or {"error": {"code", "message"}}; the codes are no-such-method, bad-args, paused, timed-out, gone and failed. It exits 2 for no such method or wrong arguments, 124 when it timed out, 125 when it couldn’t, and run with its command’s status.

When an agent asks something (a permission prompt, a question), describe shows it as data, in its state’s asking: an id, the kind (permission, question, auth), the words, and the choices. Answer it from anywhere:

Terminal window
alto answer 3 allow # the question it's asking now
alto answer studio:3 deny --message "not on main"
alto call 3 answer --args '{"question":"e129c8b095c2d9a9","choice":"SQLite"}'

An answer names the question it’s for, and is refused (stale, exit 125) once the agent is asking something else, so it never lands on the wrong one. Claude Code’s prompt stays usable at the terminal while it waits: answer it either place, and the first answer wins. Codex’s answer is typed at its prompt (y, or Escape for deny), so it waits while a person types there, and is refused while agents are paused there. The text of a question is the agent’s: shown, never typed anywhere.

An agent’s session (kind agent) also has message: words for the agent, typed at its prompt once it’s idle, in turn, marked with who sent them and how to reply. The agent replies with alto reply CALL from its own session, and the reply comes back as data:

Terminal window
alto call studio:3 message --args '{"text":"how is the migration going?"}' --wait --timeout 10m
alto call studio:3 message --args '{"text":"tests pass?","reply":"{\"type\":\"object\",\"properties\":{\"pass\":{\"type\":\"boolean\"}},\"required\":[\"pass\"]}"}' --wait --json
# {"ok":{"call":"c1a2b3c4","state":"replied","reply":{"pass":true}}}

reply, a schema in the same subset written as JSON, is the shape the reply must have; a reply that isn’t is refused, and so is one from any session but the one asked. Without --wait, the call’s id comes back and the message waits its turn. --wait keeps asking until the reply comes, through a link that drops and comes back, and exits 124 after --timeout or 125 if the session ends first. Messages are kept by the agent’s device, so its service restarting loses none. Each message, delivery and reply is an event (alto events: message, delivered, replied).

A program in a session can offer methods of its own, which describe lists and call calls, from anywhere:

Terminal window
alto offer progress --about "how far the build is" \
--takes '{"type":"object","properties":{"detail":{"type":"boolean"}}}' \
-- ./progress.sh &
alto call studio:3 progress --args '{"detail":true}'

Each call runs the handler with the arguments as JSON on its standard input, and its standard output, JSON, is the reply. It runs as you, where you ran alto offer; the session itself runs nothing. A handler that takes longer than --limit (20 seconds) is stopped and the call fails as timed out. The offer lasts until the process that started it ends, or with --keep, until the session does; built-in names and names offered already are refused, and schemas are the same small subset describe shows. A caller needs send on the session to call it, unless the offer asks for less or more: --needs read for a method that only tells (status, progress), --needs approve for one that answers for the person (see Access). When the session moves to a new build (alto reload), the offer is made again there by itself.

A program can also say what it is and how it’s getting on, which describe, alto report and ls --json show:

Terminal window
alto report --kind build --set stage=tests --set target=x86
alto report --set target= # an empty value removes it

--kind is one word (build, server) in place of what the session would say (agent, shell or job); --set keeps up to 32 values of 256 characters each. They go when the program does: the shell back at its prompt, or an agent saying it ended. The same tools are in alto mcp as describe and call (The MCP server).

Every command that shows something takes --json: ls (with --all, --waiting or --group-by too), report, run, describe, call, events, work, workspace, devices, service, settings, hooks and fonts. Exit statuses are fixed and listed in alto --help: service status exits 3 while the service isn’t running, run and wait 124 on --timeout and 125 when they couldn’t.

ALTO_HOME=/some/folder keeps a separate service, sessions and settings in that folder, for trying things without touching your own:

Terminal window
export ALTO_HOME=$(mktemp -d)
alto new -- sleep 600
alto ls