OpenClaw
Run the OpenClaw gateway as a hosted service in its own box — one box per tenant, its own identity, its config under version control
OpenClaw is an always-on AI gateway: one long-running daemon with a Control UI, health endpoints, and live channel connections. It is not a TUI you attach to, so AgentBox runs it as a service agent — a daemon the box's supervisor owns, published on the box's web URL.
agentbox openclaw # create-or-resume a box, wait for the gateway, print its URL + token$ agentbox openclaw -n claw
┌ agentbox openclaw
│
◇ box ready: claw
│
◇ openclaw ready
│
│ token: 5f3c…c1d2
│
└ openclaw on claw: http://127.0.0.1:54321Open that URL and paste the token when the Control UI asks for it.
AN OPENCLAW BOX HAS NO FRIENDLY URL
Every other box gets a stable https://<box>.localhost from
Portless. OpenClaw's box does not, and cannot: Portless adds
X-Forwarded-* headers to every request, and the gateway answers any request carrying one
with 403 proxy_attribution_required. gateway.trustedProxies does not help — configuring the
proxy is necessary but not sufficient, because OpenClaw also requires the forwarded client to be
non-loopback, and a browser on the same machine as the proxy never is. That is deliberate: it
stops a remote client claiming the loopback auth exemption by going through a proxy.
So AgentBox publishes the port directly instead. The consequences are worth knowing: the URL
carries a host port that changes every time the box restarts, and it is not the same URL from
inside the box (in there the gateway is http://localhost:18789). Re-read it with
agentbox openclaw url rather than bookmarking it. The VNC desktop keeps its own
vnc-<box>.localhost name.
ONE BOX PER TENANT
OpenClaw does not support multiple tenants in one gateway: two gateways sharing a state directory share a gateway identity and its channel pairings. AgentBox enforces that — a service agent always gets a per-box config volume, and there is no config key to turn that off.
The commands
agentbox openclaw has the shape a hosted service has, not the shape an agent TUI has — there is no login and no --resume. It does have an attach, but it means something different: it opens OpenClaw's own terminal client against the gateway the box is already running, rather than attaching to an agent session (there isn't one).
| Command | What it does |
|---|---|
agentbox openclaw [box] | Create the box if it is missing, start it if it is down, wait for the gateway, print the URL and token |
agentbox openclaw url [box] | Print the URL, the gateway token, and a link that opens the Control UI signed in |
agentbox openclaw attach [box] | Open OpenClaw's terminal UI against the running gateway (Control+a d detaches, gateway keeps running) |
agentbox openclaw status [box] | The supervisor's view of the openclaw unit |
agentbox openclaw logs [box] | Tail the gateway's log (-f to follow) |
agentbox openclaw restart [box] | Restart the gateway |
agentbox openclaw stop [box] | Stop the gateway; the box keeps running |
The create form takes the box flags you already know — -w, -n, -p/--provider, --image, --snapshot, -y, --carry-yes, --verbose — plus --timeout <seconds> for how long to wait for the gateway to report ready (default 180), and --model-auth <source...> to seed model-provider logins (see Model provider).
ALWAYS-ON BY DEFAULT
OpenClaw's box is a persistent box: never
auto-paused, never idle-lapsed, skipped by agentbox prune, and started again after a host
reboot. A gateway an autopause can reap is an outage, so that is the default for any service agent
rather than a flag you have to remember — --no-persistent opts out. It also means --provider e2b and --provider vercel are refused: their platform session cap makes an always-on box
impossible, so the box would lapse on its own.
agentbox openclaw url prints three lines: the URL, the token, and an open: link with the token in the URL fragment (<url>/#token=…) — which is where the Control UI reads it, so that link opens the dashboard already signed in. The bare URL stays on the first line, so agentbox openclaw url | head -1 still yields just the URL. The token comes from OpenClaw's own openclaw dashboard --json --no-open, run in the box on demand — the interface it offers for exactly this, so AgentBox never has to know where the gateway stores its secrets. (config get gateway.auth.token deliberately answers __OPENCLAW_REDACTED__, and reading openclaw.json directly would depend on a private layout.) Only the fragment of the URL it prints is kept: the rest is the gateway's own loopback address inside the box.
The token rides the fragment rather than a query string because a browser never sends a fragment to the server — it stays out of access logs and Referer headers.
The web hub and the macOS tray get the same link without the CLI: Open Web on a bot calls GET /api/v1/boxes/{id}/web, which answers { url, signInUrl, signInPending }. It is resolved when you click rather than carried on the box list, because reading the token is an exec into the box — and it re-resolves the URL at the same time, which matters on a box reached over an SSH forward, where the listed port can differ from the live one. That read is why the click is not instant (~5s on a remote bot), so both surfaces show a progress card while it runs. A bot whose gateway has not come up yet answers with signInPending: true and no token: the tray waits for it rather than opening a page that would show the sign-in prompt, or nothing at all.
What happens on create
Nothing here is OpenClaw-specific code. The units below are declared on OpenClaw's catalog row and folded into the box's supervisor through the same reload diff an edit to your agentbox.yaml uses — which is why a box booted from a snapshot baked before OpenClaw existed still runs it.
- Install, on demand.
npm i -g openclawruns into the live box the first time. It is not baked into any base image: OpenClaw is ~893 MB installed, a ~29% increase on the base image, and baking it would stale every provider's snapshot. openclaw-onboard— a one-shot unit runningopenclaw onboard --non-interactive --accept-risk --mode local --skip-channels --skip-health --no-install-daemon. It writes the factory config, sets the agent workspace to/workspace, and generates this box's gateway identity and auth token. It is marked run-once, so a restart never replaces that identity.openclaw-model-auth— imports a borrowed model login, if the create seeded one (see Model provider). A no-op otherwise.openclaw-render—agentbox-ctl agent render openclaw, which applies youropenclaw:overlay (below).openclaw—openclaw gateway,restart: always, ready whenhttp://127.0.0.1:18789/healthzanswers, published on the box's web port.
The gateway binds loopback only. AgentBox forwards the box's :80 to 127.0.0.1:18789 inside the same container, so the Control UI is reachable without ever widening the bind — which is both sufficient and safer than exposing the daemon on 0.0.0.0.
Model provider
A fresh box has a gateway identity and no model provider: onboarding runs with auth skipped, so the first turn fails until OpenClaw is given a login. Two ways to give it one.
Borrow your Codex login. agentbox openclaw --model-auth codex seeds the box with the Codex (ChatGPT subscription) OAuth login your host already holds, and OpenClaw runs on it — the same login agentbox codex boxes use. OpenAI explicitly permits that OAuth in external tools, and its shape maps one-to-one onto OpenClaw's OpenAI profile.
agentbox openclaw --model-auth codex # this create
agentbox config set openclaw.modelAuth codex # every create; `none` to stop askingWith neither set, an interactive create asks — defaulting to No — whenever your host holds a Codex login. -y and a non-TTY never ask and seed nothing.
Creating an OpenClaw box from the hub web UI or the macOS tray asks the same question, as a card showing which login it is and where it would land (~/.codex/auth.json on the host → ~/.codex/auth.json in the box). Declining is the default there too, and a client that does not ask simply gets none — this question, unlike carry:, has a safe silent answer.
What happens is ordinary AgentBox machinery plus one unit on OpenClaw's row:
- The host copies the login to codex's own credential path in the box (
~/.codex/auth.json, 0600), the same place a runtime install of codex would put it, at the same step yourcarry:files land — before the first supervisor task runs. Nothing about OpenClaw's auth format lives on the host. openclaw-model-auththen installs the official@openclaw/codexplugin (the harness OpenClaw's default model already runs on; ~15s, once per box) and runsopenclaw migrate apply codex --item auth:openai, OpenClaw's own import of a Codex home. It records a hash of the seeded file and skips the import on later boots unless the file changes. Every boot it also checks that OpenClaw trusts the plugin install, and reinstalls the plugin if not: an untrusted plugin loads but can't serve a turn, so the Control UI drops back to model setup.- From here OpenClaw owns the profile and refreshes it in its own store. OpenAI does not invalidate the previous refresh token on rotation — measured with two boxes seeded from one host file, both refreshing independently while the host kept working — so the box is an independent session and the seed is never read again. A later
codex loginon the host reaches the box through the normal credential fan-out and re-imports only if the file changed.
Borrowing is one-way: the box is a consumer of that login, never a source. It is not extracted back, not reconciled on resume, and a clone borrows the same login afresh from the host rather than copying the source box's session.
An API key. ANTHROPIC_API_KEY or OPENAI_API_KEY in the per-box env file OpenClaw loads (~/.agentbox/openclaw/<box>.env on the host → ~/.openclaw/.env in the box, see Channels) is read by the gateway with no other configuration; set the model in the overlay below. Claude's own subscription login is deliberately not borrowable: OpenClaw's refresh would rotate that token and log out the host and every claude box.
Configuring it: the openclaw: overlay
Put gateway config in a top-level openclaw: block in your project's agentbox.yaml. It is an overlay, not the file: agentbox-ctl agent render openclaw sends it to OpenClaw's own openclaw config patch --stdin, which performs the recursive merge and validates as it goes.
# yaml-language-server: $schema=https://agent-box.sh/schema/agentbox.schema.json
openclaw:
logging:
level: debug
channels:
telegram:
enabled: true
tokenEnv: TELEGRAM_BOT_TOKENTo apply an edit to a live box, re-run the render inside it:
agentbox shell claw -- agentbox-ctl run-task openclaw-render --forceagentbox-ctl reload alone is not enough: it applies the unit diff, and the render task's definition has not changed. Restarting the box re-runs it too.
DOCKER BOXES READ THEIR OWN WORKSPACE
On docker, /workspace is an in-container git worktree, not a bind mount of your host directory —
so an uncommitted edit to agentbox.yaml on the host is not what the render reads. Edit it in the
box (agentbox shell, or your agent), or commit and re-create.
Three properties are worth knowing:
-
Only what you changed is sent. The render diffs your block against the overlay it applied last time and patches only the keys whose value changed. A key you edit by hand inside the box therefore survives every later render — and comes back under AgentBox's control the moment you touch it in
agentbox.yaml. -
Removing a key restores OpenClaw's default, rather than freezing whatever AgentBox wrote last.
-
A bad overlay writes nothing. The render dry-runs the patch first (
--dry-run) and gates onopenclaw config validateafterwards; either failing fails the unit loudly with OpenClaw's own message, e.g.:agentbox-ctl: openclaw config patch --stdin --dry-run rejected the overlay (exit 1): OpenClaw config is invalid Problem: - openclaw.json:58 — logging: Unrecognized key: "format"
NEVER PUT A SECRET IN THE OVERLAY
agentbox.yaml is committed. Carry the real value into the box with a
carry: entry — it lands 0600 — and have the overlay reference it by name.
The render warns when a value under the overlay key looks like a literal token.
Do not set gateway.bind or gateway.auth from the overlay. Onboard already binds loopback and generates a token, and overriding either only widens exposure or fights OpenClaw for ownership of its own identity.
The JSON schema for agentbox.yaml does not know about the openclaw: key — which keys are overlay keys lives in the host's agent catalog, not in the schema — so a YAML-LSP editor flags it as unknown. The supervisor accepts it.
Channels
Token-based channels (Telegram, Discord) are fully scriptable and are the documented path: carry the token in, reference it from the overlay, and openclaw channels add --use-env inside the box. WhatsApp pairing is QR-only and interactive; it works through the Control UI in a browser, and a box with the VNC desktop enabled is a workable way to drive it.
Overriding a unit
The unit names (openclaw, openclaw-onboard, openclaw-render) are public surface. Declaring a service or task of the same name in your agentbox.yaml replaces the one the agent ships — that is the supported way to change the gateway's command, env or readiness probe. See services and tasks.
Only one unit may expose:. If your agentbox.yaml already publishes the box's web port, OpenClaw's expose: is dropped with a warning rather than silently taking it over.
What AgentBox adds to the box
Two things, both AgentBox's rather than yours, refreshed on every boot:
- A skill at
/opt/agentbox/skills/— outside your workspace, registered through OpenClaw'sskills.load.extraDirsat its lowest precedence, so a skill of your own with the same name wins. It never travels withagentbox clone. - The box facts at
/workspace/.agentbox/AGENTS.md— the same sandbox description Claude and Codex get, so the gateway's agent knows it is in a sandbox, that/workspaceis box-local, and how to reach your machine. It has to live inside the workspace because OpenClaw reads a system prompt from nowhere else, butagentbox downloadandagentbox cloneboth skip.agentbox/— it will not follow a pull into your repo, and your ownAGENTS.mdis never touched.
Both are regenerated each time the supervisor starts, which is what keeps a cloned box from inheriting the facts of the box it was cloned from.
State, sync and clones
| Path in the box | What it is | Travels? |
|---|---|---|
~/.openclaw/openclaw.json | Config and gateway identity | --backup / --restore only |
~/.openclaw/state/*.sqlite* | Live gateway state | --backup / --restore only |
~/.openclaw/tmp/openclaw-<uid>/ | Lock databases | Never |
~/.openclaw/agents/ | Your agent definitions | Both directions |
~/.config/openclaw | OpenClaw's XDG config dir | Stays in the box's config volume |
~/.openclaw/.env | Channel tokens | Carried per box, never copied |
~/.codex/auth.json | Borrowed Codex login (seed) | Seeded from the host, never read back |
agentbox download openclaw [box] pulls the agent definitions back to your host, additively — an item the host already has is never overwritten. There is deliberately no --propagate: an OpenClaw box is a tenant, and copying one tenant's definitions into another's gateway is not a sensible default.
One exception, and it is opt-in: agentbox download --backup. A backup is the one operation that keeps the identity, because a restore that mints a new gateway token has not restored the bot. It writes openclaw.json, the config-journal key and the live state/ into <project>/.agentbox/bots/<bot>/<timestamp>/state/ — gitignored and 0700 — while still dropping tmp/openclaw-<uid>/, whose name carries the box user's uid and differs per provider. The gateway databases go through SQLite's online-backup API, not a byte copy. See back a bot up.
Neither is terminal-only: the hub's box page has a Bot card with Back up now, and the menu-bar app has Back Up Now in the box submenu. A backup is restorable, and agentbox openclaw --restore <bot> is how: a new box, seeded from the backed-up workspace, with the captured state directory pushed back in once the gateway has come up on its own — same token, same pairings, same history, on whichever provider you point it at. It refuses while the backed-up box is still running, because two gateways holding one identity is the failure the per-box state directory exists to prevent. From a GUI, restore is a Start from row in the hub's and the tray's Create box form — a bundle outlives the box it came from, so there is usually no box left to click. See bring a bot back.
Spawning a second bot
agentbox clone <bot> -n <name> makes another bot from the same workspace. It runs OpenClaw, with its own gateway identity, and three things are handled for it:
AGENTS.mdandUSER.mdare not copied — onboarding regenerates them for the new box.SOUL.mdandIDENTITY.mdare copied, then rewritten through the workspace'sidentityrule-set so the copy stops introducing itself with the source bot's name.~/.agentbox/openclaw/<name>.envis carried in as a 0600~/.openclaw/.env. The clone refuses until that file exists, because two bots sharing one channel token means the new one answers as the old one. On a firstagentbox openclaw -n <name>the same file is optional and merely logged.
A bot writes its own identity rule-set: while agentbox.yaml declares none, the box facts nudge the bot to follow its agentbox-identity skill once, and the nudge disappears as soon as the rules are there. Review the rules it proposes like any other edit to your yaml — a name that is also a common word is the case worth checking.
If your host happens to run OpenClaw itself, its config and state are excluded from everything pushed into a box, for the same identity reason.
Providers
OpenClaw needs a box that is genuinely always on. Docker, Hetzner, DigitalOcean and remote-docker are the intended targets. E2B and Vercel microVMs carry hard session caps and are refused — not warned about — because a gateway that lapses on its own is not a gateway.
Nothing provider-specific had to be written for it: the install recipe, the units and the overlay all ride the agent catalog, so agentbox openclaw --provider hetzner is the same command.
Troubleshooting
service "openclaw" did not report ready—agentbox openclaw logs clawfirst. On a cold box the first run also installs ~893 MB from npm, so raise--timeouton a slow link.- The unit never appears — it arrives with the agent catalog over the host relay.
agentbox-ctl reloadinside the box re-applies it; checkagentbox relay statuson the host. config validatefailed — the render prints OpenClaw's own message and leaves the config untouched. Fix theopenclaw:block and reload.- The Control UI asks for a token you do not have —
agentbox openclaw urlprints it, and itsopen:line is a link that skips the prompt entirely.