agentbox.yaml

Reference for the in-box services and tasks configuration file

agentbox.yaml lives at your workspace root and is read inside the box by the supervisor (@agentbox/ctl, shipped as agentbox-ctl). It declares tasks (one-shot setup units) and services (long-running processes) run under a DAG scheduler, plus a few host-side blocks (carry, tools, defaults, ide).

The file is validated twice: the host CLI pre-validates it before any box work on agentbox create (a config error aborts the run), and the in-box daemon re-validates on start. Editor autocomplete and validation come free from the JSON Schema. For the conceptual guide, see services and tasks.

TIP

Point the file at the published schema on the first line so editors (e.g. the Red Hat YAML extension) give you completion and validation:

# yaml-language-server: $schema=https://agent-box.sh/schema/agentbox.schema.json

In-repo examples use a local relative path instead; published projects use the URL above.

Top-level keys

All top-level keys are optional, and unknown keys are rejected (additionalProperties: false). A missing or empty agentbox.yaml is completely fine — create does not fail without one.

KeyRead byPurpose
servicessupervisorLong-running processes (map of name → spec).
taskssupervisorOne-shot units that run to completion before dependents.
replacementssupervisor + hostNamed reusable text-substitution rule-sets — see replacements.
idehost (agentbox code)VS Code attach customizations; the supervisor ignores it.
defaultshost (@agentbox/config)Project-level AgentBox config defaults.

carry and tools are also declared at the top level but are host-applied and parsed separately — see carry and tools below. Service and task names must match [A-Za-z0-9_-]+.

A minimal valid file can be just one task — no web app or port required:

# yaml-language-server: $schema=https://agent-box.sh/schema/agentbox.schema.json
tasks:
  install:
    command: pnpm install

Services

A service is a long-running process. It needs either command or image (not both); unknown fields are rejected.

FieldDefaultMeaning
command(required, or image)Shell string (run via bash -c) or an argv array.
image(required, or command)Run a docker container instead — see docker image services.
cwd/workspaceWorking directory; relative paths resolve against /workspace.
env—Extra env vars for a command service (scalars, coerced to strings). For an image service, put container env under image.env.
autostarttrueStart automatically when the daemon boots.
restarton-failureRestart policy — see needs and restart.
backoff—Exponential backoff between restarts.
needs—DAG dependencies.
ready_when—Readiness probe — see ready_when.
expose—Mark the one web service — see expose.
ide—Per-service VS Code hints (host-side only).

A service moves through pending → waiting → starting → running → ready, and can land in unhealthy, crashed, backoff, or stopped. Logs land at /var/log/agentbox/<svc>.log inside the box.

Here is the web service from examples/express-ready:

services:
  web:
    command: 'set -a; [ -f .env ] && . ./.env; set +a; node server.js'
    needs: [install]
    env:
      PORT: '3000'
      GREETING: 'hello from agentbox'
    expose:
      port: 3000
      as: 80
    ready_when:
      port: 3000
      timeout_ms: 60000
    restart: on-failure

Manage a service from inside the box:

$ agentbox-ctl restart web
$ agentbox-ctl stop web
$ agentbox-ctl start web

stop does not exit the daemon; start restarts a previously-stopped service.

TIP

Use the array form of command to avoid shell quoting; use the string form (bash -c) when you need shell features like pipes, &&, or sourcing env files.

Docker image services

For a containerized dependency (a database, cache, …) set image: instead of command: and AgentBox generates the docker start-or-run shell for you — no hand-written docker run … || docker start … block. It runs in the box's own dockerd, so a published port like 5437:5432 is reachable from other in-box services at 127.0.0.1:5437. image: is either a bare ref string (image: redis:7) or a mapping with the container config nested under it:

services:
  postgres:
    image:
      name: postgres:17-alpine
      ports: ['5437:5432'] # "<host>:<container>" (or "<port>")
      env: # the container's -e env
        POSTGRES_USER: optima
        POSTGRES_PASSWORD: changeme
        POSTGRES_DB: optima
      args: '-c max_connections=200' # string OR ["-c", "max_connections=200"]; shell-tokenized
      container_name: optima_db # optional; default = service name
    ready_when:
      port: 5437
    restart: always

All the usual service fields (ready_when, restart, backoff, needs, expose, autostart) still apply at the service level — only the container config (name/ports/env/args/container_name) nests under image:. The container is reused by name across box stop/start (its data lives in the per-box docker volume, which a checkpoint does not capture — see the database note in services and tasks).

HEADS UP

A change to image/ports/env reuses the existing container as-is — AgentBox never auto-docker rms it (that would wipe its data). To apply the change, docker rm <container_name> inside the box, then agentbox-ctl reload.

Tasks

A task is a one-shot unit that runs to completion. It accepts only five fields: command, cwd, env, needs, and run_once. Tasks cannot have restart, autostart, backoff, or ready_when — the schema rejects them. That is the key distinction from services.

A task moves through pending → waiting → running → done, and can land in failed or skipped. Tasks run before dependent services via needs:. Typical use: install deps, build, seed a database.

Tasks re-run on every supervisor restart (which happens on box start, not just create). So a task must be idempotent. The run_once field makes the supervisor skip an already-satisfied task for you — no more hand-rolled marker checks:

tasks:
  install:
    command: pnpm install --frozen-lockfile
    run_once: true # skip while the command is unchanged

  build:
    command: pnpm build
    needs: [install]

run_once takes two forms:

FormBehavior
run_once: trueThe supervisor stores a marker keyed by a hash of the resolved command. A warm boot skips while the hash matches; editing the command invalidates it and re-runs. The marker lives at /var/lib/agentbox/tasks/<name> (box rootfs — captured by checkpoints, never under /workspace).
run_once: { check: <cmd> }Run the probe before launching; exit 0 means already satisfied (skip). No marker is written — the probe is the source of truth. Use this when the thing you'd guard on lives outside the checkpointed filesystem (e.g. a containerized database, whose data is in the in-box docker volume, not the checkpoint). The probe runs verbatim via bash -c with the box env, so use shell vars like $AGENTBOX_BOX_NAME; it does not expand {{…}} placeholders (those are render-only).
tasks:
  seed:
    command: pnpm db:seed
    needs: [migrate]
    # Probe the DB itself — a file marker would be restored from the checkpoint
    # while the containerized DB starts empty, wrongly skipping the seed.
    run_once:
      check: "psql -tAc \"select 1 from \\\"user\\\" limit 1\" | grep -q 1"

Re-run a task in-box:

$ agentbox-ctl run-task install
$ agentbox-ctl run-task install --force

run-task resets the task to pending so the scheduler reruns it; it is a no-op on an already-done task unless you pass --force. --force also bypasses the run_once skip (marker or check) and, for the marker form, rewrites the marker.

HEADS UP

Tasks re-run on every daemon start, not just at create. A non-idempotent task (an unguarded git init, a destructive migration) will fire repeatedly — declare run_once: (or guard it yourself). Prefer the {check} form for state that a checkpoint does not capture (containerized DB data), where a filesystem marker would desync.

ready_when

A per-service readiness probe. Exactly one of port, log_match, or http must be present.

FieldDefaultMeaning
port—TCP connect probe to host:<port>.
host127.0.0.1Host for the port probe; ignored otherwise.
log_match—Regex matched against the service's output; first match flips to ready.
http—HTTP(S) URL; sends GET, waits for expect_status (default: any 2xx).
expect_status—Specific status to wait for; only valid with http.
interval_ms500Poll interval (ignored for log_match).
initial_delay_ms0Delay before the first probe.
timeout_ms60000Total time before timing out.
on_timeoutkillkill re-enters the restart policy; mark_unhealthy leaves the process running but flagged.
services:
  api:
    command: 'node server.js'
    ready_when:
      http: http://127.0.0.1:3000/health
      expect_status: 200
      timeout_ms: 60000

The port and log_match variants are one-liners: port: 3000 or log_match: 'listening on'. Use on_timeout: mark_unhealthy as the escape hatch for legitimately slow cold starts.

TIP

Downstream units gated with needs: [api] start only once the probe reports ready, not merely when the process spawns.

expose

expose marks a service as the web service. At most one service may set it.

expose:
  port: 3000
  as: 80

port (1–65535) is the port the service listens on inside the box. as is the container port AgentBox forwards to it. The supervisor runs an in-process TCP forwarder binding container :80 → 127.0.0.1:<port> (the box's node binary has cap_net_bind_service, so binding :80 works as non-root).

Because expose is wired by the supervisor, not the container runtime, adding or changing it and running agentbox-ctl reload activates the web service with no box restart. The setup wizard relies on this when it writes agentbox.yaml after create.

HEADS UP

as must be 80 — it is the single container port AgentBox reserves for the published web URL. Any other value is rejected.

See web apps and tunnels for how the exposed port becomes a published URL on the host. VNC and screen sharing are separate — see browser and screen.

needs and restart

needs is an array of task/service names that must reach their terminal-good state before this unit starts, forming a DAG. Independent units launch in parallel. Cycles and unknown references are rejected at config load.

restart (services only) controls relaunch:

ValueBehavior
alwaysRestart regardless of exit code.
on-failure(default) Restart only on a non-zero exit.
neverLeave it dead after it exits.

backoff (services only) sets exponential backoff between restarts: initial_ms (default 500), max_ms (default 30000), factor (default 2). The runtime enforces max_ms >= initial_ms — a cross-field rule the JSON Schema cannot express.

The examples/test-workspace file contrasts all three restart modes:

services:
  ticker:
    command: 'i=0; while true; do echo "tick $i"; i=$((i+1)); sleep 1; done'
    restart: always

  flaky:
    command: 'echo starting; sleep 2; echo crashing; exit 1'
    restart: on-failure
    backoff:
      initial_ms: 200
      max_ms: 2000
      factor: 2

  one-shot:
    command: 'echo "hello from one-shot"; sleep 1'
    restart: never

After editing the DAG, apply the diff without restarting the box:

$ agentbox-ctl reload
added: (none)
removed: (none)
changed: web

HEADS UP

A needs cycle or a reference to a non-existent unit fails validation — the daemon will not start, and on the host agentbox create aborts before doing any docker work.

See background and parallel for running multiple boxes, and services and tasks for the DAG model in prose.

defaults

defaults is a host-side block: project-level AgentBox config defaults, with the same shape as ~/.agentbox/config.yaml. The in-box supervisor ignores it entirely; @agentbox/config validates it strictly when the host loads it.

It sits in AgentBox's layered precedence — CLI flag > workspace > project (this block) > global ~/.agentbox/config.yaml > built-in default — so it is the place to pin per-project box-creation defaults.

defaults:
  box:
    provider: docker

The full key set lives at https://agent-box.sh/schema/user-config.schema.json.

TIP

Use defaults to make a repo "remember" its provider and checkpoint so teammates get the same box without passing flags.

See configuration for the full key set and precedence. Note the cross-provider gotcha: pinning provider-native snapshot ids in shared keys can collide — the configuration page covers the per-provider box.defaultCheckpointDocker / ...Daytona / ...Hetzner / ...Vercel overrides.

carry

carry is declared at the top level but is host-applied, not parsed by the supervisor (it has its own parser). It is a declarative host→box file copy that bypasses .gitignore. Each entry maps a host src to an in-box dest:

FieldRule
srcMust start with /, ~/, or ./. ~/ = host home; ./ = project root.
destMust start with / or ~/. ~/ expands to /home/vscode.
modeOctal — accepts 0o600, "0600", or "600".
userNumeric uid that owns the file in-box. Leave unset (the default) and AgentBox resolves the box's vscode user itself — its uid is 1000 on some providers and assigned at bake time on vercel/e2b, so don't write 1000 meaning "the box user". 0 keeps it root-owned.
excludeList of tar globs / bare dir names to drop when copying a directory (additive on top of the defaults below).
optionaltrue skips a missing src silently instead of erroring.
replaceEnvstrue substitutes {{AGENTBOX_*}} placeholders in the file content host-side before copying (file entries only). See replacements.
replaceInline replacement rules applied (in order) before copying (file only).
rulesNames of top-level replacements: rule-sets to apply (file only).

A shorthand string form "src=dest" (or just "src" to mirror) is also accepted.

carry:
  - src: ~/.agentbox/carry-smoke/marker.txt
    dest: ~/carried-marker.txt
    mode: 0o600
    optional: true
  - src: ../legacy-app
    dest: ~/legacy-app
    exclude:
      - '*/cache'

The canonical use case is developing AgentBox inside an AgentBox: carry ~/.agentbox/secrets.env and ~/.agentbox/claude-credentials.json so the in-box CLI is already authenticated.

When copying a directory, heavy regenerable dirs (.git, node_modules, bin, obj, packages, dist, .next, target) are dropped by default; exclude: adds to that set. The host resolver blocks .. traversal, denies /proc|/sys|/dev|/etc/passwd|/etc/shadow, caps each entry at box.cpMaxBytes (default 100 MiB) after excludes — the same limit agentbox cp uses — and flags symlinks that escape $HOME and the project root. On create, claude, codex, and opencode the host prompts once — listing each src→dest with size, mode, and symlink warnings — before copying.

The approval is remembered per project. Say yes once and later boxes for that project carry the same files without asking again; the grant is stored host-side in ~/.agentbox/projects/<hash>/carry.yaml (never in the repo — a committed carry: block only requests the copy, exactly as a tools: block does). It is keyed on the file list: change a src/dest, add or remove an entry, change a mode, an exclude: or a replacement rule — or have an entry start resolving through a symlink out of $HOME — and the next create asks again. A carried file whose contents change does not re-ask: you approved that path, and the file it points at is expected to change.

Two things to know about what a grant covers. It applies to any provider, so a list approved while creating a local Docker box is also carried, unasked, to a cloud box on Hetzner, E2B or Vercel — approve with that in mind, since those files leave your machine. And because the approval already came from a human, a non-interactive create (CI, a non-TTY) carries a granted list rather than failing for an explicit opt-in; the fail-loud rule below applies to a list this project has not approved.

agentbox <agent> --carry ask shows the table again. Answering Skip or Cancel there withdraws the standing approval, so the next create asks — that is how you revoke one (deleting carry.yaml works too).

# auto-approve every carry entry for this box
agentbox claude --carry-yes

# skip the carry block entirely
agentbox claude --carry skip

# re-open an approval this project already gave
agentbox claude --carry ask

You can also set AGENTBOX_CARRY_YES=1 or AGENTBOX_CARRY=skip; an explicit --carry ask beats both, since it is a request to review the list. Note that -y/--yes does not auto-approve carry — a non-TTY -y with an unapproved, non-empty list fails loud and asks for the explicit env var. agentbox fork is the exception: it sends carry by default (opt out with agentbox fork --carry skip).

Approving carry outside the terminal

The same approval happens when you create a box from the hub web UI or the macOS tray: before anything is provisioned they ask the hub what the create needs approved (POST /api/v1/projects/{id}/create-preflight), and show the file table — each src→dest with size, mode, and symlink warnings — as a question you answer in the UI. The answers ride back with the create as opts.promptAnswers.

A remembered approval works there too — the preflight runs the same gate, so once a project's list is approved (from any surface) the web form and the tray card stop showing the question and go straight to the build. Both endpoints accept carryAsk: true to show the table again for a granted list, which is how a client offers a review-and-revoke.

A client that skips that step does not silently drop your files: carry has no safe default answer, so the create is refused with the same message the terminal gives you. A client that already has an answer sends it as a decision instead of an answer list — opts.carryYes to approve, opts.carrySkip to decline — which is how a scripted agentbox create --carry-yes or --carry skip gets through without a terminal. Answers are content-addressed to the question, so one collected against a carry: block that has since changed is refused rather than applied to the new one.

One case genuinely cannot ask: a remote control box has no checkout of your project and cannot read the files a carry: block names. It reports that (unavailable) and the UI tells you, instead of building a box quietly missing them. Create from the CLI on the machine that holds the files, which uploads the approved material with the create.

HEADS UP

carry deliberately bypasses .gitignore and copies host files into the box. Treat the box as the untrusted side and only carry what the agent genuinely needs (credentials, env files). The per-entry cap is box.cpMaxBytes (default 100 MiB), the same limit agentbox cp enforces.

Mark credential entries optional: true so a box still comes up when a given file or agent isn't installed on the host. optional covers exactly one case — the source does not exist. It is not a blanket "ignore problems": once a file is there and approved, a copy that fails (unreadable source, a destination the box refuses, a mode that won't set) fails the create and names every entry that failed. A box that came up reporting success while quietly missing a credential was the worse outcome.

For how non-carry env files reach the box, see environment; for how git state is seeded, see sync and git and teleport a project.

tools

Host CLIs the project's agents need — terraform, aws, ntn, linear. The box gets a shim; the host runs the real binary with its real credentials. Full guide: Host tools.

tools:
  - terraform
  - ntn

Or with per-tool options:

tools:
  aws:
    bin: aws # host binary, when it differs from the name
    allow: ['^s3 ls', '^sts '] # run with no approval prompt
    deny: ['^s3 rm', '^iam '] # refused outright
    timeoutMs: 300000 # per-call budget (default 120000)
FieldTypePurpose
binstringHost binary to run. Defaults to the tool name.
allowstring[]Regexes matched against the argv; matches skip the approval prompt.
denystring[]Regexes refused outright, on top of the built-in credential guard.
timeoutMsintPer-call wall-clock budget. Host tools are non-interactive.

allow and deny patterns are compile-checked when the file is parsed, so a typo fails loudly instead of silently never matching.

THIS BLOCK REQUESTS, IT DOES NOT GRANT

agentbox.yaml is committed, so a repo you cloned must not be able to reach your host credentials just by declaring a tool. AgentBox asks you once at create time and records the answer host-side; the relay reads only that record. Decline and the tools stay unavailable — the box is still created.

replacements

replacements declares reusable, named text-substitution rule-sets that both the carry: block (host-side) and the in-box agentbox-ctl render CLI can reference by name. Each rule is { from, to } with optional regex / flags; to may contain {{AGENTBOX_*}} placeholders.

replacements:
  box-host:
    # Repoint a hard-coded hostname at this box's published URL.
    - from: 'optima\.localhost'
      to: '{{AGENTBOX_BOX_HOST}}' # docker/hetzner: <box-name>.localhost; vercel/daytona/e2b: the real preview host
      regex: true

Three ways to apply substitutions:

1. Carry-time (host→box files). A carry: file entry can opt into replaceEnvs: true (placeholder substitution) and/or replace: (inline rules) and/or rules: (named refs). The file is rendered host-side into a temp copy before the copy — the original host file is never modified, and the box name is known by then:

carry:
  - src: ~/secrets/.env.prod
    dest: /workspace/apps/saas/.env
    replaceEnvs: true
    rules: [box-host]

3. Clone-time (a spawned bot's identity files). A rule-set named exactly identity is applied by agentbox clone to the files the box's agent declares as its identity — OpenClaw's SOUL.md and IDENTITY.md — so the new bot stops introducing itself with the source bot's name. It is applied once, on the host, to the copy on its way into the new box; your own files are never touched. Use \b word boundaries so a name does not rewrite words that merely contain it:

replacements:
  identity:
    - from: '\bAda\b'
      to: '{{AGENTBOX_BOX_NAME}}'
      regex: true

An OpenClaw bot can write this block itself — see spawning a second bot.

2. In-box (files already in the workspace). agentbox-ctl render is a declarative sed replacement — handy for rendering a gitignored .env from a committed env.example on every boot. The render is itself idempotent (the regex rules re-pin the same lines on every boot), so this task needs no run_once: guard:

tasks:
  env:
    command: agentbox-ctl render apps/saas/env.example --out apps/saas/.env --env --rules box-host

Placeholders

replaceEnvs / --env substitute a fixed whitelist of {{...}} placeholders (a stray {{FOO}} is left untouched, and secrets are never substitutable):

PlaceholderValue
{{AGENTBOX_BOX_NAME}}The box name.
{{AGENTBOX_BOX_HOST}}The host this box is reachable at. Docker/Hetzner: the published portless host <box-name>.localhost. Public-URL clouds (Vercel/Daytona/E2B): the real preview host (e.g. <sub>.vercel.run), wired in at boot so it matches what agentbox url returns.
{{AGENTBOX_BOX_ID}}The box id.
{{AGENTBOX_BOX_KIND}}docker or cloud.
{{AGENTBOX_HOST_WORKSPACE}}Host workspace path.
{{AGENTBOX_PROJECT_ROOT}}Project root.

Arbitrary substitutions go through explicit replace: / rules: rules, not the placeholder whitelist.

Generated secrets

render also expands a secret-generator token, so you can stop shelling out to openssl rand:

TokenBehavior
{{AGENTBOX_AUTO_SECRET}}A fresh 32-byte base64url secret each render. Stable in practice because you render the template→file once (guarded).
{{AGENTBOX_AUTO_SECRET:<name>}}Generated once, persisted at /var/lib/agentbox/secrets/<name>, reused on every later render — stable even if you render every boot.
# env.example
BETTER_AUTH_SECRET="{{AGENTBOX_AUTO_SECRET:better-auth}}"

AGENTBOX_AUTO_SECRET is a render-only token (the persistent store lives in the box); it isn't a replaceEnvs whitelist placeholder and isn't expanded by carry:.

agentbox-ctl render <src> flags: --out <path> (or --in-place, else stdout), --env (placeholder substitution), --rules <names> (comma-separated replacements: refs), --rule 'from=>to' (literal, repeatable), --rule-regex 'pat=>repl' (regex, repeatable), and --state-dir <path> (where named secrets persist).

TIP

Reach for replaceEnvs/render instead of hand-written sed: an env task that pins BETTER_AUTH_URL/NEXT_PUBLIC_APP_URL to https://{{ AGENTBOX_BOX_HOST }} becomes a one-liner, and the box and host browser then resolve the app at the same URL.

Validating the file

Editor validation is automatic via the schema modeline. The host CLI also pre-validates on agentbox create before any docker work — a config error aborts with a formatted message — and the in-box daemon re-validates on start. You can validate by hand inside the box without starting the daemon:

$ agentbox-ctl validate
OK: 1 service(s)

The path argument is optional and defaults to /workspace/agentbox.yaml. The command exits with code 2 on a syntax or shape error, or a missing file.

TIP

The runtime parser and the JSON Schema are kept in lockstep by a drift test, so what your editor flags and what the daemon enforces agree — except cross-field rules like max_ms >= initial_ms, which only the runtime validator can catch.

See cli for the host-side agentbox status and agentbox logs commands that proxy into the in-box supervisor.

On this page