---
title: "Cool Computers for agents"
description: "Step-by-step instructions for an agent that was asked to set up or use Cool Computers for a user."
canonical: "https://www.cool.computer/agents"
last_updated: "2026-11-04"
---
# 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 in order. Publishing needs no account, so do not ask for an email, a code or a name before the page is live. 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.

Use the `cool` CLI (`--json` prints JSON). Without a shell, use the HTTP calls under each step. Only if you can neither run commands, make HTTP requests, nor call MCP tools, tell the user to add `https://api.cool.computer/mcp` as a connector in their app and ask you again, or send them to https://cool.computer.

If the user only wants to join the waitlist, do not install anything or send a code: ask for their email address and set `EMAIL` to exactly what they gave you, then:

```sh
EMAIL="the-users-address@example.com"
curl -sS -o /dev/null -w '%{http_code}\n' https://api.cool.computer/api/waitlist -H 'content-type: application/json' --data "{\"email\":\"$EMAIL\"}"
```

`202` means it worked, and it answers the same for an address that already has an account: tell them that if the address is new, an invitation arrives there when they are let in, and if they signed up before, they can sign in instead (step 4). Then stop. On `429`, wait a minute and run it once more; on any other status, tell them it did not go through.

## 1. Install the CLI and check for an account

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
```

On Windows, install it in PowerShell with `irm https://cool.computer/install.ps1 | iex` instead. The installer prints where it put `cool`. Each command you run may start a new shell that predates the install, so when a later command cannot find `cool`, run it by that full path: `$HOME/.local/bin/cool whoami --json`, or in PowerShell `& "$env:LOCALAPPDATA\Programs\coolcomputer\cool.exe" whoami --json`, and call every later `cool` command the same way.

What `cool whoami` answers decides how you publish:

- It prints the `email` of the user you are working for: you are signed in. Set `TOKEN="$COOL_API_KEY"` if that variable exists, and publish signed in.
- It prints someone else's `email`: stop and ask the user; do not act under that account.
- 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), tell the user, and run `cool whoami --json` again, since a saved sign-in may answer now; apply this list to its answer. When a saved sign-in answers `401`, clear it with `cool logout`. If the `401` that brought you here carried `WWW-Authenticate: ... resource_metadata=...`, follow [/auth.md](/auth.md), which ends with `COOL_API_KEY` set, and publish signed in. Otherwise publish signed out: the user keeps the site afterwards.
- Any other failure is transient; see Errors and retry before deciding.

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. Publish the page

Make what the user asked for, as HTML files. If they only asked to set up or try Cool Computers, publish a small welcome page. Do not wait for anything from the user first. Some sites need an account, so sign the user up (step 4) and come back to publish signed in: a page they want private, files whose `cool.json` declares `run` or `functions` (signed out, `403 sign_in_required_for_servers`), and a signed-out publish refused with `429` and code `unclaimed_computer_limit`, `anonymous_publish_capacity` or `anonymous_publish_rate_exceeded`.

Name the site after what it is, or after the user when you know their name: lowercase it and join its words with single hyphens, dropping anything that is not an ASCII letter or digit, so Alex Heath becomes `alex-heath.cool.computer`. Names are 1–56 lowercase letters and digits, with single hyphens between words (never two in a row, never at either end); `admin`, `admin-api`, `api`, `mail`, and `www` are reserved.

A welcome page can be this one; put the user's HTML-escaped name and address into it (leave the name out when you do not know 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 alex-heath.cool.computer, your computer on the internet.</p>
  </main>
</body>
</html>
```

**Signed out**, publish it:

```sh
cool publish welcome.html --name alex-heath --json
```

The site is public and is deleted at `expires_at`, 24 hours after it was made, unless the user keeps it. The output gives `url`, the computer's `computer.id`, and `claim_url` and `claim_key`, shown only once: set `ID`, `CLAIM_URL` and `CLAIM_KEY` to them and keep them for step 4. The CLI sends a signed-out publish once. If it reports a network failure and `https://alex-heath.cool.computer` answers anyway, the publish went through but its claim key is lost for good: publish again under another name to get a site the user can keep. Over HTTP, send the page with no `authorization` and no `idempotency-key` header:

```sh
curl -sS -o "$WORK/publish.json" -w '%{http_code}\n' "https://api.cool.computer/api/publish?name=alex-heath" \
  -H 'content-type: text/html' --data-binary "@welcome.html"
ID="$(jq -er .computer.id "$WORK/publish.json")"
CLAIM_URL="$(jq -er .claim_url "$WORK/publish.json")"
CLAIM_KEY="$(jq -er .claim_key "$WORK/publish.json")"
```

**Signed in**, the site goes onto the user's account and nothing expires. This creates a new computer, so first run `cool list --json`: if a computer with that `name` 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. Then publish it, adding `--visibility private` when the user wants only themselves and the people they share it with to open it:

```sh
cool publish welcome.html --name alex-heath --json
```

Set `ID` to the `computer.id` it prints. The CLI retries a lost signed-in 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 with the bearer and an idempotency key (add `&visibility=private` for a private site):

```sh
PUBLISH_KEY="publish-alex-heath-$(date +%s)"
curl -sS -D "$WORK/publish.headers" -o "$WORK/publish.json" -w '%{http_code}\n' "https://api.cool.computer/api/publish?name=alex-heath" \
  -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")"
```

On 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. With MCP (`https://api.cool.computer/mcp`, always signed in), check the one name with `cool_get_computer` (`{"computer":"alex-heath"}`; a not-found failure means the account has no computer by that name), since `cool_list_computers` lists a page at a time and a name may be on a later page until `next_cursor` is absent; then `cool_publish` with `{"name":"alex-heath","files":[{"path":"index.html","content":"..."}]}` publishes, where `content` is the whole page.

Either way, 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`. A `409` with code `name_taken` means another account owns that name; the error lists up to three free names (`suggestions` in JSON) and the `next` command, so rerun it with the first one. `name_is_yours` means the user already has a computer with that name: stop and ask as above. The full contract is [/openapi.json](/openapi.json).

Confirm a public page before you report it:

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

A private site shows a sign-in page to this request, so check it with `cool files read alex-heath index.html` instead, and say you checked the published files.

## 3. Tell the user, and ask them to keep it

Give them the address, `https://alex-heath.cool.computer`, and say whether it is public or private. If you run on their machine, open it: `open https://alex-heath.cool.computer` (macOS), `xdg-open https://alex-heath.cool.computer` (Linux) or `start https://alex-heath.cool.computer` (Windows).

Signed out, end with the ask to keep it, with `expires_at` as a time in the user's own time zone and the real `CLAIM_URL` filled in, for example:

> It's public, and it will be deleted at 18:40 tomorrow unless you keep it. To keep it, reply with your email address and I'll set up your account (you'll get a six-digit code), or open https://www.cool.computer/claim#cmp_0123.abcd and sign up there.

Then wait for their answer. The claim link works without you until then.

Signed in, add one line: its inbox, `alex-heath@mail.cool.computer`, works now. Mention a VM, which needs Pro (USD 20 a month), only when the task needs something to run (see "When a site gets a VM" below).

## 4. Sign the user up, and keep the site

When the user gives you their email address, or asks you to sign them up, set `EMAIL` to exactly what they gave you (the address below is only an example of the shape). While Cool Computers is full, a new email needs an invitation from Cool Computers, and one without it joins the waitlist (`403 sign_up_waitlisted`, below). 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. On a wrong or expired code, ask the user for the newest code in their inbox. When they have none, or it is rejected too (a code lasts ten minutes), tell them you will send a new one and run the first command again; each run sends another email. Do not read the user's inbox for them. If you signed them up before publishing anything (a site that needs an account, step 2), there is nothing to keep: go back to step 2 and publish signed in. Otherwise keep the site: it moves to their account with its name, address and releases, and its claim link stops working.

```sh
cool claim "$CLAIM_URL" --json
```

Without a shell, make the same 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`). `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. Then keep the site:

```sh
curl -sS -o "$WORK/claim.json" -w '%{http_code}\n' "https://api.cool.computer/api/computers/$ID/claim" \
  -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' --data "{\"claim_key\":\"$CLAIM_KEY\"}"
```

Claiming it again from the same account succeeds, so this also works after the user kept it in their browser. If sign-up answers `403 sign_up_waitlisted`, the site cannot be kept yet. Tell the user Cool Computers is full right now, their invitation arrives by email (or signing up again works later once there is room), and the page stays up until `expires_at`; keep its files, and publish them again signed in once they are in.

Once the site is theirs, tell them, add its inbox line from step 3, and say 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" --yes` with that ID in `KEY_ID`.

## When code needs a connected service

If the task needs Claude, Codex, Grok, Pi, or GitHub, inspect `cool integrations`,
then run the matching command: `cool integrations connect claude`,
`cool integrations connect codex`, `cool integrations connect grok`,
or `cool integrations connect github`.
Claude, Codex and Grok sign in on a temporary VM, so they need Pro: on Free, `connect` answers `402 pro_required`; tell the user (`cool billing upgrade`, or `cool billing portal` for a lapsed subscription). GitHub works on Free.
Pi uses your Claude and Codex connections. Automatic Codex selection requires the current image and a Codex connection; on older computers, choose Codex with `pi --model openai-codex/gpt-5` or in Pi's settings. Hand the approval link or
code to the human and wait for them to approve; never approve it on their behalf.

Inside an unshared computer, run `cool integrations` and use the token
exactly as its example shows, keeping wrappers such as `sk-ant-oat01-` and the
Codex JWT form; the proxy swaps in the real credential on matching requests. Git and gh work automatically when GitHub is
connected. Real credentials stay on Cool Computers' servers. Shared computers
report integrations withheld; use an unshared computer for these services.

## Publish a site

Publishing changes a site that has no VM: it is instant and needs none. Running something needs a VM: give the site one with `cool vm` (or `POST /api/computers/$ID/vm`, or MCP `cool_give_vm`), or start its program with `cool service run`, which gives it one; what changes then is under "When a site gets a VM" below. `cool exec` (MCP `cool_exec`), `cool enter` and `cool ssh` on a site without a VM answer `409 needs_vm` and change nothing. For a new computer to run things on, `cool create` makes one with its VM at once. Read files with `cool files`, the HTTP files API, or MCP `cool_list_files` and `cool_read_file` instead of running a command to look at them; none of them needs a VM. Over MCP, `cool_exec` runs one command and `cool_write_files` writes files on a computer that has a VM.

When the user wants a website, page, browser app or artifact online, or asks you to build them a "cool" one, publish its files: they are served at the site's own address, with no VM 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 alex-heath-site --json
```

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

A new site is public unless it is created private. Signed in, create it private unless the user wants it public: `cool publish ./dist --name alex-heath-site --visibility private --json` (MCP `visibility: "private"`), so only the user's account and the people they share it with can open it. `cool share public alex-heath-site` (MCP `cool_set_computer_visibility`) makes it public later. After a publish, set `URL` to the `url` it printed and tell the user; if you run on their machine, then run `open "$URL"` (macOS) or `xdg-open "$URL"` (Linux).

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-alex-heath-site-$(date +%s)"
curl -sS -o "$WORK/site.json" -w '%{http_code}\n' "https://api.cool.computer/api/publish?name=alex-heath-site" \
  -H "authorization: Bearer $TOKEN" -H "idempotency-key: $SITE_KEY" \
  -H 'content-type: application/gzip' --data-binary "@$WORK/site.tar.gz"
```

Use `?computer=alex-heath-site` 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` (with `visibility` set to `private` for a private site) or `computer`, a site's name or id; its arguments are limited to 64 KiB in total, so publish anything larger with `cool publish` or `POST /api/publish`.

Without an account, steps 2 to 4 apply. Until the site is kept, publish to it again with `cool publish ./dist --claim "$CLAIM_URL"` (HTTP: `?computer=$ID` plus `authorization: Claim $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 VM gives the site its own VM, writes the files to `/home/runtime/app`, where the site serves them at once, then runs the command there as the service, which, once it is ready, answers every path, `/api` and the site's files included (a `run` release declares no functions). The publish waits up to 60 seconds for the program's port to accept connections; a program still starting then is no failure, and the publish succeeds with a `Note:` that it keeps starting. A program that fails to start or ends before it is ready fails the publish with `502 server_start_failed`, but the release stays live and the site keeps its VM, also when the publish created it (unless the link to its VM did not move either; the error then says the created computer was deleted, so publish again): read why with `cool service logs alex-heath-site`, fix the program with `cool files` or `cool exec`, and start it again with `cool service run`. The site keeps serving its files while the program starts and if it fails; once the program is ready the folder is no longer served as files, so the program serves its own static files. Keep keys out of the folder anyway: 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 a VM; see "When a site gets a VM" 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 `/api` and every request under `/api/` without a VM; 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. Only the site's own URL gives the functions their secret set and `env.DB`; a release URL such as `r2--mysite.cool.computer` runs them without either, the live release's too, so test secrets and data on the site's URL. Add `"database": true` alongside `"functions"` to give the live release's edge functions a D1 database through `env.DB` (reserved as a secret key):

  ```js
  const rows = await env.DB.prepare("SELECT title FROM posts WHERE id = ?").bind(1).all();
  return Response.json(rows.results);
  ```
  Every `env.DB` call returns a promise: `await` each one, or the function fails with a `TypeError`. Inspect or seed the site's database with `cool db query alex-heath-site "SELECT * FROM posts LIMIT 10"` (use `--file schema.sql` to run a SQL file).
  When a request under `/api/` answers `function failed`, read why with `cool logs alex-heath-site`: the latest failures, newest first, each with the request's method and path, the release and the exception's name, message and first stack frame (the MCP tool is `cool_get_logs`; HTTP `GET /api/computers/$ID/logs`). Only an invocation that threw or passed its limit is kept; a function that answers an error status itself, and `console.log` output, are not. A private site shows its owner the same text in the failing response itself. A site with a VM runs its functions there; its failures are in the VM's log: `cool exec alex-heath-site -- journalctl -u runtime-frontdoor -n 100 --no-pager`.
- On the site's own URL the functions can send email from the site's address, such as `alex-heath@mail.cool.computer`, with `env.EMAIL.send({ to, subject, text })` (or `html`), so a contact form needs no VM and no form service. Each send counts as the computer's own `cool email send`: the same limits and billing, and it is stored in the computer's inbox. Add `idempotencyKey` to send at most once when retrying; a refused send throws an `Error` that says what to do next. A release URL runs the functions without `EMAIL`, and `EMAIL` is reserved as a secret key. For example, `api/worker.js` emailing the owner what a form posts to `/api/contact`:

  ```js
  export default {
    async fetch(request, env) {
      const form = await request.formData();
      await env.EMAIL.send({
        to: "you@example.com",
        subject: `Message from ${form.get("name")}`,
        text: `${form.get("email")} wrote:\n\n${form.get("message")}`,
      });
      return Response.redirect(new URL("/thanks.html", request.url), 303);
    },
  };
  ```
- `{"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`, or MCP `cool_set_secrets` with `secret_set`, `values` and `proxy`, which likewise replaces the whole set). A key set with `--proxy` reaches functions once the site has its own VM, and then only as a per-key stand-in: send the supplied `env.OPENAI_KEY` as the Bearer token in the `Authorization` header and the computer's egress proxy substitutes the real value on matching `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`).

**Editing a document someone may have open.** A document artifact saves the edits of its owner and share members from their browser, so read its current files with `cool files` first: the live file may be newer than the one you published. Your publish reaches a person who has it open within about 20 seconds while the tab is visible: their page reloads to it, or, when they have unsaved edits, their next save is a conflict and they choose between your version and a downloaded copy of theirs. Keep the viewer and `artifact.json` when you change the content. Once the site has a VM, browser saves stop.

**Building a document viewer.** Declare the document in `artifact.json` at the site root: `{"kind": "docx", "file": "document/report.docx"}` (`kind` is `docx`, `xlsx`, `pptx` or `pdf`, with `file` under `document/`), or `{"kind": "slides", "file": "deck/deck.json"}`. A save may write only that file, and for slides also slide HTML such as `deck/slides/welcome.html` and `png`, `jpg`, `jpeg`, `gif`, `webp` or `svg` images under `deck/`; each file is at most 25 MiB, a save at most 64 MiB. The page talks to the platform with `window.postMessage({cool: "doc", type, ...}, location.origin)` on its own window and listens for answers there:

- Send `hello` on load, again after 3 seconds without an answer. `access` answers `canSave`, `version` and, when `canSave` is false, `reason` (`signed_out`, `read_only` or `not_document`). Edit only after `canSave`.
- Read every file the document needs when the page loads, from the release that served it: `fetch("/__cool/document/" + access.version + "/document/report.docx")`. An older release can disappear later. Once the site has a VM those reads move to the file's own path, and a page loaded from the VM gets no answer to `hello`: show the document read-only, read from the files' own paths.
- Send `save` with a new unique `id`, `base` (the version the edits were made on) and `files: [{path, bytes}]` (ArrayBuffers, transferred), about two seconds after an edit and when the page is hidden, one at a time; guard leaving with `beforeunload` while edits are unsaved. `saved` answers the new `version`, the next save's base.
- `save_failed` has `id` and `reason`: `conflict` (someone published `version`: offer "Reload their version" and "Download a copy"), `offline` (save again with the same id, after `retryAfter` seconds when given), `signed_out` (ask to sign in again), `forbidden` (editing is no longer allowed), `read_only` (the site has a VM and takes no saves), `path_not_allowed` (the page saved a file outside the declaration), `not_document` ("This page is no longer a document"), `too_large` or `error` (keep the edits and let the person try again).
- `changed` with `version` says the live document moved: reload the page and its files when there are no unsaved edits. There is no Save button.

### When a site gets a VM

A published site gets its own VM only when you ask: `cool vm alex-heath` (`POST /api/computers/$ID/vm`), `cool service run`, or a publish whose `cool.json` declares `run`. VMs need Pro; on Free these answer `402 pro_required`. It is one-way: the VM is 1 of the account's 20 and takes memory from the account's 8 GiB, which all its VMs share (one VM can use most of it while the others are idle (each idle one keeps about 110 MiB); a VM that does not fit is refused with `account_memory_full`), and publishing to the site stops. `cool vm` waits until the VM is ready, and running it again on a computer that already has one changes nothing, except that it finishes moving the site's link to the VM when an earlier attempt left it on the published site; while that still fails it answers `502 artifact_deploy_failed`, the VM is kept, and running it again in a moment retries. Before it returns, the site moves to the VM by itself: the published files are in `/home/runtime/app`, the computer's built-in site server serves them exactly as before, functions, secrets and `env.EMAIL` 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. Once a port is exposed (`cool service run --port`, a `run` publish, or `cool expose`), the program answers every path, except `/api` on a site that declares functions, and the folder is never served as files again, even after the port is removed, when the URL shows a "Nothing here yet" page: to serve files again, run a static file server as the service. From then on, change it with `cool files` and `cool exec`; `cool.json` no longer applies, and publishing to it is refused (`409 computer_has_vm`), so publish to a new name for a separate site. The command that gives the site its VM says so: the CLI prints a `Note:` line on stderr (with `--json`, the result's `notices`), the HTTP answer carries a `Cool-Notice` header, and an MCP result an extra line. Without a VM, `cool files` reads the published files and writing or deleting one publishes a new release.

If the live site uses `"database": true`, its database moves with it to `/home/runtime/.cool/database/`. While the database is copied, function requests answer `503` with `Retry-After` for as long as the copy takes, longer for a larger database; static files keep serving. After the move, `env.DB` uses the local data. Integers beyond ±9,007,199,254,740,991 (2^53−1) change during the move, because Cloudflare's D1 export rounds them; store such values as TEXT.

## 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 alex-heath-notes --visibility private --json
cool share add alex-heath-notes "$COLLEAGUE" --json
```

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

```sh
NOTES_KEY="publish-alex-heath-notes-$(date +%s)"
curl -sS -o "$WORK/notes.json" -w '%{http_code}\n' "https://api.cool.computer/api/publish?name=alex-heath-notes&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://alex-heath-notes.cool.computer`. With MCP, publish it with `cool_publish` and `visibility` set to `private`, then call `cool_share` with `computer`, the colleague's `email` and `access` set to `can_use`; `access` set to `none` stops sharing it, and the same call twice sends one invitation. Sharing gives the colleague full access to the computer: anything already on it, and the secret set its `cool.json` names for its functions, are theirs to read too. While it is shared it cannot get any other secret set (`computer_shared`) or account integrations; keep those on a computer that is not shared. A shared computer withholds all the owner's connected services (Claude, Codex, Grok, and GitHub). Proxy access granted before sharing, including secret sets and integrations, is withheld too, except the site's own function rules. Plaintext secrets already on the computer remain there. Unsharing lets the computer use those credentials again, including any program the person left running on it, so if the user no longer trusts them, keep the user's credentials on a fresh computer instead.

## Then keep going

- Run commands on a computer with a VM: give the welcome site one with `cool vm alex-heath` (`POST /api/computers/$ID/vm`), then `cool exec alex-heath -- uname -a`. On a site without a VM, exec answers `409 needs_vm` and runs nothing.
- 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 show` and `cool service logs`. Unlike `cool exec`, `cool service run` quotes every word after `--`: `cool service run bakery --port 3000 -- node server.js` works, but one quoted shell line such as `-- 'cd app && node server.js'` is passed as a single program name and fails. Write a shell line as `-- bash -c 'cd app && node server.js'`.
- A computer has one service: the one program its URL serves, or one worker. For another long-running program, start a systemd unit on the VM: `cool exec alex-heath -- "sudo systemd-run --unit=worker --uid=runtime --working-directory=/home/runtime/app --property=Restart=always node worker.js"`. Read its logs with `sudo journalctl -u worker` and stop it with `sudo systemctl stop worker`; reach a port it listens on privately with `cool proxy start alex-heath 9000`. The platform does not watch these units: `cool service` neither shows nor restarts them (systemd's `Restart=always` does), and a unit started this way is gone if the VM restarts.
- Reach a database or other TCP port on a VM from the user's machine with `cool proxy start bakery 5432`, then connect to `127.0.0.1:5432` (for Postgres, `psql -h 127.0.0.1 -p 5432`); the program must listen on `127.0.0.1` or all addresses inside the VM, and `cool proxy stop --all` ends it. The proxy runs on the user's machine, not in the account shell, and a site without a VM answers `needs_vm`.
- Serve a computer on the user's own subdomain, such as `app.example.com`: `cool domain set alex-heath app.example.com` prints the two DNS records the user creates at their DNS provider (a `TXT` that proves they own the name and a `CNAME` to `fallback--domains.cool.computer`), and `cool domain show alex-heath` shows its state and what it waits for until it is `active`, usually a few minutes after both records resolve, and why an `active` domain is not serving if it ever is not. `cool domain remove alex-heath` removes it. Without a shell, `PUT /api/computers/$ID/domain` with `{"hostname":"app.example.com"}`, `GET` and `DELETE` on the same path do the same. One domain per computer; its own `cool.computer` address keeps working, and is where page comments show: they are not on custom domains yet.
- Run something on a schedule with a systemd timer; there is no cron, so do not install one. `cool exec alex-heath -- 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.
- Take the comments the user sends you from a page: after publishing a site with no VM, such as the private page above, run `cool comments watch alex-heath-notes --agent "Claude Code"` in the background, once per site, with your own name after `--agent` (how to run it in each agent: see `cool skill collaborate`); when it prints a line, run its `next`, do the comments, answer each with `cool comments resolve`, then run the watch again.
- 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.

## Send feedback

When something in Cool Computers broke, confused you, or worked well, tell the team. No sign-in is needed; signed in, the feedback names the user's account. Keep the user's private data out of the message.

```sh
cool feedback "publishing welcome.html worked on the first try"
```

Without a shell, send it over HTTP; add `-H "authorization: Bearer $TOKEN"` to attach the user's account:

```sh
curl -sS -w '%{http_code}\n' https://api.cool.computer/api/feedback \
  -H 'content-type: application/json' --data '{"message":"publishing welcome.html worked on the first try","source":"agent"}'
```

A `201` means it was received. A `429 feedback_rate_limited` means the hourly limit was reached: wait `Retry-After` seconds, or send it later.

## Errors

- `401`: missing, expired, or revoked credential. If you came through `/auth.md`, re-exchange your assertion there; otherwise sign in again (step 4). 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 4) instead.
- `403 interactive_required` while completing sign-in: the account needs the website; give the user https://www.cool.computer and wait.
- `403 sign_up_waitlisted` while completing sign-up: Cool Computers is full right now and the user's email has no invitation, so it joined the waitlist instead of getting an account. Tell the user, and stop: they get an invitation by email when they are let in, and signing up again with that email works then, or later once there is room. Do not retry in a loop.
- `409 claim_unavailable` while keeping a site: it expired, or another account kept it; `404` once an expired site is deleted. Publish its files again signed in.
- `402 pro_required`: VMs need Pro, and the account is on Free. Tell the user they can start Pro with `cool billing upgrade`, or fix a lapsed subscription (past due, unpaid or paused) with `cool billing portal`, or use the Account page; then try again (`cool billing` says which); see [pricing](/pricing).
- `403 site_limit_reached` while publishing a new site or keeping one: Free holds 20 sites at once. Tell the user to delete one or upgrade to Pro, then try again.
- `409` on publish: read the problem `code`. `name_taken`: another account owns the name; send one of the free names in `suggestions` instead (the first has a random suffix, so concurrent agents do not collide). `name_is_yours`: the user already has a computer with that name, and `context` says whether it has a `vm` and the `visibility` it keeps. Unless the user asked you to change that computer, stop and ask them first, since it may hold their work; with their go, publish to a site with `?computer=` and that name (`cool publish --computer` with it), write a VM's files with the files API instead, change the visibility first if it differs from what the user wants, or choose another name. `name_releasing`: one of the user's own computers still holds that name while it is being created or deleted; wait `Retry-After` seconds and rerun the same request (the CLI does this itself). `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`. A signed-out publish refused with `unclaimed_computer_limit`, `anonymous_publish_capacity` or `anonymous_publish_rate_exceeded` has no wait: sign the user up (step 4), then publish signed in. Never loop on `cool signup`.
- `409 needs_vm`: the computer is a site without a VM, and the command needs one. When the task needs to run something there, run `cool vm alex-heath` (`POST /api/computers/$ID/vm`), which is one-way and stops publishing to it, then retry; to only look at its files, use `cool files` instead.
- `503 host_capacity_full` while creating a computer or giving a site its VM: Cool Computers is at capacity right now. A site remains available; wait `Retry-After` seconds (about a minute) and retry once. If it is still full, tell the user and stop.
- `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 a database query (`cool db query`, `POST /api/computers/$ID/database/query`) blindly: it has no idempotency key, so a write may have committed; read the data first and rerun only what is missing. 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. `cool create` retries a lost response itself under one idempotency key, so it never makes a second computer; if it still reports that the computer may have been created, check `cool list --json` before creating another. After a lost `cool service run` response, which sends no idempotency key, do not rerun it: check `cool service show`, 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.
