odek runs agent shell commands inside an isolated Docker container when --sandbox is active. This document covers all configuration options, the Dockerfile.odek build system, security guarantees, and best practices.
# Enable sandbox with no network (default: none)
odek run --sandbox "npm install && npm test"
# Enable sandbox with internet access
odek run --sandbox --sandbox-network bridge "npm install && npm test"
# Use a specific base image
odek run --sandbox --sandbox-image node:20-alpine "echo hello"
# Custom Dockerfile for project-specific tooling
echo 'FROM golang:1.24-alpine
RUN apk add --no-cache protobuf
WORKDIR /workspace' > Dockerfile.odek
odek run --sandbox "protoc --version"All sandbox settings are available in ~/.odek/config.json, ./odek.json, ODEK_* env vars, and CLI flags, following the same priority chain.
{
"sandbox": true,
"sandbox_image": "node:20-alpine",
"sandbox_network": "none",
"sandbox_readonly": false,
"sandbox_memory": "512m",
"sandbox_cpus": "2",
"sandbox_user": "1000:1000",
"sandbox_env": {
"HOME": "/home/node",
"NODE_ENV": "development"
},
"sandbox_volumes": [
"./.npm:/root/.npm"
]
}| Field | Env var | CLI flag | Type | Default | Description |
|---|---|---|---|---|---|
sandbox |
ODEK_SANDBOX |
--sandbox |
bool | false |
Enable/disable sandbox isolation |
sandbox_image |
ODEK_SANDBOX_IMAGE |
--sandbox-image |
string | alpine:latest |
Docker image for the sandbox container |
sandbox_network |
ODEK_SANDBOX_NETWORK |
--sandbox-network |
string | none |
Docker network mode |
sandbox_readonly |
ODEK_SANDBOX_READONLY |
--sandbox-readonly |
bool | false |
Mount working directory read-only |
sandbox_memory |
ODEK_SANDBOX_MEMORY |
--sandbox-memory |
string | "" |
Memory limit (e.g. 512m, 2g) |
sandbox_cpus |
ODEK_SANDBOX_CPUS |
--sandbox-cpus |
string | "" |
CPU limit (e.g. 0.5, 2) |
sandbox_user |
ODEK_SANDBOX_USER |
--sandbox-user |
string | "" |
Run as user (uid:gid or name) |
sandbox_env |
— | — | object | {} |
Extra env vars injected into container |
sandbox_volumes |
— | — | array | [] |
Extra volume mounts (host:container) |
Note:
sandbox_envandsandbox_volumesare config-file-only — they're too complex for flat env vars or CLI flags. For all other fields, env vars and CLI flags follow the standardODEK_*pattern.Security restriction on
sandbox_volumes: Extra volume host paths must be inside the working directory. Absolute paths outside the project (e.g./var/run/docker.sock,/etc,/home/user/...) and paths containing..or symlinks are rejected. Relative paths are resolved relative to the working directory and must stay inside it.
./odek.json can be shipped by any repository the agent runs in. Because
project-level sandbox knobs (sandbox_env, sandbox_image, sandbox_network,
sandbox_volumes) can exfiltrate host secrets via ${VAR} interpolation, pull
an attacker-controlled image, or widen network access, odek requires explicit
operator approval before applying them.
When a project requests any sandbox override, odek prints a warning and prompts:
WARNING: project config (./odek.json) requests sandbox overrides:
image: alpine:latest
network: bridge
env: X
⚠️ sandbox_env values contain ${...} interpolation against host environment variables
Allowing this means code in the sandbox can read workspace files and,
depending on network mode, contact external hosts.
Approve? [y = once / t = trust this project / N]
Approval options:
y/yes— approve once for this run.t/trust— persist approval for this project/sandbox fingerprint in~/.odek/project_sandbox_approvals.json.N/ empty — abort.
For non-interactive use (CI, cron, scheduled tasks), set:
export ODEK_APPROVE_PROJECT_SANDBOX=1Operator-controlled sources (~/.odek/config.json, ODEK_* env vars, and CLI
flags) do not require approval — only values coming from the project-level
./odek.json are gated.
ODEK_SANDBOX=true \
ODEK_SANDBOX_IMAGE=python:3.12-slim \
ODEK_SANDBOX_NETWORK=none \
ODEK_SANDBOX_READONLY=true \
ODEK_SANDBOX_MEMORY=1g \
ODEK_SANDBOX_CPUS=4 \
ODEK_SANDBOX_USER=1000:1000 \
odek run "process untrusted data"# Run (single-shot)
odek run \
--sandbox \
--sandbox-image node:20-alpine \
--sandbox-network none \
--sandbox-readonly \
--sandbox-memory 512m \
--sandbox-cpus 2 \
--sandbox-user 1000:1000 \
"run build"
# REPL (interactive multi-turn)
odek repl \
--sandbox \
--sandbox-image golang:1.24-alpine \
--sandbox-memory 2g \
--model deepseek-v4-pro`odek provides two ways to control the sandbox environment:
Pick any public or private Docker image:
# Node.js
odek run --sandbox --sandbox-image node:20-alpine "npm run build"
# Python
odek run --sandbox --sandbox-image python:3.12-slim "pytest"
# Go
odek run --sandbox --sandbox-image golang:1.24-alpine "go test ./..."
# GPU workload
odek run --sandbox --sandbox-image nvidia/cuda:12.2-runtime "nvidia-smi"Place a Dockerfile.odek in your working directory for project-specific, pre-baked tooling. odek auto-detects it and builds an image with a content-hash tag.
⚠️ Security: building runs repo-controlled code on your host.docker buildexecutes the Dockerfile'sRUNinstructions outside the sandbox threat model — with default capabilities and the entire working directory readable as build context. A malicious repository could use this for host-adjacent code execution or to copy workspace files into the image. For this reason:
- The implicit build requires explicit operator approval, exactly like project-level sandbox overrides: an interactive prompt at startup (
y= once,t= trust this project), a persisted approval in~/.odek/project_sandbox_approvals.json, orODEK_APPROVE_PROJECT_SANDBOX=1for CI. The approval is keyed on the Dockerfile's content hash — editing the file forces re-approval.- The build runs with
--network=noneby default, soRUNsteps cannot fetch remote payloads or exfiltrate build-context data over the network. If your Dockerfile legitimately needs network (e.g.RUN apk add …), opt in withODEK_SANDBOX_BUILD_NETWORK=1(operator-only; a project cannot set it).- Builds that would start after startup (e.g. a new WebUI connection, or a resumed sandboxed session) re-verify the approval and fail closed if the Dockerfile appeared or changed in the meantime.
# Dockerfile.odek
FROM node:20-alpine
# Pre-install project dependencies (requires ODEK_SANDBOX_BUILD_NETWORK=1)
RUN apk add --no-cache git openssh
RUN npm install -g typescript tsx prettier
# Set up any user-level config
ENV NODE_ENV=development
WORKDIR /workspaceBuild behavior:
- odek check for
Dockerfile.odekin the working directory - If found and no explicit
sandbox_imageis configured, odek builds it — after the approval gate above - The image is tagged as
odek-sandbox:<sha256[:12]>based on file content hash - Cached: the image is only rebuilt when
Dockerfile.odekchanges - First build takes ~5–30s depending on the image; subsequent runs are instant
Priority:
sandbox_imageconfig field → use that image directly (explicit wins)Dockerfile.odekexists → build and use it- Neither →
alpine:latest
| Mode | Internet | Host access | Use case |
|---|---|---|---|
bridge (default) |
✅ Yes | ❌ No | npm install, go mod download, git clone, API calls |
none |
❌ No | ❌ No | Fully isolated — untrusted code, malware scans |
host |
✅ Yes | ✅ Yes | Debugging, local services, port sniffing |
Security note: bridge gives the container internet access but isolates it from the host's network stack (no access to localhost:port on the host, no access to your LAN). host mode removes that isolation — use only when you need to connect to a service on the host.
When running with --sandbox --ctx <file>, odek copies the ctx files into the container via docker cp:
- Files within the working directory preserve their relative path (
--ctx subdir/file.txt→/workspace/subdir/file.txt) - Files outside the working directory use their basename (
--ctx /etc/hosts→/workspace/hosts) - Missing files and directories are silently skipped
- In read-only mode, injection still works (docker cp writes to the container's overlay, not the volume bind-mount)
This ensures the agent can both see the file content in its context and operate on the physical file using read_file, patch, shell cat, etc. without any "content visible but file doesn't exist" gap.
When sandbox_readonly is true, the working directory is mounted read-only inside the container:
odek run --sandbox --sandbox-readonly "ls -la /workspace" # can read
odek run --sandbox --sandbox-readonly "touch /workspace/x" # failsodek's sandbox follows the principle of least privilege with progressive opt-in.
| Hardening | How it's enforced |
|---|---|
| No capabilities | --cap-drop ALL — even root has zero Linux capabilities |
| No privilege escalation | --security-opt no-new-privileges — setuid binaries can't escalate |
| No executable /tmp | --tmpfs /tmp:noexec — can't download+run binaries from temp |
| Auto-cleanup | --rm — container is destroyed on exit, no state persists |
| Isolated process | Detached sleep infinity — agent commands run via docker exec |
| Ephemeral | Container destroyed when agent finishes or is interrupted |
| Hardening | How it's enforced |
|---|---|
| No network | --network none — container cannot reach internet or LAN |
| Hardening | How it's enforced |
|---|---|
| Read-only workspace | -v $PWD:/workspace:ro — agent can read but not modify project files |
{
"sandbox": true,
"sandbox_image": "alpine:latest",
"sandbox_network": "none",
"sandbox_readonly": true
}{
"sandbox": true,
"sandbox_image": "node:20-alpine",
"sandbox_network": "none",
"sandbox_readonly": false,
"sandbox_memory": "2g",
"sandbox_env": {
"NODE_ENV": "development",
"NPM_CONFIG_CACHE": "/tmp/.npm"
},
"sandbox_volumes": [
"./.npm:/root/.npm"
]
}{
"sandbox": true,
"sandbox_image": "golang:1.24-alpine",
"sandbox_network": "none",
"sandbox_readonly": false,
"sandbox_memory": "4g",
"sandbox_cpus": "4",
"sandbox_env": {
"GOMAXPROCS": "4",
"GOCACHE": "/tmp/go-cache"
}
}- GPU passthrough is not yet configurable via odek flags — use
Dockerfile.odekwithnvidia/cudaimages and run the agent without sandbox mode for now - Docker-in-Docker requires special volume mounts (
/var/run/docker.sock) — not recommended with sandbox mode - Windows containers are not supported (tested on Linux only)