Isolated, pre-configured sandbox containers for AI coding agents — Claude Code, OpenAI Codex, Pi Agent, and more.
Spin up a fully-provisioned container where AI coding agents can operate with full permissions, persistent memory, and autonomous background tasks — without touching your host system.
Only host dependency: Docker.
git clone https://github.com/ryaneggz/open-harness.git && cd open-harness
cp .devcontainer/.example.env .devcontainer/.env # configure name, password, etc.
docker compose --env-file .devcontainer/.env -f .devcontainer/docker-compose.yml up -d --build
Option A — Terminal (works everywhere):
docker exec -it -u sandbox openharness bash # use your SANDBOX_NAME
Option B — VS Code Attach to Container (local):
Install the Dev Containers extension → Cmd+Shift+P → “Attach to Running Container” → select your sandbox.
Option C — VS Code Remote SSH + Attach (remote server): If Docker is running on a remote host, SSH into the host first, then attach to the container.
Add an entry to your local ~/.ssh/config so credentials forward automatically:
Host openharness
HostName your-server-ip
User openharness
ForwardAgent yes
openharnessOption D — SSH directly into sandboxes (multi-sandbox host):
Enable the sshd overlay and assign each sandbox a unique port. This lets you SSH straight into any sandbox — from your laptop, from another sandbox, or from CI. See Multi-sandbox SSH for full setup.
gh auth login # authenticate GitHub CLI
gh auth setup-git # configure git auth (no SSH keys needed)
pi # authenticate Pi Agent (OAuth) — powers Slack, heartbeats, and extensions
claude # terminal coding agent
pi # automations — Slack, heartbeats, extensions
docker compose -f .devcontainer/docker-compose.yml down -v
The sandbox image (Debian bookworm-slim) comes pre-installed with:
| Category | Tools |
|---|---|
| AI agents | Claude Code, OpenAI Codex, Pi Agent, Mom (Slack bot) |
| Runtimes | Node.js 22, pnpm, Bun, uv (Python) |
| DevOps | Docker CLI + Compose, GitHub CLI, cloudflared, tmux, cron |
| Browser | agent-browser + Chromium (headless, for web-capable agents) |
| Utilities | git, jq, ripgrep, nano, curl, wget, SSH server |
The sandbox user has passwordless sudo and full Docker socket access (with the docker overlay). Agents run with --dangerously-skip-permissions / --full-auto so they can operate autonomously.
Copy the example env file and edit to taste:
cp .devcontainer/.example.env .env
Docker Compose reads .env automatically from the project root.
| Variable | Default | Description |
|---|---|---|
SANDBOX_NAME |
openharness |
Container name, compose project name, and CLI identifier |
SANDBOX_PASSWORD |
changeme |
Linux user password — only used when the sshd overlay is active |
TZ |
America/Denver |
Container timezone — affects cron schedules and log timestamps |
Heartbeats are cron-scheduled tasks that run an AI agent CLI on a recurring schedule (e.g., hourly issue triage). Task definitions live in workspace/heartbeats.conf.
| Variable | Default | Description |
|---|---|---|
HEARTBEAT_AGENT |
claude |
Which agent CLI runs heartbeat tasks (claude, codex, pi) |
HEARTBEAT_ACTIVE_START |
(empty) | Hour (0-23) when heartbeats start firing (e.g., 8 for 8 AM) |
HEARTBEAT_ACTIVE_END |
(empty) | Hour (0-23) when heartbeats stop firing (e.g., 18 for 6 PM) |
When both ACTIVE_START and ACTIVE_END are set, heartbeats only fire during that window. When empty, heartbeats run at any hour.
| Variable | Default | Description |
|---|---|---|
HOST_SSH_DIR |
(empty) | Host SSH directory mounted read-only for git auth. Setting this auto-enables the ssh.yml overlay — no need to add it to config.json manually. |
The Slack bot (Mom) connects to a workspace via Socket Mode and delegates messages to a Pi agent. Only used with the slack overlay.
| Variable | Default | Description |
|---|---|---|
SLACK_APP_TOKEN |
(empty) | Slack app-level token for Socket Mode (xapp-...) |
SLACK_BOT_TOKEN |
(empty) | Slack bot OAuth token for posting messages (xoxb-...) |
The base docker-compose.yml provides the sandbox container with bind-mounted workspace and persistent auth volumes. Overlays add optional services and capabilities.
Toggle overlays in .openharness/config.json:
{
"composeOverrides": [
".devcontainer/docker-compose.docker.yml",
".devcontainer/docker-compose.slack.yml"
]
}
| Overlay | File | What it adds |
|---|---|---|
| postgres | docker-compose.postgres.yml |
Postgres 16 on a bridge network. Sets DATABASE_URL, PGHOST, PGUSER, PGPASSWORD, PGDATABASE automatically. Data persisted in a pgdata volume. |
| docker | docker-compose.docker.yml |
Mounts the host Docker socket so agents can run containers from inside the sandbox (Docker-in-Docker). |
| ssh | docker-compose.ssh.yml |
Mounts HOST_SSH_DIR read-only so git can authenticate with your existing SSH keys. Auto-enabled when HOST_SSH_DIR is set in .env. |
| ssh-generate | docker-compose.ssh-generate.yml |
Persists ~/.ssh in a named volume so generated keys survive container rebuilds. Use this instead of ssh if the sandbox should have its own keys. |
| sshd | docker-compose.sshd.yml |
Runs an SSH server inside the sandbox on port 2222:22. Enables direct SSH access for remote workflows and multi-sandbox setups. Uses SANDBOX_PASSWORD for auth. |
| slack | docker-compose.slack.yml |
Passes Slack tokens into the container and persists agent auth in a named volume. The bot auto-starts in a tmux session on container boot if both tokens are set. |
| cloudflared | docker-compose.cloudflared.yml |
Sets INSTALL_CLOUDFLARED=true and INSTALL_BROWSER=true so the entrypoint installs Cloudflare Tunnel and agent-browser on first boot. |
| git | docker-compose.git.yml |
Mounts GIT_COMMON_DIR for git worktree support across host and container. |
Multiple overlays can be combined. Order doesn’t matter.
Run multiple sandboxes on a single host, each reachable via SSH on a unique port. This enables:
Enable the sshd overlay for each sandbox and assign unique host ports.
Each sandbox gets its own project directory (or override file) with a distinct port mapping:
# sandbox-alpha — port 2222
services:
sandbox:
ports:
- "2222:22"
# sandbox-bravo — port 2223
services:
sandbox:
ports:
- "2223:22"
Configure your local ~/.ssh/config so each sandbox is a named host:
Host sandbox-alpha
HostName your-server-ip # or 127.0.0.1 for local Docker
Port 2222
User sandbox
ForwardAgent yes
Host sandbox-bravo
HostName your-server-ip
Port 2223
User sandbox
ForwardAgent yes
ForwardAgent yes passes your local SSH keys through to the sandbox, so git operations inside the container use your host credentials without copying keys.
Connect:
# Terminal
ssh sandbox-alpha
# VS Code
# Remote-SSH: Connect to Host → sandbox-alpha
Sandboxes on the same host can reach each other via the host ports or the Docker network. An agent in sandbox-alpha can SSH into sandbox-bravo:
ssh -p 2223 sandbox@host.docker.internal
This is useful for orchestration patterns where one agent delegates work to others.
The base compose file creates three named Docker volumes that persist across container rebuilds:
| Volume | Mount | Contents |
|---|---|---|
claude-auth |
~/.claude |
Claude Code OAuth tokens and config |
cloudflared-auth |
~/.cloudflared |
Cloudflare Tunnel credentials |
gh-config |
~/.config/gh |
GitHub CLI auth tokens |
The workspace directory (workspace/) is bind-mounted from the host, so changes are immediately visible in both directions. Project files, agent memory, heartbeat configs, and skills all live here and survive container rebuilds without needing a volume.
Overlays may add additional volumes:
| Overlay | Volume | Contents |
|---|---|---|
slack |
agent-auth |
Pi agent auth (~/.pi) |
ssh-generate |
ssh-keys |
Generated SSH keys (~/.ssh) |
postgres |
pgdata |
Postgres data directory |
docker compose down -v removes all volumes. Omit -v to keep them.
The workspace/ directory is the agent’s home. It’s bind-mounted into the container at /home/sandbox/harness/workspace/.
workspace/
AGENTS.md # Operating procedures — decision rules, skills, sub-agents
CLAUDE.md # Symlink → AGENTS.md (Claude Code reads this automatically)
SOUL.md # Agent personality, tone, values, guardrails
IDENTITY.md # Name, role, mission, stack, URLs
USER.md # Owner preferences, constraints, goals
TOOLS.md # Environment, available tools, service endpoints
HEARTBEAT.md # Meta-maintenance routines
MEMORY.md # Long-term memory (learned decisions, lessons)
heartbeats.conf # Cron schedule for autonomous tasks
heartbeats/ # Task definitions (markdown files)
startup.sh # Runs on container boot after onboarding
memory/ # Daily activity logs (YYYY-MM-DD.md)
projects/ # Application code (e.g., Next.js app)
.claude/
rules/ # Coding standards (auto-loaded by Claude Code)
skills/ # Reusable skill definitions
agents/ # Sub-agent prompts
| File | Owns | Updated by |
|---|---|---|
IDENTITY.md |
Name, role, mission, stack, URLs | Orchestrator (initial), agent (evolves) |
SOUL.md |
Personality, tone, values, guardrails | Orchestrator (initial), agent (evolves) |
USER.md |
Owner preferences, constraints, goals | User or orchestrator |
TOOLS.md |
Environment details, service endpoints | Orchestrator |
AGENTS.md |
Decision rules, skills, procedures | Orchestrator (initial), agent (evolves) |
HEARTBEAT.md |
Meta-maintenance routines | Agent |
MEMORY.md |
Learned facts, decisions, lessons | Agent |
The orchestrator scaffolds these files during provisioning. Once the agent is running, it owns and evolves them.
Heartbeats are cron-scheduled autonomous tasks. The agent CLI runs a prompt from a markdown file on a recurring schedule.
workspace/heartbeats.conf uses a 5-field cron expression:
# cron expression | task file | agent | active_hours
*/30 * * * * | heartbeats/build-health.md | claude | 9-21
50 23 * * * | heartbeats/nightly-release.md | claude
0 * * * * | heartbeats/documentation-drift-delegate.md
| Field | Required | Description |
|---|---|---|
| Cron expression | Yes | Standard 5-field cron (min hour dom mon dow) |
| Task file | Yes | Markdown file with the prompt to run |
| Agent | No | Which CLI to use (defaults to HEARTBEAT_AGENT env var) |
| Active hours | No | START-END range (24h) — overrides env vars per-task |
# Inside the sandbox:
heartbeat.sh sync # Install cron entries from heartbeats.conf
heartbeat.sh status # Show active cron jobs and last run times
heartbeat.sh stop # Remove all heartbeat cron entries
Logs are written to ~/.heartbeat/heartbeat.log inside the container.
The openharness CLI runs on the host (outside the container).
| Command | Description |
|---|---|
openharness sandbox [name] |
Build and start a sandbox |
openharness run [name] |
Start an existing (stopped) container |
openharness shell <name> |
Open a bash shell inside the sandbox |
openharness stop [name] |
Stop the container |
openharness clean [name] |
Full cleanup — containers and volumes |
openharness onboard [name] |
Run the first-time setup wizard |
openharness list |
List running sandboxes |
openharness heartbeat <action> <name> |
Manage heartbeats (sync, stop, status) |
openharness worktree <name> |
Create a git worktree for parallel branches |
Run openharness with no arguments for interactive AI agent mode.
The CLI requires Node.js on the host. It’s optional — all core operations can be done with docker compose and docker exec directly.
curl -fsSL https://raw.githubusercontent.com/ryaneggz/open-harness/refs/heads/main/install.sh | bash
.devcontainer/ # Sandbox environment
Dockerfile # Debian bookworm-slim + all tooling
docker-compose.yml # Base sandbox service
docker-compose.*.yml # Compose overlays (postgres, sshd, slack, etc.)
entrypoint.sh # Container bootstrap script
.example.env # Environment variable template
install/ # Provisioning scripts
onboard.sh # First-time auth wizard (6 steps)
heartbeat.sh # Cron scheduler for autonomous tasks
entrypoint.sh # Late-stage container setup
cloudflared-tunnel.sh # Cloudflare Tunnel configuration
slack-manifest.json # Slack app manifest for Socket Mode
workspace/ # Agent workspace template (bind-mounted)
packages/
sandbox/ # @openharness/sandbox — CLI + container lifecycle
slack/ # Vendored fork of pi-mom — Slack bot
.openharness/ # Runtime config (config.json, settings)
.github/ # CI workflows + issue templates
docs/ # Documentation site (Nextra)
CalVer: YYYY.M.D (e.g., 2026.4.4). Push a tag to build and publish to ghcr.io/ryaneggz/open-harness.