Run Harness Agents in Docker Sandboxes (sbx)
Docker Sandboxes (sbx) runs an agent CLI inside a microVM. A custom harness provider can call sbx exec, so Dagu starts sbx on the worker and the agent runs inside the sandbox.
Compared with a harness container::
Harness container: | Docker Sandboxes | |
|---|---|---|
| Isolation | Container that shares the host kernel | MicroVM with its own kernel |
| API keys | Passed into the container through container.env or mounted files | Stored on the host. A host proxy adds them to outgoing HTTPS requests, and the sandbox sees only a placeholder. |
| Network | Container network mode | Allowlist managed with sbx policy |
| Lifecycle | Dagu creates and removes the container | You or the DAG create and remove the sandbox with sbx |
| Hosts | Any host with Docker or Podman | Apple silicon macOS, Windows 11, Ubuntu 24.04 or later with KVM |
The examples on this page were checked with sbx 0.49.
Requirements
- One of the hosts Docker supports for local sandboxes: macOS 14 or later on Apple silicon, Windows 11 on x86-64 with Windows Hypervisor Platform enabled, or Ubuntu 24.04 or later with KVM. See Install Docker Sandboxes.
- A Docker account.
sbxin thePATHof the Dagu process that executes the step. That is the server, scheduler, or worker process, not your shell.
A Dagu process that runs inside a container or a Kubernetes pod cannot start sandboxes unless that environment supports nested virtualization. With distributed execution, install sbx and sign in on each worker that runs these steps, then route the steps there with worker_selector.
One-Time Setup
On macOS:
brew trust docker/tap
brew install docker/tap/sbx
sbx login
sbx policy init balanced
echo "$ANTHROPIC_API_KEY" | sbx secret set anthropicsbx loginopens a browser. On a host without a browser, sign in with a Docker personal access token:echo "$DOCKER_PAT" | sbx login --username <docker-id> --password-stdin.sbx policy init balancedallows AI services and package registries and blocks other outbound traffic. Add hosts withsbx policy allow.sbx secret setstores the key in the OS keychain. It applies to every local sandbox, including sandboxes that already exist. Other services includeopenai,google,github, andcopilot. Runsbx secret set --helpfor the full list.
To use a Claude subscription instead of an API key, sign in once inside a long-lived sandbox (see the next section). Run sbx run --name dagu-claude claude, enter /login, then exit. The login state stays in that sandbox until it is removed.
Long-Lived Sandbox
Create the sandbox once, with the repository as its workspace:
sbx create --name dagu-claude claude /path/to/repoThen point a custom harness provider at it:
working_dir: /path/to/repo
harnesses:
claude_sbx:
binary: sbx
prefix_args: [exec, dagu-claude, claude, --dangerously-skip-permissions]
prompt_mode: flag
prompt_flag: -p
handler_on:
exit:
run: sbx stop dagu-claude
steps:
- id: review
action: harness.run
output: REVIEW
with:
provider: claude_sbx
model: sonnet
prompt: |
Review the uncommitted changes and list concrete bugs.Dagu runs this command on the worker:
sbx exec dagu-claude claude --dangerously-skip-permissions -p "Review the uncommitted changes..." --model sonnet- The workspace appears inside the sandbox at the same absolute path as on the host.
sbx execstarts in that directory. Add-w <path>afterexecto start somewhere else. sbx execstarts a stopped sandbox first. Installed tools, caches, and agent login state are kept.- The agent's stdout becomes the step's stdout, so
output:captures it. Its exit code becomes the step's exit code, so retries andwith.fallbackwork as they do for a host provider. - Claude Code in
-pmode cannot answer approval prompts.--dangerously-skip-permissionslets it edit files and run commands, and the sandbox limits what those commands can reach.
The exit handler matters. When a step is stopped or times out, Dagu ends the sbx exec process on the worker, but the agent inside the sandbox keeps running. sbx stop halts everything in the sandbox. The next run starts it again in a few seconds.
Give each DAG its own sandbox. The exit handler of one DAG would otherwise stop an agent that another DAG is running.
Fresh Sandbox per Run
Create the sandbox in a step and remove it in the exit handler. With --clone, the agent works on a private Git clone inside the sandbox, and the host repository is mounted read-only.
type: chain
working_dir: /path/to/repo
harnesses:
claude_sbx:
binary: sbx
prefix_args: [exec, dagu-fix, claude, --dangerously-skip-permissions]
prompt_mode: flag
prompt_flag: -p
handler_on:
exit:
run: sbx rm --force dagu-fix
steps:
- id: sandbox
run: |
sbx rm --force dagu-fix >/dev/null 2>&1 || true
sbx create --name dagu-fix --clone --no-progress claude .
- id: fix
action: harness.run
with:
provider: claude_sbx
prompt: |
Fix the failing unit tests and commit the change.
- id: collect
run: |
git fetch sandbox-dagu-fix
git branch -f dagu-fix "sandbox-dagu-fix/$(git branch --show-current)"sbx create --cloneadds a Git remote namedsandbox-<name>to the host repository. The clone starts on the host's current branch.- The
collectstep copies the agent's commits to the local branchdagu-fix.sbx rmremoves the remote, so fetch before theexithandler runs. - The first command in the
sandboxstep removes a sandbox left behind by a run that ended without itsexithandler, such as a run interrupted by a host restart. - Creating a sandbox boots a VM and may pull the agent image, so it takes longer than
sbx execinto an existing sandbox.
Drop --clone to mount the repository read-write instead. The agent's edits then appear in the host working tree immediately.
Environment Variables
DAG env:, params, and step outputs reach the sbx exec process on the worker, not the agent inside the sandbox. To pass a variable through, add -e NAME after exec. A bare name takes the value from the step's environment:
env:
- TICKET_ID: PROJ-123
harnesses:
claude_sbx:
binary: sbx
prefix_args: [exec, -e, TICKET_ID, dagu-claude, claude, --dangerously-skip-permissions]
prompt_mode: flag
prompt_flag: -p
steps:
- id: triage
action: harness.run
with:
provider: claude_sbx
prompt: |
Run ./scripts/fetch-ticket.sh, which reads the TICKET_ID
environment variable, and summarize the root cause.Do not pass API keys with -e. The value enters the sandbox, where the agent and anything it runs can read it. Store keys with sbx secret set instead.
Other Agents
The provider shape is the same for every agent: exec, the sandbox name, then the agent's non-interactive command.
| Agent | sbx create agent | prefix_args | Prompt | sbx secret set service |
|---|---|---|---|---|
| Claude Code | claude | [exec, NAME, claude, --dangerously-skip-permissions] | prompt_mode: flag, prompt_flag: -p | anthropic |
| Codex | codex | [exec, NAME, codex, exec] | prompt_mode: arg | openai |
| Gemini CLI | gemini | [exec, NAME, gemini] | prompt_mode: flag, prompt_flag: -p | google |
| GitHub Copilot CLI | copilot | [exec, NAME, copilot] | prompt_mode: flag, prompt_flag: -p | copilot |
| OpenCode | opencode | [exec, NAME, opencode, run] | prompt_mode: arg | the model provider's service |
Add each agent's own flag for running without approval prompts, as --dangerously-skip-permissions does for Claude Code. Run sbx create --help for the full list of agents.
Limitations
- Stopping or timing out a step does not stop the agent inside the sandbox. Use
sbx stoporsbx rm --forcein anexithandler, as in the examples above. - Dagu does not manage the sandbox. Creating, reusing, and removing it is up to the DAG or the operator.
- Do not combine these providers with a harness
container:. Withcontainer:, Dagu runs the provider binary inside the container, wheresbxis not available. sbxis under active development, and its flags have changed between releases. Checksbx versionon every worker after an upgrade.
Troubleshooting
| Message | Fix |
|---|---|
error: not signed in to Docker | Run sbx login as the user that runs Dagu. |
error: global network policy has not been initialized | Run sbx policy init balanced. |
Not logged in · Please run /login | Run sbx secret set anthropic, or sign in with /login inside sbx run --name NAME claude. |
error: sandbox 'NAME' not found | Create the sandbox first, or check the name with sbx ls. |
error: sandbox 'NAME' already exists | Reuse it with sbx exec, or remove it with sbx rm --force NAME. |
| A request from the agent is blocked | Allow the host with sbx policy allow. sbx policy log shows blocked requests. |

