holmd is the server that owns boxes. It serves the REST API under /v1, MCP over Streamable HTTP at /mcp, and the viewers. The CLI and the stdio MCP server are clients of it.
The Rust library does not need a server. A Computer drives its box directly.
Two binaries
| Binary | Role |
|---|---|
holm |
CLI, and MCP over stdio (holm mcp --stdio). It sends each command to a server. |
holmd |
The long-running server. |
Temporary and long-running servers
When holm finds no server, it starts one inside its own process. That server stops when the command exits. You do not need to start anything for a quick test.
A temporary server cannot keep state between commands. Start holmd when you need:
- Traces, for
traceandfork - Box expiry, which removes boxes after
--ttlor--idle - Remote vendors added with
holm runtime add - Access from other hosts
- MCP over HTTP
The boxes do not depend on the server. A box that a temporary server made continues to run after the command exits. A later holmd finds it and takes it back.
How a client finds a server
holm and holm mcp use the first of these:
--server URLHOLM_SERVER_URL- A
holmdonhttp://127.0.0.1:8080 - A temporary server in the same process
The client checks that the service on port 8080 is holmd. It does not use an unrelated process on that port.
--local uses no server. It drives the local runtime directly and supports only a small set of commands. See the CLI reference.
Start the server
holmd
curl http://127.0.0.1:8080/v1/healthThe server listens on 127.0.0.1:8080. Set HOLM_SERVER_ADDR to change it.
At start, the server:
- Finds the host runtimes and reads the runtimes file.
- Loads remote runtimes from the environment, the file, and the store.
- Takes back each box in its records, then scans each runtime for boxes that an earlier server labeled.
Access control
Anyone who can reach the API can create boxes, drive them, read their screens, and run commands in them. The server protects it with a bearer token.
| Address | Token |
|---|---|
Loopback, such as 127.0.0.1 |
Optional |
| Any other address | Necessary. The server does not start without one. |
HOLM_SERVER_ADDR=0.0.0.0:8080 \
HOLM_SERVER_TOKEN="$(openssl rand -hex 32)" \
holmdClients send Authorization: Bearer <token>. The CLI and holm mcp read the token from HOLM_SERVER_TOKEN.
- The token must be 16 characters or more.
- One token protects both
/v1and/mcp. /v1/healthneeds no token, so a load balancer can use it.- Viewer and CDP URLs have their own short-lived tokens. They do not contain the server token.
Workspaces
One server can serve many workspaces. A console (such as the holm console) signs a short-lived token for each call it makes, and issues API keys that SDKs, the CLI, and MCP clients send. Both name a workspace and a role. Link the server to the console:
HOLM_CONSOLE_URL=https://console.example.com \
HOLM_CONSOLE_SECRET="<the console's holmd secret>" \
holmdThe server reads the console's public keys from <HOLM_CONSOLE_URL>/api/holmd/keys.json, again every five minutes and when a token names a key it does not know, so the console can rotate its key. Every 30 seconds it reads the revoked API keys and deleted workspaces from /api/holmd/revoked, and every minute it reports which keys were used to /api/holmd/usage. Both send HOLM_CONSOLE_SECRET. HOLM_CONSOLE_PUBLIC_KEY pins one key instead, with no fetching; without HOLM_CONSOLE_URL there are then no API keys, only console tokens.
The roles:
| Role | Can |
|---|---|
viewer |
Read boxes, runtimes, and templates, and watch a screen. It cannot drive a screen. |
member |
Everything a viewer can, and launch, drive, fork, and remove boxes, and build images. |
admin |
Everything a member can, and add, change, and remove the workspace's runtimes. |
A box belongs to the workspace that launched it. Another workspace gets
404for it, as if it were not there.A runtime added by a workspace is that workspace's own. A runtime from the server's environment or runtimes file is shared by every workspace, and only the operator changes it.
HOLM_SERVER_TOKENis the operator. It sees and changes everything, as before.An
Idempotency-Keybelongs to one workspace.An API key is refused until the server has read the revoked list once. If the console cannot be reached later, the last list stays in force.
A revoked key stops working within 30 seconds.
A call token is a JWT signed with Ed25519 (alg: EdDSA, kid naming the key), with iss: "console", aud: "holmd", ws (the workspace), role, and exp. An API key is holm_sk_ followed by a JWT with kind: "key", sub (the key id), ws, and role; its exp is optional.
Behind a reverse proxy
Put TLS in a reverse proxy in front of holmd. Forward /v1, /mcp, and WebSocket upgrades. The viewers use WebSockets.
Set HOLM_PUBLIC_URL to the public origin, such as https://boxes.example.com, when the proxy hides it. The MCP Apps live screen uses it to connect back to the server.
Storage
The server stores box records, traces, frames, and remote runtimes. The default store is in memory, so all of this is lost when the server stops. The boxes themselves continue to run.
HOLM_STORAGE_BACKEND |
Settings | Build feature |
|---|---|---|
memory (default) |
None | None |
local |
HOLM_STATE_DIR |
None |
sqlite |
HOLM_STATE_URL |
sqlite |
postgres |
HOLM_STATE_URL |
postgres |
s3 |
HOLM_S3_ENDPOINT, HOLM_S3_BUCKET, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY; optional HOLM_S3_REGION, HOLM_S3_PREFIX |
s3 |
Release builds include the sqlite, postgres, and s3 features.
HOLM_STORAGE_BACKEND=local \
HOLM_STATE_DIR=/var/lib/holm \
holmdWith the memory store:
- After a restart, the server takes the boxes back, but their traces start again, marked
adopted. A fork can repeat only the actions after the restart. - Remote runtimes added with
holm runtime addare lost.
Retention
| Variable | Default | Effect |
|---|---|---|
HOLM_KEEP_FRAMES_SECS |
2 hours | How long to keep frames |
HOLM_KEEP_ENTRIES_SECS |
7 days | How long to keep trace entries |
HOLM_PRUNE_SECS |
1 hour | How often to remove old data |
Idempotency
Box creation, action batches, and forks accept an idempotency-key header. If a client sends the same request again with the same key, for example after a network error, the server returns the first result. It does not click two times or make a second box. The server keeps these keys in memory, so a restart forgets them.
Errors
All API errors have the same shape:
{ "code": "…", "message": "…", "retryable": false }retryable tells a client if the same request can succeed later.
Environment variables
| Variable | Default | Effect |
|---|---|---|
HOLM_SERVER_ADDR |
127.0.0.1:8080 |
The listen address |
HOLM_SERVER_TOKEN |
None | The bearer token. Necessary on a non-loopback address, unless a console key is set. |
HOLM_CONSOLE_URL |
None | The console's base URL. The server reads its keys and revocations from it. See Workspaces. |
HOLM_CONSOLE_SECRET |
None | The secret the server sends to the console |
HOLM_CONSOLE_PUBLIC_KEY |
None | One console public key to trust, in base64 or PEM, instead of reading them from the console |
HOLM_PUBLIC_URL |
None | The public origin behind a reverse proxy |
HOLM_SERVER_CONFIG |
None | The path to the runtimes file |
HOLM_SERVER_RUNTIMES |
All that answer | The host runtimes to offer, separated by commas |
HOLM_SERVER_SANDBOXES |
None | Remote vendors to add from the environment, such as e2b, vercel, daytona, or modal |
HOLM_SERVER_SECRET_KEY |
None | 32 bytes, base64 or hex, that encrypt stored vendor keys. With no key, holm runtime add is refused. |
HOLM_SERVER_SECRET_FILE |
None | A file that holds the secret key. If the file does not exist, the server makes it with mode 0600. |
HOLM_SERVER_REAP_SECS |
30 | How often to remove expired boxes |
HOLM_SERVER_JOBS |
inline |
inline launches, forks and builds in the request. queue answers 202 and does the work from a queue in the store. |
HOLM_SERVER_SCHEDULE |
internal |
internal runs the periodic work in the server. external waits for a scheduler to call the /v1/jobs routes. |
CRON_SECRET |
None | A bearer token that opens the /v1/jobs routes and nothing else. |
HOLM_CONTENT_BOUNDARIES |
Off | 1 puts page text between nonce markers in MCP results |
Storage variables are in Storage.