# Case: persistent computers for AI agents Case gives AI agents a persistent Linux desktop with Chromium, files, and saved logins. Run it with Docker and connect your agent over MCP, or use Drive, the web interface. Files and saved logins stay with the computer across sleep, wake, and restart. ![Case demo](.github/demo.gif) **[Managed Case](https://case.computer):** We run the computers for you, including DNS, HTTPS, and managed images. **Self-hosted:** Run the open source version on your own machine or server. Start with Docker below. ## Quick start You need Git and Docker 20.10+ with Compose v2. Start Docker, then run: ```bash git clone https://github.com/case-computers/case.git cd case docker compose up --build -d ``` The first build may take a few minutes. Case runs in the background once it starts. Open the [computers page](http://127.0.0.1:4174/deploy), click **+ New computer**, enter a name, and press Enter. Wait for the computer to show **AWAKE**. This setup is for local use. For remote access, follow the [server hosting instructions](#server-hosting) and read [SECURITY.md](SECURITY.md). ## Run your first task ### Use Drive in your browser Drive needs an OpenAI or Anthropic API key to run tasks. 1. Click **DRIVE** next to your computer. 2. Click **KEY**, choose your provider, enter its API key, and click **SAVE**. 3. Send a task, such as: `Open example.com and save a summary to /home/agent/example.txt.` Watch the desktop as the agent works, then open **FILES** to read the summary. The file stays on the computer when you sleep and wake it. Use **CREDENTIALS** to save logins for that computer. ### Connect an existing agent Case supports MCP (Model Context Protocol), which lets an agent use the desktop as a set of tools. The local MCP address is `http://127.0.0.1:8788/mcp`. For Claude Code: ```bash claude mcp add --transport http case http://127.0.0.1:8788/mcp ``` Then ask the agent to list your Case computers and run a task on the one you created. This uses your agent's model connection; you do not need to add a key in Drive. Cursor: ```json { "mcpServers": { "case": { "type": "http", "url": "http://127.0.0.1:8788/mcp" } } } ``` ## Features - **A real desktop:** navigate pages, click and fill elements, upload files, take screenshots, run commands, and inspect network activity. - **Vault logins:** save a credential once through a one-time link or Drive. Case types it into the site's login page without returning the password to the agent. See the [security model](SECURITY.md) for the limits of this protection. - **Human handoff:** 2FA codes, captchas, and approvals pause the run for a human to help through Drive, Telegram, or an Assist link. - **Skills:** the agent saves a completed task as a `SKILL.md` on the computer and follows it next time. The file survives restarts. - **Schedules:** recurring runs use the computer's saved identity. They use `CASE_DRIVE_API_KEY` from `.env` (the same box key as phone chat). Set `CASE_BRAIN_CMD` if you want a different harness. - **Phone chat:** send tasks and answer handoffs through Telegram or ntfy. See [phone setup](#phone-chat). ## Optional setup The defaults are enough to try Case locally. Optional settings are listed in [.env.example](.env.example). For Compose, put them in `.env` next to `compose.yaml`, then run `docker compose up -d` to apply changes. Edit an existing `.env` rather than replacing it. Case sends a few usage numbers that help us see what breaks: a random install id, how many computers and schedules exist, and whether scheduled runs succeed. Never a name, prompt, domain or credential. Like any web request it reaches our server with your IP. Turn it off with `CASE_TELEMETRY=0` (or `DO_NOT_TRACK=1`) in `.env`.
Stop and start again From the repository directory, stop Case with: ```bash docker compose down ``` Your computers' files and saved logins remain on their Docker volumes. To start Case again: ```bash docker compose up -d ``` Open the [computers page](http://127.0.0.1:4174/deploy) and click **WAKE** next to the computer you want to use.
Phone chat: Telegram and ntfy Send tasks and answer handoffs through Telegram or ntfy. Both are optional. Drive connects out to the service, so phone chat works without exposing a local port. Your host and Docker must stay running to receive messages and run tasks. Phone tasks use a shared thread named `Phone`. They need a provider key on the server because there is no browser tab to supply one. Schedules use the same key. If you do not have a `.env` file yet, copy `.env.example` to `.env` next to `compose.yaml`. Add these settings to that file, replacing the placeholder with your key: ```dotenv CASE_DRIVE_PROVIDER=openai # or anthropic CASE_DRIVE_API_KEY= ``` You can also set `CASE_DRIVE_MODEL`. Choose either service below to finish setup. ### Telegram 1. Open [@BotFather](https://t.me/BotFather) in Telegram, send `/newbot`, and follow the prompts to create a bot. Copy its token. Use `/setjoingroups` to disable adding the bot to groups. 2. Add the token to `.env`: ```dotenv CASE_TELEGRAM_TOKEN= ``` 3. Start or update Drive from the repository directory: ```bash docker compose up -d ui ``` 4. Send `/start` to your bot. It replies with your chat ID. Add that ID to `.env`: ```dotenv CASE_TELEGRAM_CHAT_ID= ``` 5. Run `docker compose up -d ui` again to apply the setting, then send a task such as `What is on the screen?`. Only the configured chat can drive the computer. The bot shows a typing indicator while it works, then sends the result or error. Long replies arrive in separate messages. Approval handoffs have **Approve** and **Deny** buttons; for a code handoff, reply to its prompt with the code. Pending handoffs are sent again when Drive reconnects after a restart. ### ntfy [ntfy](https://ntfy.sh) delivers messages through named topics. Anyone who knows an unprotected topic's name can read and write to it. Generate a random topic name and treat it like a password: ```bash openssl rand -hex 32 ``` 1. Install the [ntfy app](https://docs.ntfy.sh/subscribe/phone/) and subscribe to the generated topic. If you use your own ntfy server, point the app at it. 2. Add these settings to `.env`, replacing the topic placeholder: ```dotenv CASE_NTFY_CHAT=1 CASE_NTFY_URL=https://ntfy.sh CASE_NTFY_TOPIC= CASE_NTFY_TOKEN= ``` Change `CASE_NTFY_URL` if you use another server. For a protected topic, set `CASE_NTFY_TOKEN` to an access token that can publish and subscribe. See ntfy's [authentication instructions](https://docs.ntfy.sh/publish/#authentication). 3. Apply the settings to Drive and cased: ```bash docker compose up -d ``` 4. Send a task using one of these methods. Replace `` with your topic name, and use your server's URL if you host ntfy yourself: - Android: use the message bar in the topic view. Enable **Show message bar** in settings if it is hidden. - iOS: create a Shortcut with **Ask for Input**, then **Get Contents of URL**. Use `https://ntfy.sh/`, method POST, and the input as the request body. Add the Shortcut to your home screen or run it with Siri. - Terminal: run `curl -d "check my mail" "https://ntfy.sh/"`. For a protected topic, include an `Authorization: Bearer ` header when sending from a Shortcut or curl. Drive posts `Working`, then the result or error, to the same topic. It marks its own posts so it does not read them as new tasks. ### Replies and handoffs When one handoff is waiting, your next message answers it. If several are waiting, prefix the answer with its handoff ID, such as `h_ab12 483920`. An approval takes only `approve` or `deny`; any other reply is refused and the handoff keeps waiting. With no handoff waiting, a message steers the current Phone task or starts a new task on the first computer returned by cased. Create a computer before sending your first task. `approve`, `deny`, `done`, or a bare code with nothing waiting gets a "Nothing waiting" reply. Telegram skips ordinary messages more than ten minutes old when Drive reconnects and tells you which ones it skipped. ntfy does not replay messages sent while Drive was stopped. Send the task again if it was missed. ntfy handoff notifications can include an Assist link and signed approval buttons. These require `CASE_PUBLIC_HOST` and an HTTPS reverse proxy in front of cased. They are omitted without a public hostname. See [server hosting](#server-hosting) to enable them; phone chat itself does not require this setup.
Computer size and memory Choose a computer's size under **+ New computer**, then **SIZE**. The default is 2 GB of RAM and 1 CPU. Case keeps that choice when it recreates the container. | Setting | What it controls | Compose default | | --- | --- | --- | | `CASE_MAX_RUNNING` | Maximum number of awake computers | 4 (8 without Compose) | | `CASE_MAX_RAM_MB` | Total RAM that awake computers may reserve, in MB | 75% of the memory visible to cased | On macOS with Compose, that memory comes from the Docker VM. A 4 GB VM has room for one 2 GB computer within the default budget. To give Colima more memory when starting it: ```bash colima start --cpu 4 --memory 8 ``` If creating or waking a computer exceeds a limit, Case returns `409`. Sleep another computer or adjust the limits before retrying. Asleep computers use disk space only; Case does not limit their volume size.
Server hosting and remote access Keep ports 4174, 8787, and 8788 bound to loopback. For remote access, put an HTTPS reverse proxy in front of Case. Generate a token: ```bash openssl rand -hex 32 ``` Set `CASE_TOKEN` to that value in `.env`. Add your proxy hostname to `CASE_ALLOWED_HOSTS`, using commas for multiple hostnames. Run `docker compose up -d` to apply the settings. `CASE_TOKEN` protects Drive and the REST API. Open Drive through the HTTPS proxy with `?token=` on the first visit. MCP on port 8788 has no built-in client authentication: keep it local or configure authentication at its proxy. Setting `CASE_TOKEN` alone does not protect the MCP endpoint. MCP checks `Host` against the same list as cased and Drive, so a proxy in front of it needs its hostname in `CASE_ALLOWED_HOSTS` too. For ntfy Assist links and approval buttons, set `CASE_PUBLIC_HOST` to the public hostname of your cased proxy, without a scheme. That hostname is allowed without repeating it in `CASE_ALLOWED_HOSTS`. These links carry their own access tokens; treat them as secrets. Case does not configure DNS or HTTPS for you. Read the [self-hosting trust model](SECURITY.md#self-host-trust-model) before providing remote access.
Run the API and MCP without Drive ```bash docker compose up cased mcp --build -d ``` An MCP agent or the CLI can create and use computers in this mode. The MCP address remains `http://127.0.0.1:8788/mcp`.
Install without Compose This runs cased and Drive as host processes. Desktops still run in Docker. Install Python 3.12, Node 22, and Docker first. On macOS, `bin/case up` uses Colima; start it before building the image. Use either this setup or Compose at a time. Both use port 8787, but their vaults are separate: this setup uses `~/.case`, while Compose uses a Docker volume. Computers created through one setup do not appear in the other. With Docker running, install the dependencies and build the desktop image: ```bash python3 -m venv .venv .venv/bin/pip install -r requirements.txt npm --prefix web ci docker build -t case-desk:0.1 image ``` Start cased, then Drive: ```bash bin/case up CASE_URL=http://127.0.0.1:8787 node web/web-ui/serve.mjs ``` Open the [computers page](http://127.0.0.1:4174/deploy). Drive runs in the foreground in this terminal. Host processes use environment variables, not the Compose `.env` file; `bin/case` also reads `~/.case/env` if it exists. If cased runs directly on macOS, set `CASE_MAX_RAM_MB` explicitly; its automatic memory budget requires Linux's `/proc/meminfo`. A few settings only apply here. `CASE_PORT` moves cased off 8787. `CASE_VNC_PORT` pins every desktop's noVNC to one host port for a reverse proxy. `CASE_BRAIN_BIN` points the scheduler at a `claude` binary that is not on `PATH`, and `CASE_MCP_CONFIG` replaces `case-mcp.json`. On macOS, `CASE_CPU` and `CASE_MEM` size the Colima VM that `bin/case up` starts (default 4 CPUs, 4 GB). For a client that uses stdio MCP, [case-mcp.json](case-mcp.json) starts `mcp/case_mcp.py` with Python. It needs the installed Python dependencies and access to cased. The scheduler uses this file with Claude's `--mcp-config`.
Use a published desktop image If a desktop image has been published, you can pull it instead of building it: ```bash docker pull ghcr.io/case-computers/case-desk:latest ``` After a successful pull, set this value in `.env`: ```dotenv CASE_IMAGE=ghcr.io/case-computers/case-desk:latest ``` Then run `docker compose up -d`. If the image is unavailable, use the source build in the [quick start](#quick-start).
Storage and backups Each computer has a Docker volume mounted at `/home/agent`. It holds files, Chromium's profile, saved logins, and skills. It survives sleep, wake, and container recreation. Deleting a computer through Case deletes this volume too. Changes outside `/home/agent`, such as installed system packages, do not survive container recreation. cased stores the credential database and encryption key in `~/.case` for a host installation, or the `case-home` volume mounted at `/data` in Compose. Back up the database and key together, along with the desktop volumes you want to keep. Treat these backups as sensitive data. Drive keeps screenshots under `drive/shots` and attachments under `drive/inbox` in its home directory, `~/.case` by default. Its threads are in `web/web-ui/threads.json` by default, or the file `CASE_THREADS` names. Compose keeps all of them on the `ui-data` volume mounted at `/data`. Deleting a thread does not remove its screenshots or attachments. Files added through the plus menu stay on the Drive host for the model to read; they are not copied onto the desktop computer.
How Case works Drive (`web/web-ui/`) and MCP agents (`mcp/`) send requests to cased (`control-plane/`), which manages computers, the vault, and handoffs. The CLI (`bin/case`) uses the same REST API. Each desktop runs deskd inside the Docker image built from `image/`. Desktop tools cover navigation, numbered element snapshots, clicking and filling by reference, hovering, uploads, screenshots, commands, files, and network capture. Uploads select files already under `/home/agent`. Navigate and click responses include the first 2000 characters of page text. Compose puts desktops on `case-desks` with no published host ports. cased joins that network and the application network. It relays the live desktop view and adds the desktop's token. Desktops can reach their peers and cased, so service tokens remain necessary. See [SECURITY.md](SECURITY.md) for the trust model. `CASE_TURN_TOKENS` defaults to 2 million and caps cumulative input tokens for a Drive turn. Messages sent during a turn use `/api/chat/steer`. Client development details are in the [Drive README](web/web-ui/README.md).
## Troubleshooting - If Docker cannot connect to its daemon, start Docker and check `docker info`. - If a port is already in use, check whether another Case installation is running. Stop that installation before starting this one. - If Drive does not open, check `docker compose ps` and `docker compose logs cased ui` for startup errors. - If creating or waking a computer fails because of memory or capacity, check [computer size and memory](#computer-size-and-memory) and Docker's available RAM.
Move an existing desktop to the new network A desktop created before the network split keeps its original network until its container is recreated. Waking it alone does not change the network. Sleep the computer, remove its container with `docker rm case-`, then wake it again. Replace `` with the computer ID. Keep its home volume: that is where its files, browser profile, and saved logins live.
## Contributing and security See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and tests, and [SECURITY.md](SECURITY.md) for the trust model and vulnerability reporting. ## License AGPL-3.0 for `control-plane/` and `image/` (the box and API). MIT for `mcp/`, `bin/case`, and `web/` (clients). A **commercial license** is available if the AGPL does not fit your deployment. See [LICENSE.md](LICENSE.md). Contributions come in under a [CLA](CLA.md), signed once by adding your GitHub username to [`CLA-SIGNERS`](CLA-SIGNERS) in your first pull request. You keep your copyright.