# Cool Computers for agents

> Step-by-step instructions for an agent that was asked to set up or use Cool Computers for a user.

You are an agent. A user asked you to set up or use Cool Computers: computers at `name.cool.computer` with a public address, an inbox, and a disk that stays. Work through the steps below in order. Do not use the website's sign-in form; it is for humans in a browser, and it will stall you at a code screen.

Install the `cool` CLI in step 1 unless you cannot run shell commands; every step shows it first. Only without a shell, use the HTTP calls under it. The CLI prints JSON when you pass `--json`.

## 1. Check for existing access

Make a scratch directory for responses, install the CLI (this also updates an older install to the latest release), then run `cool whoami`:

```sh
WORK="$(mktemp -d)"
curl -fsSL https://cool.computer/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
cool whoami --json
```

If it succeeds and the `email` it prints is the user you are working for, you are signed in: set `TOKEN="$COOL_API_KEY"` if that variable exists and skip to step 3. If it is someone else's account, stop and ask the user; do not act under it. If it fails with `"status_code":401`, you hold no working credential. When `COOL_API_KEY` was set, that key is dead: `unset COOL_API_KEY` (sign-in refuses to run while a key is set) and tell the user. Then, if the `401` that brought you here carried `WWW-Authenticate: ... resource_metadata=...`, you are in the standards-based flow: follow [/auth.md](/auth.md), which ends with `COOL_API_KEY` set, and continue at step 3. Otherwise continue with step 2. Any non-`401` failure is transient; see Errors and retry before deciding.

If your only access is an MCP client connected to `https://api.cool.computer/mcp`, use `cool_list_computers` for the name check in step 3, then call `cool_publish` with `{"name":"alexheath","files":[{"path":"index.html","content":"..."}]}`, where `content` is the whole welcome page with the user's name and address HTML-escaped into it.

Never ask the user to paste an API key into the conversation. If they have one, ask them to put it in `COOL_API_KEY` and resume.

## 2. Sign the user up, or sign them in

Sign-up is open to everyone. Ask the user for their email address and set `EMAIL` to exactly what they gave you (the address below is only an example of the shape). New and existing accounts use the same two commands: the first sends a six-digit code to that inbox, the second exchanges it.

```sh
EMAIL="the-users-address@example.com"
cool signup --email "$EMAIL" --json
```

Tell the user a code was sent and wait for them to give it to you. Set `CODE` to the six digits they relay, then:

```sh
CODE="the six digits the user gave you"
printf '%s\n' "$CODE" | cool signup --email "$EMAIL" --code-stdin --json
```

The CLI stores the credential; you are signed in. Run `cool signup` once per code request: each run sends a new email. Do not read the user's inbox for them.

Without a shell, make the same two calls over HTTP:

```sh
curl https://api.cool.computer/api/auth/email/start \
  -H 'content-type: application/json' --data "{\"email\":\"$EMAIL\"}"
curl -sS -o "$WORK/session.json" -w '%{http_code}\n' https://api.cool.computer/api/auth/email/complete \
  -H 'content-type: application/json' --data "{\"email\":\"$EMAIL\",\"code\":\"$CODE\"}"
TOKEN="$(jq -er .access_token "$WORK/session.json")"
```

The printed status tells you which Errors entry applies (a wrong or expired code is `401`; ask the user for a fresh one). `TOKEN` now holds an access token, and it expires after the response's `expires_in` seconds (about five minutes). Before anything else, trade it for a durable API key named after you; every step below uses that key:

```sh
curl -sS -o "$WORK/key.json" -w '%{http_code}\n' https://api.cool.computer/api/api-keys -H "authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' --data '{"name":"agent"}'
COOL_API_KEY="$(jq -er .api_key "$WORK/key.json")" && TOKEN="$COOL_API_KEY"
```

The printed status tells you which Errors entry applies. The key is returned once; store it where you keep the user's secrets, never print it, and delete `$WORK` when you are done. If the call fails, do not retry it: a key may already exist.

## 3. Publish the user's welcome page

The user's first computer is a site: publish a small, well-made page that greets them by name, and it is live at once at its own address, with no Linux running behind it.

Name it after the user. Ask for their name if you do not have it, lowercase it, and drop anything that is not an ASCII letter or digit: Alex Heath becomes `alexheath.cool.computer`. Names are 1–56 lowercase letters or digits; `api`, `mail`, and `www` are reserved. If nothing is left after that, or more than 56 characters are, ask the user for a short name instead of guessing.

This step creates a new computer. First run `cool list --json`: if a computer with that `slug` is already in the list, the user or someone who shares with them already has it, and it may hold their work. Stop there, tell the user, and ask whether to use it (then follow their instructions, inspecting before you change anything) or to publish under a different name.

This is the page; put the user's HTML-escaped name and address into it and save it as `welcome.html`:

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Welcome, Alex</title>
  <style>
    body { margin: 0; min-height: 100vh; display: grid; place-items: center; font-family: system-ui, sans-serif; background: #0e0e11; color: #fff; }
    main { text-align: center; padding: 2rem; }
    h1 { font-size: clamp(2rem, 6vw, 4rem); margin: 0 0 .5rem; }
    p { color: #b3b3b8; margin: 0; }
  </style>
</head>
<body>
  <main>
    <h1>Welcome, Alex!</h1>
    <p>This is alexheath.cool.computer, your computer on the internet.</p>
  </main>
</body>
</html>
```

Then publish it:

```sh
cool publish welcome.html --name alexheath --json
```

The output gives the computer's `url` and its `computer.id`; set `ID` to that `computer.id`, since the share and exec steps below use it. A `409` with code `slug_taken` means someone else owns that name; add the user's initial or a short word and try once more. The CLI retries a lost response itself; if it still reports a network failure, do not rerun it: check `cool list --json` and continue from what exists.

Over HTTP, `GET https://api.cool.computer/api/computers` with the bearer is the same list check. Then send the page itself as the body, with an idempotency key:

```sh
PUBLISH_KEY="publish-alexheath-$(date +%s)"
curl -sS -D "$WORK/publish.headers" -o "$WORK/publish.json" -w '%{http_code}\n' "https://api.cool.computer/api/publish?name=alexheath" \
  -H "authorization: Bearer $TOKEN" -H "idempotency-key: $PUBLISH_KEY" \
  -H 'content-type: text/html' --data-binary "@welcome.html"
ID="$(jq -er .computer.id "$WORK/publish.json")"
```

The printed status, `publish.headers` (for `Retry-After`) and `publish.json` (for the problem `code`) tell you which Errors entry applies; `publish.json` holds the computer on `201`. If the request fails with a `5xx` or no response, rerun only the `curl` line with the same `PUBLISH_KEY`, so a retry cannot publish twice. A `409` whose `code` is `idempotency_in_progress` is that retry arriving early: wait `Retry-After` seconds and rerun it again, unchanged. `ID` now holds the computer's id; the HTTP examples below use it. The full contract is [/openapi.json](/openapi.json).

Publishing does not start the trial: every new account gets a 3-day Pro trial, no card, starting the first time a computer runs Linux; see [pricing](/pricing). Confirm the result before you report it:

```sh
curl -fsS https://alexheath.cool.computer | grep -c "Welcome, Alex"
```

## 4. Tell the user

Report, in this order:

1. The address: `https://alexheath.cool.computer`, and that the welcome page is live there.
2. What the computer is: `alexheath@mail.cool.computer` is its inbox and works now. Linux, with Claude Code and Codex installed, arrives the first time they or you run `cool exec alexheath -- uname -a` or `cool enter alexheath` (see "When a site gets Linux" below); that also starts the trial, and from then on files and installed tools persist.
3. Where you stored their credential and how to revoke it: `cool logout` for the CLI sign-in; for an API key, `cool api-keys list` to find its ID, then `cool api-keys revoke "$KEY_ID" --force` with that ID in `KEY_ID` (without `--force` it asks for confirmation), both from a signed-in shell.

## Publish a site

Publishing changes a site that has no Linux: it is instant and needs no Linux computer. To run something, use the computer with the CLI or the HTTP API (`cool exec`, `cool enter`, `cool service run`); what changes then is under "When a site gets Linux" below. Read files with `cool files` or the HTTP files API instead of running a command to look at them. MCP has no command or file tools, so use the CLI or HTTP for both.

When the user wants a website or browser app online, publish its files: they are served at the site's own address, with no Linux computer running behind them. Build framework apps first (for example `npm run build`), on your machine or any computer, and publish the build output; publishing never runs a build.

```sh
cool publish ./dist --name alexheathsite --json
```

That creates a new computer serving `./dist` at `https://alexheathsite.cool.computer`. The output gives `url` and `release_url`, this release's own address, such as `https://r1--alexheathsite.cool.computer`. To update it, run `cool publish ./dist --computer alexheathsite --json`; only changed files are uploaded. One file works too: `cool publish welcome.html` publishes it as `index.html`.

Over HTTP, send the directory as one gzip tar with an idempotency key:

```sh
tar -czf "$WORK/site.tar.gz" -C dist .
SITE_KEY="site-alexheathsite-$(date +%s)"
curl -sS -o "$WORK/site.json" -w '%{http_code}\n' "https://api.cool.computer/api/publish?name=alexheathsite" \
  -H "authorization: Bearer $TOKEN" -H "idempotency-key: $SITE_KEY" \
  -H 'content-type: application/gzip' --data-binary "@$WORK/site.tar.gz"
```

Use `?computer=alexheathsite` instead of `?name=` to publish to an existing site. Add `&visibility=private` next to `?name=` to create the site private, so only the user's account and the people they share it with can open it. On a `5xx` or no response, rerun only the `curl` line with the same `SITE_KEY`. With MCP, call `cool_publish` with `files`, each a `path` with text `content` or `content_base64` (add `"executable": true` for a script the server runs), and `name` or `computer`; its arguments are limited to 64 KiB in total, so publish anything larger with `cool publish` or `POST /api/publish`.

Without an account, `cool publish ./dist` (or the same `curl` with no `authorization` and no `idempotency-key` header) still publishes files, up to 100 MiB, to a new public site that is deleted after 24 hours unless claimed; give the user the `claim_url` from the output, whose key is shown only once, and publish to it again with `cool publish ./dist --claim "$CLAIM_URL"` or `?computer=$ID` plus `authorization: Claim $CLAIM_KEY`.
To keep it, the user opens the `claim_url` they were given in a browser and signs in, or a signed-in shell runs `cool claim "$CLAIM_URL"` (or `POST /api/computers/$ID/claim` with `{"claim_key": "..."}`).

A `cool.json` at the root of the files configures the site:

- `{"spa": true}` serves `index.html` for paths that are not files, for apps that route in the browser.
- `{"run": "node server.js", "port": 3000}` is a shortcut for starting the app's server by hand (`port` defaults to 3000): publishing it to a site with no Linux gives the site its Linux computer, writes the files to `/home/runtime/app`, where the site serves them at once, then runs the command there as the service, which answers every path no file matches. The site keeps serving while the program starts and if it fails. Every file in `/home/runtime/app` is public on the site's URL (hidden paths such as `.env`, `node_modules/` and `cool.json` excepted), so keep keys out of it: put them in a secret set (`cool secrets set`). `node_modules/` is never published, so install dependencies in the command: `"run": "npm ci && node server.js"`. Files keep their execute bit, so `run` can be `./start.sh`. The site then has Linux; see "When a site gets Linux" below.
- `{"functions": "api/worker.js"}` names one bundled ES module (`.js` or `.mjs`, at most 1 MiB; bundle its imports first, e.g. `esbuild api.js --bundle --format=esm --outfile=api/worker.js`) whose `export default { fetch(request, env) }` answers every request under `/api/` without a running Linux computer; every other path stays a file, and the module itself is not served. Use only Web-standard APIs (`fetch`, `Request`, `Response`, `crypto`); each request gets 50 ms of CPU and 20 outgoing requests, and there is no storage.
- `{"functions": "api/worker.js", "secrets": "site-keys"}` gives the functions the keys of an account secret set as `env`: create it with `cool secrets set site-keys STRIPE_KEY=sk_... OPENAI_KEY=sk-... --proxy OPENAI_KEY=api.openai.com` (or `PUT /api/secrets/site-keys`). A key set with `--proxy` reaches functions once the site has its Linux computer, and then only as `runtime-proxy`: send it as `Authorization: Bearer runtime-proxy` and the computer's egress proxy sets the real value on `https` requests to that host, so the function never holds it; before that the function gets only the plain keys. Changing or deleting the set reaches published functions within seconds of the command returning (on the edge a replaced script can take up to a minute to serve everywhere); a release naming a set that does not exist fails (`422 secret_set_not_found`). A private site checks the visitor before any function runs.
- `functions` needs an account and cannot be combined with `run` (`422 functions_and_run`).

### When a site gets Linux

The first `cool exec`, `cool enter` or `cool service run` on a published site gives it its Linux computer and waits until it is ready. Before the command runs, the site moves to that computer by itself: the published files are in `/home/runtime/app`, the computer's built-in site server serves them exactly as before, functions and secrets included (the module is kept out of the served folder, under `/home/runtime/.cool/functions`), and the URL never stops answering. From then on `/home/runtime/app` is the site: edit a file there with `cool files` or inside `cool enter` and it is live at once; hidden files such as `.env`, `node_modules/` and `cool.json` are never served. A program you start with `cool service run` answers only the paths no file matches, and stopping it never takes the site down. From then on, change it with `cool files` and `cool exec`; publishing to it is refused (`409 computer_has_vm`), so publish to a new name for a separate site. Without a Linux computer, `cool files` reads the published files and writing or deleting one publishes a new release.

## Private page, then share it

When the user wants a page only they and a colleague can open, publish it private from the start, so it is never public, then share it with the colleague's email. There is no share link: ask the user for the colleague's email address and set `COLLEAGUE` to exactly what they gave you.

```sh
COLLEAGUE="the-colleagues-address@example.com"
cool publish notes.html --name alexheathnotes --visibility private --json
cool share add alexheathnotes "$COLLEAGUE" --json
```

Over HTTP, publish as in step 3 with `visibility=private` added, set `ID` from `computer.id` in its response, then share:

```sh
NOTES_KEY="publish-alexheathnotes-$(date +%s)"
curl -sS -o "$WORK/notes.json" -w '%{http_code}\n' "https://api.cool.computer/api/publish?name=alexheathnotes&visibility=private" \
  -H "authorization: Bearer $TOKEN" -H "idempotency-key: $NOTES_KEY" \
  -H 'content-type: text/html' --data-binary "@notes.html"
ID="$(jq -er .computer.id "$WORK/notes.json")"
curl -sS -o "$WORK/share.json" -w '%{http_code}\n' "https://api.cool.computer/api/computers/$ID/shares" \
  -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' --data "{\"email\":\"$COLLEAGUE\"}"
```

A signed-out visitor to a private site is sent to sign in. The colleague gets an email invitation, signs in with that address, and can open `https://alexheathnotes.cool.computer`. MCP can neither publish a private page nor share one, so use the CLI or HTTP for this recipe.

## Then keep going

- Run commands with `cool exec alexheath -- uname -a`; the first one gives a published site its Linux computer.
- Over HTTP, `POST /api/computers/$ID/exec` with `{"command":"uname -a"}` waits for the result, and the server mints the `execution_id` when you send none. Send your own (the computer's `ID`, `.exec.`, and 32 new hex digits) when a lost response must not run the command twice: send the same `execution_id` to `/exec/attach` instead of running it again:

```sh
EXEC_ID="$ID.exec.$(openssl rand -hex 16)"
curl -sS -o "$WORK/exec.json" -w '%{http_code}\n' "https://api.cool.computer/api/computers/$ID/exec" \
  -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  --data "{\"execution_id\":\"$EXEC_ID\",\"command\":\"uname -a\"}"
jq -r '.stdout, .stderr, .exit_code' "$WORK/exec.json"
```

- Read and write files with `cool files`; copy whole directories with `cool files upload` and `cool files download`. A relative path is relative to `/home/runtime`.
- Change the public service with `cool service run`; check it with `cool service status` and `cool service logs`.
- Run something on a schedule with a systemd timer; there is no cron, so do not install one. `cool exec alexheath -- sudo systemd-run --uid=runtime --working-directory=/home/runtime --unit=report --on-calendar='*:0/15' /home/runtime/report.sh` runs `report.sh` every 15 minutes, and `systemctl list-timers` shows it. That timer ends if the computer restarts; `cool help reference` shows how to keep one.
- For everything else read [/docs.md](/docs.md) and [/openapi.json](/openapi.json).

Ask the user before you delete a computer, send email from it, install a durable command that runs on its own, or do anything that costs money. Inspect before you change anything.

## Errors

- `401`: missing, expired, or revoked credential. If you came through `/auth.md`, re-exchange your assertion there; otherwise sign in again (step 2). If an API key stopped working, it was revoked; delete your copy.
- `403 access_token_required`: API keys cannot manage API keys; send the access token from email sign-in (`cool login`, or step 2) instead.
- `403 interactive_required` while completing sign-in: the account needs the website; give the user https://www.cool.computer and wait.
- `402`: the trial ended or the plan limit was reached. Tell the user; see [pricing](/pricing).
- `409` on publish: read the problem `code`. `slug_taken`: pick a variant once, then ask the user. `idempotency_in_progress`: wait `Retry-After` seconds and rerun the identical request. `idempotency_outcome_unknown`: do not rerun; check `cool list --json` and continue from what exists. `publish_in_progress`: another publish of that site is running; wait and retry.
- `429`: rate limited. Wait for `Retry-After`. Never loop on `cool signup`.
- `5xx` or no response: temporary failure. For HTTP requests, back off and rerun the same request, reusing its `PUBLISH_KEY` or `SITE_KEY` when it has one; never regenerate an idempotency key for a retry. Never rerun `POST /api/auth/email/start` on your own: each call sends another code, so ask the user whether one arrived before requesting a new one. After a lost `cool create` or `cool service run` response, which send no idempotency key, do not rerun it: check `cool list --json` or `cool service status`, and continue from what already exists. API-key creation is never retried: a key may already exist, so ask the user to check `cool api-keys list` first.

## Reference

- [/auth.md](/auth.md): the standards-based WorkOS agent registration, for agents arriving from a `401` challenge.
- [/authentication.md](/authentication.md): every credential type and how they differ.
- [/llms.txt](/llms.txt): the index of everything here.
- [https://cool.computer/docs](https://cool.computer/docs): the full guide.
