# A deep dive into exe.dev

> exe.dev gives you persistent Linux VMs over SSH, each with an HTTPS URL, private-by-default sharing, and a built-in coding agent. How it works, what it costs, and how I use it.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-09-07 | Topics: [AI](https://flaviocopes.com/tags/ai/) | Canonical: https://flaviocopes.com/exe-dev/

[exe.dev](https://exe.dev) is a subscription that gives you Linux virtual machines. You create one with `ssh exe.dev new`. Less than a second later you have a real computer with root access, a disk that persists, and an HTTPS address that only you can open.

That is the whole product. Everything else builds on that.

I read the docs and the team's blog, then moved a few of my own projects onto it. This post explains what exe.dev is, how it works under the hood, what it costs, and how I use it.

## Who built exe.dev?

exe.dev is built by a small team in the Bay Area.

The CEO is David Crawshaw. He co-founded Tailscale and was its CTO from 2019 to 2024. Josh Bleecher Snyder was an early Tailscale engineer and is a long-time contributor to the Go compiler and runtime. Philip Zeyliger worked at Airtable, Cloudera, and Google. Ian Lance Taylor, a long-time Go core contributor, writes on the company blog.

These are people who spent years on networking, SSH, and Linux at scale. You can feel it in the product.

Before exe.dev, the same team built [Sketch](https://github.com/boldsoftware/sketch), an open source coding agent that ran in Docker containers. Working on it, they kept hitting the same wall. They needed many more machines than before. Every small tool an agent produced needed a place to run. Containers were not enough. So they built the platform they wanted, and Sketch became Shelley.

exe.dev [went public](https://blog.exe.dev/meet-exe.dev) as a developer preview on December 15, 2025. In April 2026 the company [announced a Series A](https://blog.exe.dev/series-a), for a total of $35 million raised, from Amplify, CRV, and Heavybit.

One detail from that announcement: they are not building on AWS or another cloud. They rent bare metal in data centers, wrote their own global load balancer, and run their own DNS.

## What do you get?

The docs describe exe.dev in one line: "VMs, on the internet, quickly."

In practice you get:

- Real KVM virtual machines, not containers sharing a kernel
- Root access, `apt`, `systemd`, and Docker if you want it
- A persistent disk. Files survive restarts. VMs are never put to sleep
- `https://vmname.exe.xyz/`, an HTTPS address for each VM with TLS handled for you
- Private by default. Only you can open that URL until you share it
- SSH access with `ssh vmname.exe.xyz`
- Shelley, a web coding agent, pre-installed and reachable from your phone

The homepage says "Computers for developers and agents." Both are the target.

![The exe.dev homepage: Computers for developers and agents](https://flaviocopes.com/images/exe-dev/homepage.png)

## You pay for a pool, not for VMs

On most clouds you pay per VM. Each one has its own CPU, RAM, and a monthly price. Idle VMs still cost money, so you keep few of them.

exe.dev flips this. You buy a pool of CPU and RAM. Then you create as many VMs as you want inside it, up to 50. One VM can use the whole pool. Or twenty small ones can split it.

Pricing at the time of writing:

| Plan | Pool | Individual | Team, per user |
| --- | --- | --- | --- |
| Small | 2 vCPU, 8 GB RAM | $20/mo | $25/mo |
| Medium | 4 vCPU, 16 GB RAM | $40/mo | $50/mo |
| Large | 8 vCPU, 32 GB RAM | $80/mo | $100/mo |
| XLarge | 16 vCPU, 64 GB RAM | $160/mo | $200/mo |

![The exe.dev pricing page, with a slider to pick the pool size](https://flaviocopes.com/images/exe-dev/pricing.png)

Every plan includes 100 GB of pooled disk, 200 GB of outbound transfer per month (250 GB on Team), and a $20 monthly credit for Shelley. Extra disk is $0.08 per GB per month. Extra transfer is $0.05 per GB. Inbound traffic is free.

Each VM starts with a 25 GB disk. Disk is billed on actual filesystem usage, not allocated size. An idle VM takes nothing from your CPU pool. Only its disk counts.

If none of the four tiers fits, there is a fully usage-based Cloud Pool plan. You email support and they price it for your workload.

`ssh exe.dev stat <vm>` shows what a VM is using right now and over the last 24 hours, with sparklines. Here is mine after the Astro deploy: 4.8 GB of the 25 GB disk in use, the CPU idle, out of a 2 vCPU pool.

![Output of ssh exe.dev stat for jitney-of-ottoman: vCPU, disk, IO, and network usage with sparklines](https://flaviocopes.com/images/exe-dev/stat.png)

Memory is the soft limit. exe.dev tracks active memory and contacts you if you stay over your plan for a long time. The guest kernel cooperates with the host to give memory back when it is not needed.

This model changes how you use machines. A VM becomes as cheap as a folder. You make one per project, per experiment, per agent task. You stop watching the meter.

There is a free trial. An invite link from an existing user gives you 30 days.

## How do you get started?

You can register at [exe.dev/auth](https://exe.dev/auth) with your email or a passkey.

![The exe.dev login and registration form](https://flaviocopes.com/images/exe-dev/auth.png)

But you do not need the browser. You can start with SSH:

```bash
ssh exe.dev
```

The first time, exe.dev asks for your email:

![ssh exe.dev asking for an email address](https://flaviocopes.com/images/exe-dev/ssh-email.png)

You confirm it by opening a link, and exe.dev registers your SSH public key. Then you land in a small interactive shell with an `exe.dev ▶` prompt.

![Email verified, registration complete, welcome to exe.dev](https://flaviocopes.com/images/exe-dev/ssh-verified.png)

Type `help` to see every command:

![The list of exe.dev commands](https://flaviocopes.com/images/exe-dev/help.png)

Before you trust it, check the host key fingerprint against the one in the docs:

```text
SHA256:JJOP/lwiBGOMilfONPWZCXUrfK154cnJFXcqlsi6lPo
```

Create a VM from that shell with `new`:

![Creating a VM from the exe.dev shell, ready in 0.5 seconds](https://flaviocopes.com/images/exe-dev/new.png)

Half a second, and you get three addresses: the coding agent, the app, and SSH.

You can also do it in one line from your terminal:

```bash
ssh exe.dev new
```

![Creating a VM from the terminal with ssh exe.dev new](https://flaviocopes.com/images/exe-dev/new-from-terminal.png)

You get a generated name like `jitney-of-ottoman`. You can pass your own with `--name`, but names are unique across the whole platform, so the short ones are taken. I kept the generated one. You can also set the size:

```bash
ssh exe.dev new --cpu 2 --memory 4GB --disk 40GB
```

Now SSH into it:

```bash
ssh jitney-of-ottoman.exe.xyz
```

You are on a fresh Ubuntu machine, logged in as `exedev` with `sudo`. It already answers at `https://jitney-of-ottoman.exe.xyz/`, though nothing is listening yet.

![Logged into a VM over SSH: the disk is persistent, you have sudo](https://flaviocopes.com/images/exe-dev/ssh-vm.png)

Every command you type after `ssh exe.dev` is also the API. Add `--json` and you can script it:

```bash
ssh exe.dev ls --json | jq '.vms[].vm_name'
```

There is a web dashboard at exe.dev too, with a browser terminal and one-click links to open a VM in VS Code. Those links use VS Code's Remote SSH and look like this:

```text
vscode://vscode-remote/ssh-remote+jitney-of-ottoman.exe.xyz/home/exedev
```

Nothing exe.dev specific is going on. The VM is an SSH host, so Cursor works the same way: open a remote window, type `jitney-of-ottoman.exe.xyz`, and you are editing on the VM.

You can create a VM from the dashboard as well, starting from a template like Web App, Devbox, or Data Analysis, and typing what you want built.

![The exe.dev dashboard: create your first VM from a template](https://flaviocopes.com/images/exe-dev/dashboard.png)

The browser terminal lives at `https://<vmname>.xterm.exe.xyz/`. It is a real shell on the VM, with tabs, so you can work from a machine where you have no SSH key set up:

![The browser terminal for a VM at vmname.xterm.exe.xyz](https://flaviocopes.com/images/exe-dev/browser-terminal.png)

The UI shows the command it runs for every action, so you always know how to script it. There is also an iOS app.

## How does exe.dev work under the hood?

The FAQ has a short section on this, and it explains the speed.

Your VM runs on a bare metal machine exe.dev rents. The hypervisor is [Cloud Hypervisor](https://github.com/cloud-hypervisor/cloud-hypervisor), though they say that may change.

Most providers start a VM from a disk image. exe.dev starts from a container image. The default is [exeuntu](https://github.com/boldsoftware/exeuntu), an Ubuntu 24.04 image with an open source Dockerfile. exe.dev puts it on a block device and boots a VM around it. That is why creating a VM takes well under a second.

The trade is that you do not pick the kernel. exe.dev provides it.

Networking is different too. Your VM does not get its own public IP. exe.dev terminates TLS at its edge and proxies HTTPS to your VM.

SSH is trickier, because SSH has no `Host` header. The web solved shared IPs decades ago: the browser sends the hostname, the proxy routes on it. SSH sends nothing like that. exe.dev's [solution](https://blog.exe.dev/ssh-host-header) is neat. They keep a pool of public IPs and make sure each of your VMs has an IP that is unique among your VMs. When you connect, your SSH public key identifies you, and the IP you hit identifies which of your VMs you want.

And VMs are persistent on purpose. They are not paused when idle. Disks are not wiped. The team [wrote about why](https://blog.exe.dev/persistent). Pausing VMs breaks cron jobs. Ephemeral disks push you into a remote database, which means a distributed system on day one. They wanted something that feels like a laptop, not a container you can barely get a shell on. They develop exe.dev on exe.dev VMs, their blog runs on one, and so do the bots that run the company, more on those below.

This is a big plus over the alternatives. Codespaces stop after 30 minutes of inactivity. Fly Machines auto-stop when idle. Agent sandboxes like [Vercel Sandbox](https://flaviocopes.com/vercel-sandbox/) are gone when the task ends. Every time you come back, you wait for a cold start and hope the state is still there. On exe.dev the machine is where you left it, with the process still running and the file still open.

## The HTTPS proxy

Every VM has a URL: `https://vmname.exe.xyz/`.

exe.dev picks a port to forward. For the default image, that is 8000. Start a server on port 8000 and your URL works. Change the port with:

```bash
ssh exe.dev share port jitney-of-ottoman 3000
```

Ports 3000 through 9999 are also reachable directly at `https://jitney-of-ottoman.exe.xyz:3456/`, but only for people with access to the VM. Only one port can be public.

By default the proxy is private. When someone opens the URL, exe.dev sends them to log in, then checks whether you shared the VM with them. To make it public:

```bash
ssh exe.dev share set-public jitney-of-ottoman
```

The proxy adds the usual `X-Forwarded-Proto`, `X-Forwarded-Host`, and `X-Forwarded-For` headers so your app can reconstruct the original request. If headers are new to you, my free [HTTP course](https://flaviocopes.com/courses/http/) covers them.

One gotcha. Dev servers like Next.js and Vite reject requests from unknown hosts. Add your hostname to `allowedDevOrigins` (Next.js) or `server.allowedHosts` (Vite), and bind to `0.0.0.0`.

## Custom domains

You can point your own domain at a VM.

Add a CNAME at your DNS provider:

```text
test.flaviocopes.com  CNAME  jitney-of-ottoman.exe.xyz
```

Then register it:

```bash
ssh exe.dev domain add jitney-of-ottoman test.flaviocopes.com
```

![Registering test.flaviocopes.com for the VM with ssh exe.dev domain add](https://flaviocopes.com/images/exe-dev/domain-add.png)

exe.dev checks the DNS, then issues a TLS certificate. Until you register the domain, requests to it get a `421 Misdirected Request`. That is deliberate. Nobody can CNAME a random hostname at your VM and send you traffic.

In the Cloudflare dashboard, the record looks like this:

![Adding a CNAME record in Cloudflare that points at an exe.dev VM](https://flaviocopes.com/images/exe-dev/cloudflare-cname.png)

Apex domains need an ALIAS or a flattened CNAME. If Cloudflare manages your DNS, turn the proxy (orange cloud) off for that record, which is on by default as in the screenshot. Otherwise the CNAME target gets replaced with Cloudflare IPs and the routing breaks.

My free [DNS course](https://flaviocopes.com/courses/dns/) explains CNAME, ALIAS, and why apex records are special. The [TLS and certificates course](https://flaviocopes.com/courses/tls-certificates/) covers what exe.dev does when it issues those certificates.

## How do you deploy an Astro site?

Let's put the pieces together. We create the default Astro project on the VM, preview it privately, then serve the built site at `https://test.flaviocopes.com/`.

SSH into the VM:

```bash
ssh jitney-of-ottoman.exe.xyz
```

The default image ships Go, Python, Docker, and nginx, but not Node.js. Install the current LTS from NodeSource:

```bash
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get install -y nodejs
node -v
```

![node -v on the VM prints the installed Node.js LTS version](https://flaviocopes.com/images/exe-dev/node-version.png)

Now create the project. This is the same command you would run on your Mac, with flags so it does not ask questions:

```bash
npm create astro@latest -- mysite --template basics --install --git --yes
cd mysite
```

### Preview it privately

The Astro dev server listens on port 4321. Ports 3000 through 9999 are reachable at the VM hostname for anyone with access to the VM, so we can use it as a private preview:

```bash
npm run dev -- --host --allowed-hosts jitney-of-ottoman.exe.xyz
```

![The Astro dev server running on the VM, listening on port 4321](https://flaviocopes.com/images/exe-dev/astro-dev.png)

Open `https://jitney-of-ottoman.exe.xyz:4321/` and you see the Astro welcome page. Nobody else can, because the VM is still private.

![The Astro welcome page served by the dev server at jitney-of-ottoman.exe.xyz:4321](https://flaviocopes.com/images/exe-dev/astro-preview-4321.png)

Both flags matter. `--host` makes the server listen on all interfaces, otherwise the proxy cannot reach it. `--allowed-hosts` tells Vite to accept requests for that hostname. Without it you get a "Blocked request" page, because the `Host` header is not `localhost`.

### Serve the built site with nginx

For the real thing we build the site and let nginx serve the static files. nginx is already installed on the default image, just disabled. Its default config serves `/var/www/html` on port 8000, which is exactly the port the proxy forwards to.

Stop the dev server, then:

```bash
npm run build
sudo rm -rf /var/www/html/*
sudo cp -r dist/. /var/www/html/
sudo systemctl enable --now nginx
```

Check it from inside the VM:

```bash
curl -I http://localhost:8000/
```

![Copying the build into /var/www/html, enabling nginx, and curl returning 200 OK](https://flaviocopes.com/images/exe-dev/nginx-deploy.png)

You get a `200 OK` from nginx, and `https://jitney-of-ottoman.exe.xyz/` now shows the site on the default port, no `:4321` needed. Still private, though. If the page does not load, check that nginx is listening with `sudo ss -ltnp | grep 8000`.

![The built Astro site served by nginx at jitney-of-ottoman.exe.xyz](https://flaviocopes.com/images/exe-dev/astro-live.png)

### Make it public

Two things need to be true for `https://test.flaviocopes.com/` to work for everyone. The domain must be registered for the VM, which we did in the previous section. And the VM must be public:

```bash
ssh exe.dev share set-public jitney-of-ottoman
```

That is it. The custom domain follows the VM's sharing settings, so `https://test.flaviocopes.com/` serves the Astro site to anyone, with a valid certificate, and `https://jitney-of-ottoman.exe.xyz/` keeps working too.

![The Astro site live at test.flaviocopes.com](https://flaviocopes.com/images/exe-dev/custom-domain-live.png)

Every change from now on is `npm run build` and a copy into `/var/www/html`. I put those two lines in a `deploy.sh`. Or I ask Shelley to do it.

If your Astro site is server-rendered instead of static, skip nginx. Add the Node adapter, run `node dist/server/entry.mjs` as a `systemd` service with `PORT=8000`, and the rest of the steps are the same. That is how I run Events Logger, further down.

## Sharing is the auth system

This is where exe.dev stops feeling like a VPS.

Sharing works like a Google Doc. There are two levels. Web access lets someone open the private HTTPS URL after logging in. Root access adds SSH, the browser terminal, and Shelley.

Share the web app with one person:

```bash
ssh exe.dev share add jitney-of-ottoman anna@example.com
```

Give a trusted collaborator root:

```bash
ssh exe.dev share add jitney-of-ottoman anna@example.com --root
```

Or create a link. Anyone who opens it gets web access after they register:

```bash
ssh exe.dev share add-link jitney-of-ottoman
```

You get a URL with a `?share=` token, and the command to revoke it later:

![Creating a share link for a VM with ssh exe.dev share add-link](https://flaviocopes.com/images/exe-dev/share-link.png)

Root access requires the other person to already have an exe.dev account. Links only ever grant web access.

### Login with exe

The proxy also tells your app who is visiting. Authenticated requests carry two headers:

- `X-ExeDev-UserID`, a stable user id
- `X-ExeDev-Email`, the user's email

So you can build a private internal tool with zero login code. Read the email header, decide what that user can do, done. No password hashing, no OAuth setup, no email recovery flow.

If your site is public, you can still send someone to `/__exe.dev/login?redirect=/admin` to force a login and get those headers back.

### Not even support gets in

The same rules apply to exe.dev's own staff. Support cannot log into your VM. If you open a ticket and want them to look, you grant it yourself, and you take it back:

```bash
ssh exe.dev grant-support-root jitney-of-ottoman on
ssh exe.dev grant-support-root jitney-of-ottoman off
```

A small command, but it tells you how they think about your machine.

## Email in and out of a VM

A VM can have an email address. Turn it on:

```bash
ssh exe.dev share receive-email jitney-of-ottoman on
```

Now any message sent to `anything@jitney-of-ottoman.exe.xyz` lands as a file in `~/Maildir/new/` on the VM. A `Delivered-To:` header on the first line tells you which address it was sent to. To react to new mail, poll the directory or use `inotifywait`:

```bash
inotifywait -m ~/Maildir/new -e create -e moved_to |
  while read dir action file; do
    # process "$dir/$file", then move it out of new/
    mv "$dir/$file" ~/Maildir/cur/
  done
```

You must move messages out of `new/`. Past 1000 files, exe.dev turns receiving off until you clear the backlog.

Sending works through a link-local endpoint inside the VM. No SMTP, no API key:

```bash
curl -X POST http://169.254.169.254/gateway/email/send \
  -H "Content-Type: application/json" \
  -d '{"to": "flavio@flaviocopes.com", "subject": "Build done", "body": "dist/ is on the VM."}'
```

Attachments go in an `attachments` array, base64 encoded. Sending is rate limited, and the recipient list is deliberately narrow: you, your team, people you shared the VM with, and people who emailed the VM first. Replies to correspondents are scoped to their thread unless they email `subscribe` back, and an `unsubscribe` from them beats every other permission, including yours.

Why this matters: email is an input channel your agent already understands. Forward a bug report to the VM, a script wakes Shelley with the message body, and the fix comes back as a reply. No webhook, no bot token, no Slack app to register.

Limits at the time of writing: 1 MB per message, no custom domains for inbound mail (only `*.exe.xyz`), and no spam filtering at all. They deliver the bits.

## Integrations: secrets your VM never sees

Integrations solve an old problem. Your agent needs a GitHub token, an API key, a Slack webhook. You put it in an environment variable on the machine. Now the agent, and anything the agent runs, can read it and leak it.

exe.dev moves the secret out of the VM. You create an integration and attach it to a VM. Inside the VM, you get an internal hostname like `myapi.int.exe.xyz`. Your code makes a plain, unauthenticated request to it. At the network edge, exe.dev adds the real credential and forwards the request to the target.

Here is an HTTP proxy integration that injects a bearer token:

```bash
ssh exe.dev integrations add http-proxy \
  --name myapi \
  --target https://api.example.com \
  --bearer sk-live-... \
  --attach vm:jitney-of-ottoman
```

From inside `jitney-of-ottoman`:

```bash
curl http://myapi.int.exe.xyz/v1/customers
```

The request reaches `api.example.com` with the `Authorization` header set. The VM never had the key. `integrations list --json` shows it as `***`.

`--bearer=-` reads the token from stdin, so you never paste it into the terminal. I keep my API keys in 1Password, and its `op` CLI can print one straight into the command:

```bash
op read "op://Private/Resend/api-key" | ssh exe.dev integrations add http-proxy \
  --name resend --target https://api.resend.com --bearer=-
```

The key goes from 1Password to exe.dev without touching my shell history or the VM.

### GitHub

The GitHub integration replaces personal access tokens. You install the exe.dev GitHub App, then create an integration per repo:

```bash
ssh exe.dev integrations add github --name site \
  --repository flaviocopes/flaviocopes.com --attach vm:jitney-of-ottoman
```

Inside the VM:

```bash
git clone https://github.int.exe.xyz/flaviocopes/flaviocopes.com.git
```

The `gh` CLI works too, with `GH_HOST=github.int.exe.xyz`. You can mark an integration read-only when the agent should look but not push. Add `--act-as-user` if you want commits attributed to you rather than the bot.

### LLM

New accounts get a default `llm` integration attached to every VM. It exposes Anthropic, OpenAI, and Fireworks models at `https://llm.int.exe.xyz`, paid from your monthly Shelley credit. No API keys on the VM.

You can swap the source per provider: exe.dev's managed gateway, your own API key, or a ChatGPT subscription for the OpenAI models. Shelley, Codex, Claude Code, and `pi` can all use it.

### The rest

A reflection integration lets a VM discover its own metadata and attached integrations. Agents use it to find the LLM endpoint and the repos they can clone. VMs are isolated from each other by default, so there is a VM-to-VM integration when one needs to call another. Slack and Discord integrations come in send-only and full bot flavors, with the token held off the VM. AWS and GCP workload identity federation let a VM assume a cloud role with a short-lived token instead of a stored key. The catalog covers over a hundred services.

## Shelley, the built-in coding agent

Every exeuntu VM runs Shelley on port 9999. Open `https://jitney-of-ottoman.shelley.exe.xyz/` and you are talking to a coding agent that lives on the machine.

Shelley is [open source](https://github.com/boldsoftware/shelley). It is web-based and works on a phone. It is multi-model: pick Claude, GPT, or other models from the default LLM integration, or bring your own key. It has a browser tool that drives a Chromium, takes screenshots, and profiles pages. It reads `AGENTS.md` files, both in your project and at `~/.config/shelley/AGENTS.md`, and notices `CLAUDE.md` too. My post on [AGENTS.md](https://flaviocopes.com/agents-md/) explains what to put in one.

Because it runs on the VM, it can build the app and also run it. Ask it to start a web server, then open `https://jitney-of-ottoman.exe.xyz/` on your phone to test. The team calls this the killer feature and I agree. Most of my time with coding agents goes into testing what they did.

The same shell makes it a sysadmin for the box. You can ask it what the VM exposes to the internet, why nginx stopped answering, or what is filling the disk. It runs `ss`, reads the logs, and fixes what it finds. On a VPS those questions mean twenty minutes of searching for the right command. Here they are a chat message.

Long conversations get expensive because every message is re-sent to the model. Shelley handles this with generations. Compact a conversation and only the recent part goes to the LLM.

You can also hand a VM a task at creation. I tried it with this:

```bash
echo 'build a small link shortener with SQLite' | ssh exe.dev new --prompt=/dev/stdin
```

exe.dev created a VM called `vanity-router`, printed its three URLs, and sent the prompt to Shelley.

![Creating a VM with a prompt: exe.dev prints the Shelley, app, and SSH URLs, then hands the task to Shelley](https://flaviocopes.com/images/exe-dev/prompt-new.png)

I opened `https://vanity-router.shelley.exe.xyz/` to watch. Shelley was already writing the SQLite schema for the `links` table.

![Shelley working in the browser: the conversation shows shell commands and the SQL migration it wrote](https://flaviocopes.com/images/exe-dev/shelley-working.png)

Two minutes later it was done. Custom aliases, click tracking, a small dashboard, tests, and a systemd service so the app survives a reboot. The terminal on my Mac got the same summary and the link to continue the conversation.

![Shelley's final message listing the features it built and the app URL](https://flaviocopes.com/images/exe-dev/shelley-done.png)

![The terminal after the run: each tool call, the summary, and the Shelley URL to continue](https://flaviocopes.com/images/exe-dev/prompt-finished.png)

And the app was live at `https://vanity-router.exe.xyz/`. Go server, SQLite file, one VM. I did not open an editor.

![The link shortener Shelley built, running at vanity-router.exe.xyz](https://flaviocopes.com/images/exe-dev/link-shortener.png)

Shelley is optional. `claude`, `codex`, and `pi` are pre-installed on the default image. If you do not want Shelley, run `sudo systemctl disable --now shelley.service`.

If you let it choose, Shelley builds on what the team calls the GUTS stack: Go, Unix, TypeScript, SQLite. One server, one file database, one machine. They bet on the monolith.

## Customizing VMs

Three ways.

Use SSH. `scp`, `rsync`, clone a repo, run your dotfiles script. It is a Linux box.

Docker is part of that. The default image ships the Docker daemon, so `docker run --rm alpine echo hello` works out of the box, and so does `docker compose up` for an app plus Postgres on one VM. That is how most self-hosted software gets installed here. The exe.dev docs have walkthroughs for Gitea, Forgejo, and Cal.diy, all with exe.dev login in front. Docker inside a VM, not a container pretending to be one.

Use a custom image. Any container image works as the base:

```bash
ssh exe.dev new --image ubuntu:22.04
```

Private registries work with `--registry-auth`. Add the label `exe.dev/install-shelley=true` to your Dockerfile and exe.dev installs Shelley on boot.

Use a setup script. The default image runs `/exe.dev/setup` once at first boot:

```bash
cat setup.sh | ssh exe.dev new --setup-script /dev/stdin
```

You can store a default setup script for every future VM with `defaults write`.

Once a VM is how you like it, clone it:

```bash
ssh exe.dev cp jitney-of-ottoman jitney-experiment
```

That is my favorite command. Get one VM right, then stamp copies.

The rest of the lifecycle is short. `restart` reboots a VM. `rename` and `comment` help when you have twenty of them. `rm` deletes one, disk included. There is no `stop`. VMs are meant to stay up, and since an idle one costs you nothing from the pool, there is little reason to want one.

Other useful ones: `resize` changes CPU, memory, or disk. `tag` groups VMs so integrations can attach to every VM with a tag. `stat` shows CPU, disk, IO, and network for a VM.

### Deploy on exe.dev buttons

Any GitHub repo can get a one-click deploy. Open this URL with your repo in it:

```text
https://exe.dev/new?repo=https://github.com/OWNER/REPO
```

exe.dev creates a VM, clones the repo, opens Shelley, and tells it to get the thing running. If the repo has an `AGENTS.md`, Shelley follows it. For a public repo you can put the button in the README:

```markdown
[![Deploy on exe.dev](https://raw.githubusercontent.com/boldsoftware/exe.dev/main/assets/buttons/deploy-on-exe-dev.png)](https://exe.dev/new?repo=https://github.com/OWNER/REPO)
```

There is a second button, "Build with Shelley", that points at the same URL. The difference is only the label.

A related trick is `https://exe.dev/suggest?command=share+set-public+jitney-of-ottoman`. It opens a page that shows one command, explains what it does, and offers a Run button. It exists so an agent can hand you an action it should not take on its own, like making a VM public.

## The HTTPS API

The SSH API is for humans. For code, there is an HTTPS version.

It is the same commands, sent as a POST body:

```bash
curl -X POST https://exe.dev/exec \
  -H "Authorization: Bearer exe1.AAA" \
  -d 'new --name sbx-42 --image exeuntu'
```

Generate a token with `ssh exe.dev ssh-key generate-api-key --exp=30d`. With `--cmds` you can restrict a token to specific commands, or to a single VM.

Run a command on a VM the same way:

```bash
curl -X POST https://exe.dev/exec \
  -H "Authorization: Bearer exe1.AAA" \
  -d 'ssh sbx-42 python3 -c "print(2+2)"'
```

The output comes back as the response body. The exit code is in an `X-Exe-Exit` trailer.

This is how exe.dev sells itself as an agent sandbox. Create a VM, run work, delete it, all from your program. The difference from other sandboxes is that this one is a full computer with a disk. It can also stay around for months.

For API servers running on a VM, there are VM-scoped tokens. The proxy accepts them as bearer or basic auth, so `git push` over HTTPS to a VM works.

### Teaching your own agent

You do not have to use Shelley to get an agent that knows exe.dev. Three options, from least to most setup.

**Tell it to use the CLI.** There is no MCP server for exe.dev, and it does not need one. Every command is `ssh exe.dev <command>`, every command takes `--json`, and every coding agent can already run a shell. One line in your `AGENTS.md` is enough to start:

```markdown
To create or manage servers, run `ssh exe.dev help` and use those commands with `--json`.
```

I tested this with the Cursor CLI agent in an empty folder containing only that `AGENTS.md`. I asked it to create a server, forward the proxy to port 3000, and give me the URL. It ran `ssh exe.dev help`, then `new --json`, then `share port crane-reef 3000`, and answered with `https://crane-reef.exe.xyz` in under a minute. `ssh exe.dev ls --json` confirmed the port. Then I ran `rm crane-reef`.

**Install the skill.** The team publishes an [agent skill](https://github.com/boldsoftware/exe.dev/blob/main/skill/SKILL.md), a `SKILL.md` in the format Cursor, Claude Code, and Codex all read. Drop it in your skills folder and the agent picks it up whenever you mention exe.dev or a `*.exe.xyz` host. It is short, and most of it is the gotchas an agent hits in a sandbox: `ssh exe.dev` is a control shell and does not do `scp` or arbitrary commands, `ssh <vm>.exe.xyz` is the full SSH host that does, and a first connection can hang on a host key prompt the agent cannot see, so pass `-o StrictHostKeyChecking=accept-new`. It also recommends an SSH config block so both destinations use the same key:

```text
Host exe.dev *.exe.xyz
  IdentitiesOnly yes
  IdentityFile ~/.ssh/id_ed25519
```

**Point it at the docs.** The docs are served as plain markdown. `exe.dev/docs.md` is an index that links out, so the agent loads one page and follows only what it needs. `exe.dev/docs/all.md` is everything in one file when you want the whole thing in context. I did the same for this site, and wrote about it in [Serving my site as Markdown to AI agents](https://flaviocopes.com/serving-markdown-to-ai-agents/).

The pattern I like: the skill for Cursor and Claude Code on my Mac, and `AGENTS.md` inside each VM for Shelley. Both end up running the same `ssh exe.dev` commands.

## Teams and regions

Team plans add per-user pools, admin visibility into every VM, SSO with Google or any OIDC provider, and team-wide sharing. Members can borrow some of a teammate's unused capacity.

Each account lives in one region. The options at the time of writing are Los Angeles, New York, Dallas, Frankfurt, Tokyo, Sydney, Singapore, and London. All of an account's VMs live in the same region.

## What the team runs on exe.dev

The best argument for "computers for agents" is how the team runs its own company. In August 2026 Philip Zeyliger published [an inventory](https://blog.exe.dev/inventory) of the bots behind exe.dev. Most of them are agents, and each one is a small program on a VM.

- A security reviewer that walks recent changes looking for problems.
- Sisyphus, which picks up every alert, reads logs, metrics, and source code, compares notes with its own past investigations, and writes up what it found before a human looks.
- A daily email with interesting trends in the logs.
- A bot that watches CI for flaky and slow tests and proposes fixes.
- Reporter bots that summarize the day's commits and support threads in Slack.
- Athena, which supervises deploys.
- The blog itself. It started as Markdown files in git. Now it is a CMS with collaborative editing, revision history, comments, and a content calendar. Shelley runs on the same VM and imports a draft from whatever you paste in.

Even their UI tests changed shape. A test is a paragraph of text describing the behavior you want. A cached translation into browser steps, checked into git, keeps it fast. When it fails, CI asks an LLM to heal it and commits the fix.

Athena got [its own post](https://blog.exe.dev/athena-deploys-exe), and the reasoning applies to more than deploys. Deploys are the moment things break, so people delay them, so each one gets bigger and scarier. Continuous deployment fixes that, but the hard part is the automated gate. The list of things that can go wrong has no end, and a gate that misses one takes you down. The team's point is that this has the shape of an agent problem, not a code problem: lots of rich data, a long tail of states, a handful of runs a day.

So Athena has read access to git, metrics, and logs. At each wave it decides whether to continue, which machines go next, and whether to pause or refuse to start. It reports in Slack. The honest part of the post: the author admits he could probably watch a rollout better than Athena. But he would not, because nobody has that patience. Athena does the job that in practice was not getting done.

One warning from the inventory, because it applies to any bot you build: the [lethal trifecta](https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/). An agent that reads private data, ingests untrusted content, and can talk to the outside world can be tricked into leaking the first through the third. One VM per bot helps, because the blast radius is that VM. The other half is the integrations section above: give the bot exactly the tools it needs, and never a raw credential.

## How I use exe.dev

I have been running a few of my projects on it. Here is what I did.

### Agent work, off my Mac

I ship small products almost every day, and most of them get changed by coding agents afterwards. Those agents used to run on my Mac. That works, but the agent has my whole filesystem, my Keychain, and my SSH keys in reach.

Now I keep one VM per project. I attach a GitHub integration for the repo, clone it from `github.int.exe.xyz`, and let Claude Code run without permission prompts. No token sits on the machine. The worst it can destroy is that VM, and `cp` from a clean one takes two seconds.

The first repo I moved was this site. The agent edits posts, runs the Astro dev server on port 3000, and I check the result at the VM URL, often from my phone. The site still deploys from Cloudflare Pages. The VM is only where the agent works.

The session lives on the VM, not on the laptop. Start the agent inside `tmux`, close the lid, and it keeps going. From another machine, or from the browser terminal on the phone, `tmux attach` puts you back in the same conversation, at the same line. A long refactor no longer depends on my Mac staying awake.

### Events Logger on a VM

[Events Logger](https://flaviocopes.com/software/events-logger/) is an Astro SSR app with HTMX, Alpine.js, and a SQLite database. One process, one file, one disk. That is the shape exe.dev is built for.

I cloned it on a VM, ran `npm run build`, and started the Node server as a `systemd` service so it comes back after a reboot. Astro's Node adapter listens on 4321, so I pointed the proxy at it:

```bash
ssh exe.dev share port amber-lantern 4321
```

Then I pushed events from the CLI on my Mac to `https://amber-lantern.exe.xyz/api/events`. The dashboard stays private. I shared it with one other person and read `X-ExeDev-Email` instead of building a login. Shelley runs next to it, so when I want a new chart I ask for it.

### A base VM I stamp copies of

I set up one VM the way I like it: Node, pnpm, my dotfiles, an `AGENTS.md` in `~/.config/shelley/`. I never run anything on it. When I start a project, I `cp` it and give the copy a name. Two seconds later I am in a machine that already knows how I work.

### Demos with a link

Instead of a screen recording, I share a link. I loaded the Events Logger demo scenarios on a second VM and ran `share add-link`. Whoever opens it registers and clicks around a live dashboard. I revoke the link when I am done.

### From the phone

One morning I created a VM from the iOS app with a prompt: check the TLS expiry of my domains and show the result as a page. Shelley wrote a small Go program, put it on port 8000, and by lunch the list was at the VM URL. The whole thing took one message from me.

### What I plan to do next

A few things I have not done yet, but the pool model makes them obvious.

**Bootcamp demos.** Every project we build in the [bootcamp](https://bootcamp.dev/) can live on its own VM. Students open a link and see the finished thing running before we write a line. I can also give each student a clone of the week's starting point, with Node and the repo already there, so nobody loses the first hour to setup.

**A live demo for every app.** Each app in my [software catalog](https://flaviocopes.com/software/) gets a VM with demo data and a public URL. A cron job resets the database every night. You try the app before you download the source. Screenshots never sold anything as well as a thing you can click. And each README gets a "Deploy on exe.dev" button, so trying it on your own VM is one click too.

**Integration testing.** Most test suites mock the database and the network. On exe.dev, a test run can `cp` a base VM, deploy the branch, and hit the real HTTPS URL. Real Postgres, real SQLite file, real TLS. When the run ends, `rm` the VM. The pool makes a fresh machine per run cost nothing extra.

**Webhooks without tunnels.** Paddle and Creem need a public HTTPS endpoint to deliver payment webhooks. Today that means ngrok on my Mac. A VM has a stable HTTPS URL from the start, so I can point the sandbox webhooks at it and leave it running for days.

**One VM per pull request.** A GitHub Action clones the base VM, deploys the branch, and comments the URL on the PR. Reviewers click. When the PR merges, the action deletes the VM. That is a preview deployment, but for anything with a server and a database, not only static sites.

**Long-running jobs.** Scrapers, feed checkers, a bot that watches a Discord channel. Anything that should run for weeks and does not fit the request-response shape of Workers. Right now these run on my Mac, which means they stop when I close the lid.

**A deploy watcher.** Cloudflare Pages builds this site several times a day, and I find out about a failed build when a post is missing. A bot on a VM can poll the deployments API, read the log, and email me the failing line. When Astro 7.3.0 broke three builds in a row, that email would have pointed at the bad release before I opened the dashboard.

**A remote MCP server.** The tools I give my agents run as MCP servers on my Mac, so they exist only while the Mac is on. On a VM they get a stable HTTPS URL. The VM can stay private: Cursor sends a VM-scoped token as the bearer, and the proxy lets it through. One server, every device, no tunnel.

**A GitHub Actions runner.** GitHub's hosted runners start empty on every run, so each build downloads the same `node_modules` and Docker layers again. A VM registered as a self-hosted runner keeps its disk between runs, and the second build finds a warm cache. A few runners on the same VM share it. Between runs the VM sits idle and takes nothing from the pool.

Where I do not use it:

- Hosting this site. flaviocopes.com is a static Astro build on Cloudflare Pages. It does not need a server.
- Anything built for Cloudflare Workers. [Waiting Lists](https://flaviocopes.com/software/waiting-lists/) uses Workers, D1, and Queues. Those primitives do not exist on a Linux box.
- Local tools. [Port Pilot](https://flaviocopes.com/software/port-pilot/), [Local Hoster](https://flaviocopes.com/software/local-hoster/), and [Livestream Recorder](https://flaviocopes.com/software/livestream-recorder/) are macOS apps. A remote VM cannot see my ports, my certificates, or my screen.
- Heavy egress or big media. The overage math adds up fast.
- Multi-VM systems that need a private network. VMs are isolated. You would reach for Tailscale or the VM-to-VM integration.

## How exe.dev compares

The obvious comparison is a VPS from Hetzner or DigitalOcean. A VPS gives you a public IP and a raw machine. You set up TLS, a reverse proxy, a firewall, and some kind of auth. Every extra VM costs money. exe.dev takes the machine, adds everything around it, and makes extra VMs free inside your pool. You give up the public IP and the choice of kernel.

The other comparison is agent sandboxes. I wrote about [Vercel Sandbox](https://flaviocopes.com/vercel-sandbox/) and [Cloudflare Computer](https://flaviocopes.com/cloudflare-computer/). Both are ephemeral by design. Cloudflare Computer even tries to avoid a container for most of an agent's work. exe.dev goes the other way. The agent gets a whole computer that keeps running after the task, with a disk that remembers.

Remote development environments have been tried many times. Codespaces, Gitpod, a long list of startups. The team addresses this directly in the docs. The difference now is agents. If your development is chat-first, your carefully tuned dotfiles matter less, and starting a task from your phone matters more.

## Limits to know before you sign up

A few things to keep in mind:

- No public IP per VM. Anything that is not HTTP or SSH needs another path.
- VMs do not share a private network.
- You do not pick the kernel.
- One region per account.
- Memory over your plan is tolerated for a while, then they contact you.
- The default image is about 4 GB. Build your own if disk matters.
- Only one port per VM can be public.
- There is no `stop`. A VM runs until you `rm` it.
- Cancel and you keep access to existing VMs, but you cannot create new ones.

And one that matters if you host real data: I found no snapshot or backup feature in the docs. The disk persists, but if you want a copy, that is on you. `cp` gives you a manual snapshot of a whole VM. For a SQLite app like Events Logger, the tool is [Litestream](https://litestream.io/): it runs next to your process and streams every change to an R2 or S3 bucket. `rsync` covers the rest. Cheap, and it turns "the disk persists" into "I can rebuild this anywhere".

On lock-in the team is honest. It is Linux. `rsync` gets you out.

## Closing thoughts

Give an agent a computer. Make VMs free at the margin. Keep everything private until you share it. Move secrets into the proxy.

Each of these is a small idea. Together they describe a cloud that feels built by people who use SSH all day.

I am keeping it.
