Recipes
Each of these is meant to be copied. They work from a script or an agent's tool call, with no terminal.
Run one command on a throwaway machine
quivvo run --push . -- "npm ci && npm test"
run creates a machine, sends the current directory, runs the command there, deletes the machine and exits with the command's code. It waits for the whole command, so it's for work shorter than your own tool's timeout (often 120 seconds). For anything longer, have it start a job and answer right away:
quivvo run --push . --no-wait --timeout 1800 --json -- "npm ci && npm test"
That prints machine, job and the commands that come next: quivvo jobs wait, quivvo jobs logs and quivvo rm. The server has also been told to delete the machine, at the timeout plus 30 minutes. So if run is killed with no chance to clean up (SIGKILL), the machine bills until then at most, and on the Free plan it holds the one running slot. Any other signal, as most tool timeouts send, deletes it on the way out. To clean up, find it and delete it:
quivvo ls
quivvo jobs ls run-58658f99
quivvo jobs wait run-58658f99 j_8x0qvxi3k9 --timeout 100
quivvo rm run-58658f99 --yes
- Up to 280 seconds, it behaves like
exec, with stdout and stderr apart. A longer--timeoutmakes it a job, up to 86400 seconds, and their output arrives interleaved. --pushsends whatquivvo pushwould: what git tracks, no.git, no.env.--keepleaves the machine there.
Run something long in the background
A command that waits for its result stops at 280 seconds, and when a tool timeout (often 120 seconds) kills quivvo exec, the command is stopped too. Start anything that might outlast either as a job. It takes up to 86400 seconds, 3600 by default, and goes on if you disconnect.
quivvo create --name work --if-not-exists
JOB=$(quivvo exec work --background --timeout 1800 -- "npm test")
quivvo jobs wait work "$JOB" --timeout 100
jobs wait exits with the job's code. It exits 124 only when the wait ran out and the job is still running, so run it again. A job that hit its own timeout exits 1 here, and exit_code under --json holds the real 124. That means a retry loop can't spin forever.
quivvo jobs logs work "$JOB" --tail 50 # the last 50 lines
quivvo jobs logs work "$JOB" -f # follow until it ends, then exit with its code
quivvo jobs kill work "$JOB" # TERM by default
quivvo jobs ls work
A job ends as one of:
| State | Meaning |
|---|---|
exited |
It finished. exit_code is its code |
killed |
It was signalled. exit_code 124 means it hit its timeout |
lost |
Its process went away with its exit status unknown, usually because the machine restarted or stopped. Its log is still there |
A running job keeps the machine awake and billing, so kill the ones you don't need. To keep a server up for as long as a job can run, a day, pass that as its timeout, and start it again when it ends:
quivvo exec work --background --timeout 86400 -- "npm run dev"
Get a private repo onto a machine
Send your working copy. It's what git tracks, without .git or .env:
quivvo push ./api work
Add --include .git if you need to run git on the machine. To clone instead, forward a token. --env with no value sends your local one, and it stays out of history, where the command text would not:
quivvo exec work --env GITHUB_TOKEN -- "gh repo clone acme/api"
gh is on the universal template.
Hand a task to Claude Code or Codex
The universal template has both installed, not signed in. quivvo agent prompt starts one as a run that outlives your command. It checks first that the agent has credentials, and exits 3 with the fix if not. A bare --env KEY whose variable isn't set on your side exits 2 before that, saying so.
Claude Code
Pass one of these:
ANTHROPIC_API_KEY, an API key.CLAUDE_CODE_OAUTH_TOKEN, a long-lived token from your Claude login. Make one withclaude setup-tokenwherever you have a browser, and copy it from the output: it isn't saved anywhere.
RUN=$(quivvo agent prompt work --env CLAUDE_CODE_OAUTH_TOKEN --cwd api --name fix-test -- "fix the failing test")
quivvo agent wait work "$RUN" --timeout 100
quivvo agent result work "$RUN" --diff
Or sign in once on the machine with a person at the keyboard: quivvo ssh work -- claude. The sign-in stays on its disk.
Codex
Pass CODEX_API_KEY (OPENAI_API_KEY works too), or sign in once on the machine with quivvo ssh work -- codex login --device-auth.
RUN=$(quivvo agent prompt work --provider codex --env CODEX_API_KEY --cwd api --name fix-test -- "fix the failing test")
quivvo agent wait work "$RUN" --timeout 100
quivvo agent result work "$RUN"
What to know
agent waitexits 0 when it's done, 1 if the agent failed, and 124 if it's still running. Run it again on 124.--namemakes a retry safe: repeating it returns the same run instead of starting a second one.--continue RUNcarries on from an earlier run's session.--budget USDstops Claude Code at a spending limit.--timeout SECis an hour by default.agent result --jsongivesstate,exit_code,session_id,text,cost_usd,changed_filesanddiff_stat.- The prompt goes in a file on the machine, in
~/.quivvo/runs/<run>, and stays out of command history. - A running agent keeps the machine awake, and billing.
Everything the agent runs can read its key, as root. It runs with permission prompts off, and claude -p runs a repo's hooks and .mcp.json with no trust prompt, so only use code you trust with those keys.
Let an agent use a browser
The universal template has Chromium and Playwright MCP, already set up to use it. Add the MCP server to an agent once. It's saved in the agent's settings, on the machine's disk:
quivvo exec work -- claude mcp add -s user playwright -- playwright-mcp
quivvo exec work -- codex mcp add playwright -- playwright-mcp
Then give it a task that needs a browser:
RUN=$(quivvo agent prompt work --env CLAUDE_CODE_OAUTH_TOKEN --cwd app -- "start the dev server, open it in the browser and check that signing up works")
- The browser is headless unless the machine's desktop is running, and then you can watch it there: see watching on a desktop. Either way, the agent can save screenshots to a path for you to copy back:
quivvo cp work:/data/home/app/signup.png . - The browser keeps its profile, with cookies and sign-ins, under
~/.cacheon the persistent disk. chromiumis on the PATH for other tools, such as Puppeteer, orchromium --headless --screenshot=page.png https://example.com. It runs without Chromium's sandbox, which won't start as root.- A Playwright script of another version needs that version's Chromium: run
npx playwright install chromium, orplaywright install chromiumfor Python. It's installed outside/data, so a snapshot or fork doesn't copy it. - Chromium needs memory. If a page crashes or a command exits 137, use a bigger size.
Watch it on a desktop
The universal template has a desktop that runs only when you start it: a 1280x800 screen with a window manager, a terminal and Chromium on its right-click menu.
quivvo desktop work
That starts it and opens the machine's Desktop tab in the dashboard, where you can watch and take over with the mouse and keyboard. Opening the tab starts it too. With a VNC client instead, forward its port and connect to localhost:5901:
quivvo port-forward work 5901
- While it runs, a browser the agent opens through Playwright MCP appears on it, and so does any window a program on the machine opens. An agent started before the desktop keeps a headless browser until its next run.
xdotoolis there for an agent that drives the desktop itself, withDISPLAY=:1.- An open view keeps the machine awake, and billing, like an open shell. The desktop itself doesn't: with nobody watching and nothing running, the machine suspends as usual.
quivvo desktop work --stopstops it and closes its windows. A stop of the machine stops it too. It starts again the next time you open it.- Only the machine's own tunnel reaches it, with the machine's token, so VNC asks for no password.
Set a machine up once
quivvo create --name base --setup-file ./setup.sh --wait-setup
quivvo snapshot save base --name base-image
quivvo stop base
quivvo create --name work --snapshot base-image
A snapshot copies /data and nothing else. What your script leaves there, such as a cloned repo, node_modules or downloads, is on the new machine. What it installs anywhere else, such as a package from apt-get, stays on base, and nothing tells you it's missing until you need it. So keep the slow work under /data, and give the new machine its own script for the rest. It runs once the machine is up:
quivvo create --name work --snapshot base-image --setup-file ./install-packages.sh --wait-setup
The script is off the disk before its first line runs, so a snapshot doesn't copy it. Its output stays in the job's log, so don't print secrets. --wait-setup waits for the script and fails if it fails. setup on the machine shows state, exit_code and job_id, and the job's logs are the script's output. A failed script leaves the machine there and running, and billing, so you can see what went wrong. Delete it with quivvo rm before you try again: a setup script runs once, so --if-not-exists finds the same machine and doesn't run it again. The stop is for the Free plan, which runs one machine at a time.
Copies are made by the provider. If it won't make one, the answer is an error, nothing is created and the source is untouched: see provider_error.
Tell a coding agent about Quivvo
quivvo init installs the skill for the tools it finds. For a repo whose agents should all know, add this to its AGENTS.md or CLAUDE.md:
## Cloud machines
Run anything long, untrusted or heavy on a Quivvo machine, not on this computer.
- Start: `quivvo create --name work --if-not-exists`. Run: `quivvo exec work -- "..."`. Finish: `quivvo rm work --yes`.
- Anything that might outlast a tool call: `quivvo exec work --background --timeout 1800 -- "..."`, then `quivvo jobs wait`.
- More: https://tryquivvo.com/skill.md
Give your agent a computer
npx quivvo@latest sh