# 639 Shell docs # Getting started 639 Shell is a cloud for agents: Linux machines that you and your AI agents create, run and put online, from a terminal, over MCP or through the API. A machine is ready in seconds (see Benchmarks), sleeps when nobody is using it and wakes right where it left off. Compute is billed per second while a machine is awake; its disk is billed all the time. ## Sign up 639 Shell is invite-only for now. 1. Run `ssh shell.639cloud.com` and enter your email. Not invited yet? That records a request, and we email you when you're approved, with an **approval code**. 2. Run `ssh shell.639cloud.com` again with the same email. You get a sign-in link and a QR code. No SSH key yet? It tells you how to make one (`ssh-keygen -t ed25519`). 3. Open the link (or scan it). Check the key fingerprint it shows is the terminal in front of you. 4. **Continue with Google**, or **use your approval code** (from the approval email). 5. The page shows a short code. Type it into your terminal: that confirms it's really you at that terminal. Never give this code to anyone. 6. Fill in your profile and accept the terms. Back in the terminal, the app opens by itself. Next time, `ssh shell.639cloud.com` just logs in. **Another computer:** do the same from it, signing in the same way (the same Google account, or a new approval code from us), and type the page's code in that computer's terminal. Then both keys see the same machines. **Questions or problems?** **Send feedback** in the app, or email micah@639cloud.com. ## Your first machine ```sh ssh -t shell.639cloud.com ``` Choose **New machine**, pick a size and an image, then **Open shell**. You are root. Type `exit` to come back to the app. Or skip the app entirely: ```sh ssh -t ubuntu-1@shell.639cloud.com # a root shell on ubuntu-1 (wakes it) ssh ubuntu-1@shell.639cloud.com 'uname -a' # one command, output streamed ``` ## Getting around the app Arrows or `j`/`k` move, Enter opens, Esc or `q` goes back. Press `:` for the command line and `/` to search. These docs are under **Docs**; press `o` there for a link to read them in your browser. ## What the login name does | Login | Opens | |---|---| | a machine name (`ubuntu-1@`) | that machine: a root shell, a command, or scp/sftp | | `mcp@` | the MCP server for AI agents | | `vpc@`, `ip@`, `keys@` | one-shot commands for private networks, public IPs and API keys | | `export@` | your machines as an SSH config or JSON | | `docs@` | these docs as Markdown | | `legal@` | the terms of service, privacy policy and abuse contact | | anything else | the app | The app and interactive shells need a terminal: use `ssh -t`. ## Save these docs ```sh ssh docs@shell.639cloud.com # list the pages ssh docs@shell.639cloud.com machines > machines.md # save one ssh docs@shell.639cloud.com all > 639-shell-docs.md # all of them, in one file ``` # Machines ## Instant or Full VM **Instant** machines are ready in seconds on our tuned Linux (see Benchmarks). A **Full VM** runs a distro's own cloud image with its own kernel (Ubuntu, Debian, Fedora, Rocky, AlmaLinux, CentOS Stream, Arch, openSUSE): pick FULL VM when you create a machine. Same sizes, same prices; its first boot takes about 15 seconds. To boot your own cloud image, pick **Your own image…** and paste its https URL (qcow2 or raw with cloud-init, not compressed, no bigger than the machine's disk). If a Full VM doesn't come up, its page has **Boot log…** (its console; `c` copies it), and agents read the same log with `boot_log` over MCP or `GET /v1/machines/{name}/bootlog`. To type into a Full VM before it has networking (a broken network config, a firewall that locked you out), join its **serial console**: `ssh -t console-web-1@shell.639cloud.com` (put your machine's name after `console-`). Press Enter for a login prompt. To leave, press Enter, then `~.`. One person at a time; viewers can't. Every Full VM can also run its own VMs (KVM is available inside), and Docker works on all of them. **Proxmox VE 9** (Full VM, 4 GiB RAM or more) comes ready to sign in to: the machine's "ready" screen (and `create_machine`'s or the API's create reply) has its web UI link and a `root` password made for that machine alone. The password is shown only then: save it (lost it? `ssh` in and run `sudo passwd root`). The link is private: its URL is a one-time sign-in (use it within 15 minutes; the browser then stays signed in for 30 days), and the machine's **Links** makes new ones. VM 9000 `debian-13-template` is ready to clone (set a password or SSH key under Cloud-Init), a Debian 13 container template is in **Create CT**, and guests on bridge `vmbr1` get DHCP and internet through the machine. ## Sandboxes For code you don't trust yet, make the machine a **sandbox**: an Instant machine with no public address and no VPC, deleted when its lifetime ends (an hour unless you change it). Pick SANDBOX when you create a machine, or see the Sandboxes page. ## Lifetimes Any machine can have a lifetime: it is deleted, with everything in it, when the lifetime ends. Use it for work that should clean up after itself (a batch job, a review environment). On its page: **Lifetime…**; from code: `lifetime_seconds` on create or `PATCH`; agents use `set_lifetime`. Delete protection wins over a lifetime. A sandbox always has one unless you keep it. ## Sleep and wake A new machine stays on. Turn on **auto-sleep** (machine page → **Sleep after**, or `set_auto_sleep` / `"auto_sleep": true` in the API) and it goes to sleep on its own when it's idle: nobody attached, no command running, and next to no CPU in use. Sleeping keeps its memory and disk exactly as they were, and you pay only for its disk. The next shell, command, file transfer or API call wakes it, right where it left off. Everything inside a sleeping machine is frozen. Traffic arriving at its public address does not wake it. ## Keep a job running: keepawake Start long background jobs with `keepawake`, and auto-sleep waits until the job exits: ```sh nohup keepawake ./build.sh > build.log 2>&1 & ``` Or turn auto-sleep off for the machine instead. ## Sizes | Size | CPU | RAM | Disk | |---|---|---|---| | s1 | 1 core | 1 GiB | 20 GB | | s2 | 1 core | 2 GiB | 25 GB | | s4 | 2 cores | 4 GiB | 50 GB | | s8 | 4 cores | 8 GiB | 100 GB | | s16 | 8 cores | 16 GiB | 200 GB | | s32 | 16 cores | 32 GiB | 400 GB | | s64 | 32 cores | 64 GiB | 800 GB | Cores are shared between machines: a machine gets its CPU when it needs it. Usable RAM is a little under the size, because the kernel keeps some for itself. ## Resize live Machine page → **Resize…**: sliders for RAM, CPU and disk, with the live price. RAM and CPU change with no reboot up to the machine's ceiling; past it, the app offers a restart (about 10 seconds). The disk grows live and never shrinks. From the command line: ``` :resize ubuntu-1 ram 2G cpu 1 :resize! ubuntu-1 ram 8G # ! allows a restart past the ceiling ``` ## Restart and recovery **Restart** cold-boots the machine from its disk in about 10 seconds. If a machine crashes, the app shows the error and offers a one-key restart. ## Labels Tag machines with `key=value` labels (a project, a customer, an environment) to find them and see what each costs. On a machine's page they show under the cost; `/` in My machines finds them (`acme`, or `label:project=acme`). From code: `labels` when you create a machine, `PATCH` to change them, `GET /v1/machines?label=project=acme`, and `GET /v1/usage?…&group_by=label:project` for the cost per project. Agents use `set_labels`. ## Secrets Give a machine its Git token or API keys as it's made: `secrets` on create or fork, from the API or an agent (`create_machine`, `fork_machine`, `fork_template`). Each one is a root-only file in `/etc/639/secrets/` and an environment variable in shells and commands. We keep no copy; they live on the machine's disk, so its snapshots and forks have them too. ## Many machines at once On **My machines**, `space` marks a machine, `a` marks everything shown, and `enter` opens the actions for the marked set: sleep, wake, restart, auto-sleep on/off, delete. Search picks the set: `state:running`, `size:s1`, `vpc:NAME`, `image:ubuntu`, and wildcards like `web-*`. The command line takes the same selectors: ``` :sleep state:running :delete web-* # always asks first ``` ## Notes Each machine keeps a running log that everyone in your account sees: who wrote what, and when. Use it for the things the next person (or agent) needs to know: "migrated the db, don't restart before 5pm", "port 8080 is the staging API", "ask Ana before upgrading". - **In the app:** the machine page shows the latest two; **Notes…** lists them all. `n` adds one (several lines are fine; `ctrl+s` saves), `e` edits your own, `d` deletes. - **From a terminal:** ```sh ssh note@shell.639cloud.com ls web-1 ssh note@shell.639cloud.com add web-1 "migrated the db, don't restart before 5pm" ssh note@shell.639cloud.com add web-1 < handover.txt ssh note@shell.639cloud.com edit n_k7f2q9xm3a "fine to restart now" ssh note@shell.639cloud.com rm n_k7f2q9xm3a --yes ``` - **Agents** read and write them with `list_notes` and `add_note`, and `list_machines` shows how many each machine has. An agent's note is labelled with the agent and the person it works for, e.g. "Claude Code via MCP · Ana Ruiz". - **The API and SDKs:** `GET/POST /v1/machines/{name}/notes`; `machine.notes()`, `machine.add_note("…")`. You edit and delete your own notes; your account's owners and admins can delete anyone's, and viewers can read them but not write. A note is plain text, up to 4 KB; a machine keeps up to 200. Deleting a machine deletes its notes. For instructions agents should *follow* rather than just read, use **Rules** (see Rules for agents). ## Inside a machine You are root. There are no loadable kernel modules (`modprobe` fails; drivers are built in). # Sandboxes A sandbox is a machine for code you don't trust yet: something an agent just wrote, a dependency you haven't read, a script from the internet. It's an Instant machine with three differences: - **It has a lifetime.** When the lifetime ends, the sandbox is deleted with everything in it. A new sandbox gets one hour. You can change that at any time, or keep the sandbox. - **It has no public address.** No public IPv6, no public IPv4, no open ports: nothing on the internet can connect to it. Web links still work, because they go through us. - **It isn't in a VPC.** It can't be put on a private network with your other machines. Everything else is a normal machine: its own kernel, root, shells, commands, files, snapshots, the same sizes and prices, billed per second. ## What a sandbox doesn't change - **Outbound internet is on**, as on any machine, so the code inside can download what it needs. It can also send out anything it can read inside the sandbox. To cut it off, turn the machine's internet off (machine page → **Internet**, `set_network`, or `"network"` in the API) and allow only the destinations it needs. - **Don't put secrets in it** that the code shouldn't see: API keys, SSH keys, customer data. - It's deleted on time whether it's awake or asleep, and whether or not someone is connected. Copy out what you want to keep before then, or give it more time. ## Make one - **In the app:** New machine → **SANDBOX**, then size, image and name as usual. - **Agents (MCP):** `create_machine` with `sandbox: true`, and `lifetime_minutes` if an hour isn't right. - **API:** `POST /v1/machines` with `"sandbox": true`, and `"lifetime_seconds"` if an hour isn't right. ```sh curl -s -X POST https://api.shell.639cloud.com/v1/machines \ -H "Authorization: Bearer $KEY" \ -d '{"size": "s1", "image": "python", "sandbox": true, "lifetime_seconds": 900}' ``` The reply says `"sandbox": true` and gives `"expires_at"`, the time it will be deleted. Lists show the same, and the app marks sandboxes on **My machines** with the time they have left. ## The lifetime - From 1 minute to 30 days, counted from the moment you set it. - **Change it:** the machine's page → **Lifetime…**, `set_lifetime` over MCP, or `PATCH /v1/machines/{name}` with `"lifetime_seconds"`. - **Keep it:** a lifetime of `0`. The machine stays until you delete it, and it stays a sandbox: still no public address and no VPC. - **Delete protection wins.** If you turn delete protection on for a sandbox, it isn't deleted when its lifetime ends: it's kept, and its lifetime is removed. - A **fork** of a sandbox is a sandbox too, and ends when the one it was forked from does. - A normal machine can't be turned into a sandbox, and a sandbox can't be turned into a normal machine. To move work out of a sandbox, copy the files out. Every sandbox deleted at the end of its lifetime is in your history (`sandbox-expire`). # Shells and files Every machine is reachable by plain `ssh`, using its name as the login. Each of these wakes the machine if it's asleep. ```sh ssh -t ubuntu-1@shell.639cloud.com # root shell ssh ubuntu-1@shell.639cloud.com 'apt-get install -y jq' # one command; exits with its code scp app.py ubuntu-1@shell.639cloud.com:/root/ # upload scp ubuntu-1@shell.639cloud.com:/root/out.csv . # download sftp ubuntu-1@shell.639cloud.com ``` A one-shot command runs for at most 10 minutes this way; use `keepawake` with `nohup` for longer jobs. Commands from the API (`timeout_seconds`, default 120 s, up to 3600) and from an agent's `exec` tool (default 120 s, up to 600; a longer timeout is refused, not shortened) are stopped in the machine at their timeout, along with everything they started: the result says `timed_out`, with exit code 124 and the output so far. A machine stays awake while you are attached or a transfer runs. ## Port forwarding Reach a port on one of your machines from your own computer, through 639 Shell: ```sh ssh -N -L 8080:web-1:80 shell.639cloud.com # then open http://localhost:8080 ssh -N -L 5901:desk-1:5900 shell.639cloud.com # a VNC viewer on localhost:5901 ``` The destination is one of your machines, by name. It wakes the machine and keeps it awake while the forward is open. Only your own machines (and your team's) can be reached this way, and it takes the access a shell does (a viewer can't forward). At most 16 forwards on one connection and 64 open in your org at once; each is in the machine's history as `forward`. `-L` only: remote (`-R`) and SOCKS (`-D`) forwarding are refused. With required rules on the machine, file transfers (scp, sftp) and `ssh web-1@… 'cmd'` commands wait until your key acknowledges them (see Rules for agents); an interactive shell is never refused, it shows the rules when it opens. ## Use your own SSH config Put your machines in your computer's SSH setup, so `ssh web-1`, `scp web-1:file .` and VS Code Remote-SSH just work: ```sh ssh export@shell.639cloud.com ssh-config > ~/.ssh/639.conf ``` Then add this once, at the top of `~/.ssh/config`: ``` Include 639.conf ``` Re-run the export after creating or deleting machines. For scripts, there is JSON too: ```sh ssh export@shell.639cloud.com json > 639.json ``` Nothing secret is exported. # Networking ## Public addresses Every machine gets its own public IPv6 address, free. You can also reserve public IPv4 addresses and attach one to a machine. Addresses stay with the machine when it sleeps. (A sandbox has no public address and can't be given one: see Sandboxes.) ```sh ssh ip@shell.639cloud.com list ssh ip@shell.639cloud.com reserve # billed monthly while reserved ssh ip@shell.639cloud.com attach ADDRESS web-1 ssh ip@shell.639cloud.com detach ADDRESS # stays reserved ssh ip@shell.639cloud.com release ADDRESS # detach first ``` ## Opening ports Inbound traffic is closed until you open ports. The list applies to both the machine's IPv4 and IPv6. ```sh ssh ip@shell.639cloud.com inbound web-1 # show ssh ip@shell.639cloud.com inbound web-1 443 udp:51820 icmp # open these ssh ip@shell.639cloud.com inbound web-1 22 from 203.0.113.0/24 # only from a range ssh ip@shell.639cloud.com inbound web-1 none # close everything ``` Agents (an API key, MCP, an `ssh` command) can't open SSH (22), the Docker API (2375, 2376), MySQL (3306), PostgreSQL (5432), Redis (6379), Elasticsearch (9200), memcached (11211) or MongoDB (27017) wider than a /16 (IPv4) or /32 (IPv6) in all: they give a range (`22 from 203.0.113.0/24`), or a person opens it in the app. A sleeping machine doesn't answer traffic on its public address until something wakes it. ## Private networks (VPCs) A VPC is a private network between your machines. Members reach each other on `eth1` at stable addresses, and machines join or leave live. ```sh ssh vpc@shell.639cloud.com create backend # picks a free /16 ssh vpc@shell.639cloud.com create backend 10.20.0.0/24 # or choose the range ssh vpc@shell.639cloud.com attach ubuntu-1 backend ssh vpc@shell.639cloud.com detach ubuntu-1 ssh vpc@shell.639cloud.com delete backend # must be empty ssh vpc@shell.639cloud.com list ``` Ranges are /16 to /28 inside `10.0.0.0/8` or `192.168.0.0/16`. VPC traffic doesn't wake a machine: while a member is asleep, packets sent to it are dropped. For a machine others reach over the VPC (a database, an internal API), turn auto-sleep off. ## Outbound rules Per machine, the internet is either on with a deny list or off with an allow list (machine page → **Internet**, the API's `network` field, or the MCP tool `set_network`): ```json {"internet": false, "allow": ["1.1.1.1:53", "151.101.0.0/16:443"]} {"internet": true, "deny": ["203.0.113.0/24"]} ``` Allow a DNS server when the allowed hosts are reached by name. Outbound email (port 25) is always blocked. To allow sites by name instead, with the internet off, add presets (`github`, `packages`, `model-apis`) or names of your own (`api.example.com`, `*.example.com`): ```json {"internet": false, "presets": ["github", "packages"], "allow_names": ["*.example.com"]} ``` The machine can then reach those sites over HTTPS and HTTP, and look names up (DNS to 1.1.1.1 and 1.0.0.1); other traffic stays blocked. Each connection is checked by the site name the program asks for (the TLS server name or the HTTP Host); content isn't decrypted or changed. A name check can't tell apart sites that share an address (a CDN), so it narrows what code can reach rather than sealing it. A machine has up to 256 such connections open at once, within its usual connection limits. # AI agents (MCP) 639 Shell has an MCP server, so any MCP agent (Claude Code, Codex, Cursor, VS Code Copilot, Gemini CLI, Muse Code, Claude Desktop and the rest) can create machines, run code in them and delete them by itself, dozens at a time, within your account's limits. ## Connect an agent The server is a standard MCP server over SSH: your agent runs `ssh` and talks MCP over it. Your SSH key is the login, so there's no token to paste. The app's **Connect an agent** screen shows each of these for your account, with `c` to copy. | Agent | Setup | |---|---| | Claude Code | `claude mcp add 639 -- ssh -T -o BatchMode=yes mcp@shell.639cloud.com` | | Codex | `codex mcp add 639 -- ssh -T -o BatchMode=yes mcp@shell.639cloud.com` | | Cursor | `~/.cursor/mcp.json` (or `.cursor/mcp.json` in a project): the JSON below | | VS Code (Copilot) | `.vscode/mcp.json`: the JSON below, with `"servers"` for `"mcpServers"` and `"type": "stdio"` | | Claude Desktop | `claude_desktop_config.json` (Settings → Developer → Edit Config): the JSON below | | Gemini CLI | `~/.gemini/settings.json`: the JSON below | | Muse Code (Meta) | `~/.config/muse/settings.json`: `{"schema_version": 1, "mcp_servers": {"639": {"transport": "stdio", "command": "ssh", "args": [the args below], "mode": "optional"}}}` | | Anything else | any MCP client that can run a local (stdio) server: command `ssh`, the arguments below | ```json { "mcpServers": { "639": { "command": "ssh", "args": ["-T", "-o", "BatchMode=yes", "mcp@shell.639cloud.com"] } } } ``` - `BatchMode=yes` means ssh never stops to ask a question: your key needs to be loaded in your SSH agent (or have no passphrase). - If a session drops, reconnect the server in your agent (in Claude Code: `/mcp`, then `639`). After a 639 Shell update, an open session gets a warning a few minutes before it closes; agents connected over HTTPS keep working without reconnecting. - Notes an agent leaves are labelled with the agent's own name, e.g. "Cursor via MCP · Ana Ruiz". ## From claude.ai, ChatGPT and other web agents (beta) Agents that run in a browser or in the cloud can't use your SSH key, so they connect over HTTPS instead, at `https://api.shell.639cloud.com/mcp`. Beta: accounts not on the beta yet are refused (`403`). - **claude.ai:** Settings → Connectors → **Add custom connector**, paste the URL. - **ChatGPT:** Settings → Connectors → **Create**, paste the URL. A 639 page opens: sign in the way you signed up (**Continue with Google**, or, for an account made with an approval code, **Email me a sign-in code** and type the 6-digit code), check where it will send you back to, and **Allow**. The agent gets **full access** to your account, the same tools as above: it can create, run commands in, resize, sleep and delete your machines, read and write their files, and leave notes. Rules your account requires still apply. See what's connected, and disconnect any of it, in the app (**Agents → API keys → Connected agents**) or from a terminal: ```sh ssh keys@shell.639cloud.com connections ssh keys@shell.639cloud.com disconnect g_… --yes ``` A connection that isn't used for 30 days ends by itself. Up to 10 at a time. ## From any HTTPS client Coding tools, scripts and hosted agent platforms that speak MCP over HTTPS (Streamable HTTP) use the same URL with an API key: `Authorization: Bearer 639_…` (make one with `ssh keys@shell.639cloud.com new NAME`). Beta: accounts not on the beta yet get `403`. ## What an agent can do | Tool | |---| | `list_notifications` | | `whoami` | | `list_connected_agents` | | `list_machines` | | `create_machine` | | `exec` | | `write_file` / `read_file` | | `resize` | | `restart` | | `rename_machine` | | `create_machine_from_url` | | `boot_log` | | `protect_machine` | | `sleep` / `delete` | | `set_auto_sleep` | | `set_lifetime` | | `set_labels` | | `set_isolated_memory` | | `set_network` / `set_inbound` | | `history` | | `vpc_create`, `vpc_list`, `vpc_delete` | | `machine_vpc_attach`, `machine_vpc_detach` | | `ip_reserve`, `ip_list`, `ip_release` | | `machine_ip_attach`, `machine_ip_detach` | | `create_api_key`, `list_api_keys`, `revoke_api_key` | | `list_notes`, `add_note`, `edit_note`, `delete_note` | | `get_rules`, `ack_rules` | Beta accounts also get: | Tool | Does | |---|---| | `snapshot_machine`, `list_snapshots`, `restore_snapshot`, `delete_snapshot` | save a machine's exact state and put it back (see Snapshots and undo); `exec` also takes `checkpoint` | | `fork_machine` | a new machine from a snapshot, or from a machine as it is now; `count` makes up to 50 at once, and `secrets`, `labels`, `lifetime_minutes` apply to each (see Forking) | | `create_template`, `list_templates`, `fork_template` | name a machine's snapshot for the org, and make machines from it (see Forking) | | `set_history` | a restore point every time the machine sleeps | | `create_link`, `list_links`, `delete_link`, `sign_in_link` | HTTPS links that wake a machine (see Web links) | **Limits.** Each person's agents make up to 20 calls a second (bursts to 40), and delete (or give a lifetime to) at most 5 existing machines that aren't sandboxes in any 10 minutes, shared with their API keys; past that, a person does it in the app. ## Watch what your agents do **Agents → Activity** in the app is the account's history as it happens: every action on a machine, newest first, with when it happened, who did it (an agent, the API, the app, `ssh`, or 639 Shell itself: auto-sleep, a sandbox's lifetime ending), what, which machine, and whether it worked. It updates by itself while it's open. The same history is the `history` tool for an agent and `GET /v1/history` in the API. ## Tips - Machines sleep between an agent's steps, so a fleet of idle agent machines costs nothing. - For a long job, have the agent start it with `nohup keepawake CMD > log 2>&1 &` and check the log later. - `history` shows exactly what the agent did and when. # HTTPS API and SDKs Your machines and account over HTTPS: everything an agent or a script needs (writing rules, turning delete protection off and answering invites stay in the app). Base URL: `https://api.shell.639cloud.com`. JSON in and out. ## API keys ```sh ssh keys@shell.639cloud.com new laptop # prints the key once ssh keys@shell.639cloud.com list ssh keys@shell.639cloud.com revoke ID ``` Send it on every request as `Authorization: Bearer 639_…`. We store only a hash, so a lost key can't be recovered: revoke it and make a new one. ## Quick start ```sh export KEY=639_… curl -s https://api.shell.639cloud.com/v1/catalog -H "Authorization: Bearer $KEY" curl -s -X POST https://api.shell.639cloud.com/v1/machines \ -H "Authorization: Bearer $KEY" -d '{"size": "s1", "image": "ubuntu"}' curl -s -X POST https://api.shell.639cloud.com/v1/machines/ubuntu-1/exec \ -H "Authorization: Bearer $KEY" -d '{"command": "uname -a"}' curl -s -X DELETE https://api.shell.639cloud.com/v1/machines/ubuntu-1 -H "Authorization: Bearer $KEY" ``` Every call wakes a sleeping machine as needed. ## Endpoints | | | |---|---| | `GET /v1/catalog` | sizes and images | | `GET`, `POST /v1/machines` | list, create | | `GET`, `PATCH`, `DELETE /v1/machines/{name}` | read, change (RAM, CPU, disk, sleep, network, inbound, name), delete | | `POST /v1/machines/{name}/sleep`, `/restart` | sleep now, cold restart | | `POST /v1/machines/{name}/exec` | run a command; add `?stream=1` for live output (Server-Sent Events) | | `PUT`, `GET /v1/machines/{name}/files?path=…` | upload, download (up to 512 MB) | | `GET`, `POST /v1/vpcs`, `DELETE /v1/vpcs/{name}` | private networks | | `GET /v1/machines/{name}/bootlog` | a Full VM's boot log (plain text) | | `PUT /v1/machines/{name}/protection` | delete protection on (`{"on": true}`); off only in the app | | `POST`, `DELETE /v1/machines/{name}/vpc` | join, leave a VPC | | `GET`, `POST /v1/ips`, `DELETE /v1/ips/{addr}` | public IPv4s | | `GET /v1/usage?from=&to=&group_by=` | usage and cost, by machine or by a label | | `GET /v1/events`, `GET`, `POST /v1/webhooks`, `DELETE /v1/webhooks/{id}` | what happened, as a list or a live stream, and webhooks (see Events and webhooks) | | `GET`, `POST /v1/orgs`, `PATCH`, `DELETE /v1/orgs/{id}` | a platform's managed orgs; `639-Org: ` on any call acts in one (see Platforms) | | `GET`, `POST /v1/templates`, `DELETE /v1/templates/{name}`, `POST /v1/templates/{name}/fork` | templates, fork many (beta; see Forking) | | `GET /v1/machines/{name}/rules`, `POST …/rules/ack` | the rules agents get, and acknowledging them (see Rules for agents) | | `GET`, `POST /v1/machines/{name}/notes`, … | the machine's notes | | `GET /v1/history`, `GET /v1/notifications` | what happened, notices | The full list, with every field: the OpenAPI spec and the API reference. | `POST /v1/ips/{addr}/attach`, `/detach` | attach, detach | | `GET`, `POST /v1/keys`, `DELETE /v1/keys/{id}` | API keys | Errors are JSON, `{"error": "…"}`. Limits per key: 20 requests a second (bursts to 40), 30 creates a minute, and 5 machine deletes (sandboxes aside) in any 10 minutes; over that you get `429` with `Retry-After`. Agents over MCP have the same call and delete limits (as tool errors), and the delete limit is per person: all their keys and agents share it. ## Python ```python from cloud639 import Client c = Client() # reads CLOUD639_API_KEY with c.create("s1", "ubuntu", delete_on_exit=True) as m: print(m.exec("uname -a").stdout) ``` ## TypeScript ```ts import { Client } from "cloud639"; const c = new Client(); // reads CLOUD639_API_KEY await using m = await c.create({ size: "s1", image: "node", deleteOnDispose: true }); console.log((await m.exec("node -e 'console.log(6*7)'")).stdout); ``` Both SDKs have no dependencies. # Rules for agents Rules are standing instructions for the AI agents that work on your machines, like a repo's `CLAUDE.md` or `AGENTS.md`: "never restart postgres", "this is production: ask before changing anything", "use uv, not pip". You write them once; every agent that touches the machine gets them. There are two kinds, and an agent gets both: - **Account rules** apply to every machine in your account. - **Machine rules** apply to one machine, after the account's. ## Where they apply | Where | What happens | |---|---| | **MCP** (`ssh mcp@`, HTTPS MCP) | the agent gets the rules; with `required`, its first change is refused until it calls `ack_rules` | | **API keys and SDKs** | the same: with `required`, a change answers `409 rules_unread` until the key acknowledges | | **SSH commands and file transfers** (`ssh web-1@… 'cmd'`, scp, sftp: how agents on your computer use a machine) | the same, per SSH key: with `required`, the first command or transfer is refused (exit 77, the rules on stderr) until the key runs `ssh rules@… ack web-1 `; advisory rules are printed once per version | | **Agents inside the machine** (Claude Code, Codex in a shell there) | they read `/root/AGENTS.md`, which 639 keeps up to date (see *Inside the machine*) | | **Interactive shells** (`ssh web-1@…` at a terminal, the app) | a person at a keyboard: shown the rules when the shell opens, never refused | Only people in your account write rules (viewers can read them, not change them). Agents over MCP and API keys can read them (`get_rules`) and acknowledge them, never change them. **The rules lock.** An agent on your computer has your SSH key, so it can do what `ssh rules@` can. Over `ssh rules@` rules can be made, made stricter, or reworded at the same level, but lowering them, turning them off or deleting them is done in the app only (like turning delete protection off). Owners and admins get a warning whenever rules change outside the app (rewording can still weaken them, so read it), when an attempt to loosen them there is refused, and when rules are loosened in the app. **What rules can't do.** They're instructions, checked at the door: an agent that has acknowledged them can still ignore them, and `ssh -tt` with commands piped into the shell looks like a person at a terminal (it gets the rules, but isn't stopped). For anything that must hold, use the controls that enforce it: delete protection, internet off or sites by name, team roles. ## How strictly Each set of rules has an enforcement level: | Level | What agents get | |---|---| | `advisory` | the rules, with their first response about the machine (once per MCP session, once per SSH key and version); nothing is refused | | `required` | the first change to the machine is refused until the agent acknowledges the rules, naming the version it read: once per MCP session, once per API key or SSH key. Over MCP and the API reading is never refused; over SSH every command and transfer waits for the acknowledgement, reads included | | `off` | nothing: the rules are kept, but not shown | Account rules also go into the agent's instructions when it connects, so it has them from the start. When you edit rules, agents get the new version: with `required`, every agent acknowledges again. An acknowledgement by an API key or SSH key also lapses after 30 days. ## What it costs Rules go into the agent's context, so they cost tokens and time: - Each session reads the rules once per machine it touches: about 1 token per 4 bytes. 2 KB of rules is about 500 tokens a session. - `required` adds one extra call per machine per session. Keep them short and specific. The app shows each set's size and token estimate as you type. ## Inside the machine Agents that run *inside* a machine (Claude Code, Codex and others in a shell there) read `/root/AGENTS.md`. 639 keeps that file in step with the machine's rules: - If there's no `/root/AGENTS.md`, or it's the one 639 wrote, it's replaced with the current rules, and `/root/CLAUDE.md` links to it. If you have your own `CLAUDE.md`, a line pointing to the rules is added to the end of it, once. - If you have your own `/root/AGENTS.md`, it's never overwritten. The rules go to `/root/.639/AGENTS.md`, and one line pointing there is added to the end of yours (and of your `CLAUDE.md`), once. - The file is brought up to date whenever the machine wakes, and at once when you edit the rules of an awake machine. A sleeping machine isn't woken for it. New and forked machines get theirs when they first start. - Delete the rules and 639's file and pointer lines are removed; your own text stays. - Edit the rules in the app or with `ssh rules@`. Changes made to the file inside the machine are overwritten. ## Setting rules - **In the app:** a machine's page has **Rules…**; account rules are under **Settings** (`r`). `e` edits, `tab` changes the level, `ctrl+s` saves. - **From a terminal**, from a file: ```sh ssh rules@shell.639cloud.com set web-1 --required < rules.md ssh rules@shell.639cloud.com set account --advisory < team-rules.md ssh rules@shell.639cloud.com show web-1 # what agents get: account + machine ssh rules@shell.639cloud.com history web-1 # earlier versions, who wrote them ssh rules@shell.639cloud.com ack web-1 # what an agent runs after reading them ``` Lowering, turning off and deleting rules is done in the app. - **Not through the API.** API keys are what agents and scripts use, so they can read rules (`GET /v1/machines/{name}/rules`) and acknowledge them, but never change them. With `required` rules, a change through an API key is refused with `409 rules_unread` until that key acknowledges them (`POST /v1/machines/{name}/rules/ack` with the `version` it read). The SDKs raise `RulesUnread`, with the text in `.rules` and its `.version`, and have `ack_rules(version)`. Up to 8 KB of Markdown per set, and the 20 latest versions are kept for 30 days. Deleting a machine deletes its rules; account rules stay. ## An example ```md # web-1: production API - This is production. Don't restart services or reboot between 08:00 and 20:00 UTC. - Never run database migrations here; they go through CI. - Logs are in /var/log/api/. Read them before changing anything. - Leave a note (add_note) saying what you changed and why. ``` # Orgs and teams An **org** is a shared account: its machines, VPCs, addresses, rules, API keys and bill belong to the org, and you invite people into it. Everyone starts with a personal org of their own; you can make more and belong to as many as you're invited to. **Teams** split an org into groups of people. A machine, VPC or address is either **org-wide** (everyone in the org sees it) or **in a team** (only that team, and the org's owners and admins, see it). Teams are about who can see and use what; they aren't a firewall (see Networking below). ## Roles Everyone in an org has one role: | | Owner | Admin | Member | Viewer | |---|---|---|---|---| | See machines | all | all | their teams' and org-wide | their teams' and org-wide | | Shell, commands, files, wake, sleep | all | all | what they see | no (read only) | | Create machines | yes | yes | yes, into one of their teams | no | | Delete and resize machines | all | all | what they see | no | | Notes | read and write | read and write | read and write, on what they see | read | | Rules for agents | set, delete | set, delete | set | read | | Invite and remove people, change roles | yes | yes (not owners) | no | no | | Teams, and where things go | yes | yes | no | no | | API keys and connected agents | all of the org's | all of the org's | their own | none | - **An API key acts as the person who made it,** with their role and teams, in the org it was made in. Remove someone and their keys in that org stop working. - **An org always has an owner:** the last owner can't leave or be demoted. - **A member on no team** sees only org-wide things and can't create until an admin puts them on one. ## Switching orgs Your SSH key acts in one org at a time; your personal org until you switch. The status line in the app shows which. - **In the app:** Account → Org → Overview, pick an org, `enter`. The app restarts in it. - **Over ssh:** `ssh org@shell.639cloud.com switch "Org name"`. The next session uses it. A connection that's already open stays in the org it started in: an agent connected over MCP, or an API key, never wanders into another org because you switched. ## Inviting people Owners and admins invite by email, with a role and optionally teams: ``` ssh org@shell.639cloud.com invite ann@example.com --role member --team web ``` or in the app: Org → Members → `i`. The invite waits for them for 14 days. If they don't have an account yet, the invite also lets them sign up with that email. It shows up in their Inbox: they accept with `y` (or decline with `d`), or with `ssh org@shell.639cloud.com accept ID`, and you're told either way. Removing someone (`remove EMAIL --yes`, or Members → `d`) takes effect at once: their API keys there are revoked, their connected agents are cut off, and their open shells, file transfers and commands end within seconds. The same happens to a shell on a machine moved to a team they're not on. An org can have 25 invites waiting at once, and one person can send 50 a day. ## Teams and where things go ``` ssh org@shell.639cloud.com team new web ssh org@shell.639cloud.com team add web ann@example.com ssh org@shell.639cloud.com assign my-machine web # a machine ssh org@shell.639cloud.com assign vpc:backend web # a VPC ssh org@shell.639cloud.com assign ip:ADDRESS org # back to org-wide ``` In the app: Org → Teams makes (`n`) and deletes (`d`) teams, Members → `t` puts a person on teams; a machine's page has a Team line (owners and admins change it); the VPC and IP lists have a TEAM column and `t` moves the selected one to the next team. - **What a member creates goes into their team.** On several teams, they pick one (`t` on the create screen; agents and the API pass the team). - **A fork stays in its source's team.** - **A team can't be deleted while it has machines, VPCs or addresses:** move them first. - **Things that existed before teams** start org-wide. ## Networking The org is the hard boundary: different orgs never share a VPC, an address or a link. Inside an org: | | Who creates | Who uses | |---|---|---| | VPCs | owners, admins, and members (into their team) | anyone who can see both the machine and the VPC | | Public IPv4s | owners and admins reserve and release them (they're billed) | anyone who can see both the machine and the address | | Wake links, inbound ports, internet rules | part of the machine | whoever can use the machine | - **Teams aren't a firewall:** machines in different teams reach each other privately only if they share a VPC, just as before. - **An org-wide VPC** is the usual way for teams to share a private network. - **Moving a machine to another team** keeps its VPC and address. - **Members delete only their team's VPCs;** org-wide ones are for owners and admins. ## For agents An agent sees exactly what the person whose key it uses sees. The MCP `whoami` tool reports the org, role and teams, and `list_machines` shows each machine's team. `GET /v1/org` does the same over the API. # Notifications 639 tells you about things that need your attention: planned maintenance, a machine that stopped with an error, getting close to a limit, an invite to an org, and the occasional bit of news. You see them in the app, over ssh, and so do your agents. ## Where they show up - **The app:** Home → **Inbox** (with how many are new), and `✉ 2` at the start of the status line. An urgent one (maintenance under way, a suspended machine) stays in the message line under the keys until you've read it. - **Over ssh:** `ssh inbox@shell.639cloud.com` lists what's unread; `ssh inbox@shell.639cloud.com all` lists everything. `read ID` shows one (and marks it read), `read all` marks everything read, `dismiss ID` hides one. `history` lists everything from the last 90 days, read, dismissed and expired ones too, and `undismiss ID` puts a dismissed one back. - **Agents:** the MCP tool `list_notifications`, and `list_machines` shows urgent notices on the machines they're about, so an agent doesn't fight a machine that's about to pause. - **The API:** `GET /v1/notifications?unread=1` (or `?history=1`) and `POST /v1/notifications/{id}/read`. In the Inbox: `enter` reads one, `d` dismisses it, `a` marks everything read, `h` shows **History**: everything sent to you in the last 90 days, read, dismissed and expired ones too, so you can go back to something you already cleared (`u` puts a dismissed one back in the inbox). ## What you'll get | Kind | When | How urgent | |---|---|---| | Maintenance | when it's planned, an hour before, when it starts, when it's done | the start is urgent until it's over | | Machine | a machine stops with an error, or its network is suspended | a warning; a suspension is urgent | | Limits | you reach 80 % of a limit, and when you reach it | info, then a warning | | Invite | someone invites you to an org | shown when you connect | | News | something new in 639 | info | | Rules | (owners and admins) rules changed outside the app, an attempt to loosen them there was refused, or they were loosened in the app | a warning | | Webhooks | (owners and admins) a webhook was turned off after a day of failed deliveries | a warning | - **Maintenance notices** name your machines that are affected. - **The same problem isn't repeated** more than once a day. - **Notices last 30 days** unless they say otherwise. ## Org invites An invite shows up in your Inbox. Select it and press `y` to accept (the app then offers to switch you into the org) or `d` to decline; whoever invited you is told either way. You can also accept with `ssh org@shell.639cloud.com accept ID`. Agents can see invites but never accept them. ## In an org Everyone in an org gets the org's notices, and each person's read and dismissed state is their own. A notice about a machine reaches only the people who can see that machine. # Events and webhooks Your org's events tell your own systems what happened to your machines, as it happens. They're kept 7 days. | Event | When | |---|---| | `machine.created`, `machine.deleted` | a machine is made or deleted | | `machine.awake`, `machine.asleep` | it wakes or goes to sleep | | `machine.error` | it stopped with an error | | `machine.expired` | its lifetime ended and it was deleted | | `exec.finished` | a command finished (`ok`: whether it did; never the command or its output) | | `snapshot.created` | a snapshot was taken | | `limit.reached` | you reached one of your limits | Each is `{"id", "org", "type", "machine", "at", "data"}`. Events carry names, states and outcomes, never commands, output, notes or secrets. ## Reading them ```sh curl -H "Authorization: Bearer $KEY" 'https://api.shell.639cloud.com/v1/events?type=machine.error' curl -N -H "Authorization: Bearer $KEY" -H 'Accept: text/event-stream' https://api.shell.639cloud.com/v1/events ``` The list gives `next`, the id to pass as `after` for the next page. The stream sends new events as they happen and picks up after `Last-Event-ID` when it reconnects; it re-checks your key and role as it goes and ends after an hour (reconnect). A member's key sees the events of machines their teams can reach; owners and admins see the whole org's. SDKs: `events()`, `stream_events()` / `streamEvents()`. ## Webhooks Owners and admins can add up to 5 webhooks per org: each gets every event of the types it asks for (all, if none), as a JSON POST, in order, from the moment it's added. ```sh curl -X POST -H "Authorization: Bearer $KEY" https://api.shell.639cloud.com/v1/webhooks \ -d '{"url": "https://example.com/hooks/639", "types": ["machine.error", "limit.reached"]}' ``` - **Signed.** The reply's `secret` is shown once. Each delivery has `639-Signature: t=,v1=`, where `` is HMAC-SHA256 of `.` with the secret. Check it (and that `t` is recent) before trusting a delivery; the SDKs have `verify_webhook` / `verifyWebhook`. - **Delivery.** Answer `2xx` within 10 seconds. Anything else (redirects aren't followed) is retried with backoff, from 10 seconds growing to 30 minutes; a webhook failing for a day is turned off and your owners and admins are told. Add it again to turn it back on. - **Where.** `https://` on port 443, to a name that resolves to an internet address (checked when connecting; private and local addresses are refused). # Platforms If you run a product on 639 Shell (agents for your own customers, for example), a **platform** org makes an org for each of your customers and works in it by API. The platform tier is set by us: ask for it. ```sh curl -X POST .../v1/orgs -d '{"name": "Customer A", "limits": {"machines": 20, "cores": 32}}' # → {"id": "o_…"} curl -H '639-Org: o_…' -X POST .../v1/machines -d '{"size": "s2", "image": "ubuntu"}' curl -H '639-Org: o_…' -X POST .../v1/keys -d '{"name": "customer-a-agents"}' curl .../v1/orgs # each with its machine count curl -X DELETE .../v1/orgs/o_… # once it has no machines, IPv4s or VPCs ``` - **Acting in one.** Any `/v1` call with the header `639-Org: ` acts in that managed org. Only the platform org's owners and admins send it. Over MCP an agent acts in its own org only. - **Keys for your customer.** A key made with the header belongs to that managed org alone (it can't reach your platform org or your other customers), for handing to your customer's agents. It works while the person who made it is still an owner or admin of the platform org. - **Limits.** Each managed org has its own limits (machines, cores, RAM, disk, IPv4s; none above your platform's), and all of them together with your own machines stay within your platform's limits: an error says "the platform's pool" when that's what's full. - **Usage and events.** `GET /v1/usage` with the header gives a managed org's usage; `GET /v1/events?managed=true` gives your managed orgs' events with your own. - **Holds.** Suspending or closing your platform org does the same to its managed orgs. A managed org can't make managed orgs. # Benchmarks Our servers aren't the fastest cloud, and we don't claim to be: we're aiming for value. These are the numbers we measured, so you can judge for yourself. Measured 2026-11-13 on 639 Shell (shell.639cloud.com), size `s1` (Ubuntu), over MCP from outside our data center, so every figure includes the network round trip to it. One machine at a time; 10 runs each (20 for commands). | What | Median | 95th percentile | |---|---|---| | Create a machine: the call returns | 0.85 s | 1.77 s | | Create a machine: its first command has run | 1.52 s | 2.47 s | | Fork a running machine: the call returns | 3.43 s | 3.80 s | | Fork a running machine: its first command has run | 4.12 s | 4.42 s | | Wake a sleeping machine: its first command has run | 1.25 s | 1.27 s | | Run a command on an awake machine (round trip) | 0.08 s | 0.11 s | What each step means: - **Create**: `create_machine`, then `exec` with `true` until it succeeds. - **Fork**: `fork_machine` of a running `s1`, then the same first command on the copy. - **Wake**: `sleep`, then `exec`, which wakes it. - **Round trip**: `exec` with `true` on a machine that's already awake. Your numbers will differ with where you are, the machine's size and image, and how busy the server is. We'll measure again as 639 Shell changes, and this page will say when.