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 --buildOption A β Terminal (works everywhere):
docker exec -it -u sandbox oh-local bash # use your SANDBOX_NAMEOption 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/configso credentials forward automatically:Host openharness HostName your-server-ip User openharness ForwardAgent yes -
In VS Code: Remote-SSH: Connect to Host β
openharness -
Once connected to the remote host: Attach to Running Container β select your sandbox.
Option 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 extensionsclaude # terminal coding agent
pi # automations β Slack, heartbeats, extensionsdocker compose -f .devcontainer/docker-compose.yml down -vThe 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 .devcontainer/.envDocker Compose and the openharness CLI read .devcontainer/.env directly.
| 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). Each heartbeat is a markdown file in workspace/heartbeats/ with YAML frontmatter defining its schedule, agent, and active hours.
| Variable | Default | Description |
|---|---|---|
HEARTBEAT_AGENT |
claude |
Default agent CLI for heartbeats without an agent frontmatter field |
HEARTBEAT_INTERVAL |
1800 |
Default interval (seconds) for legacy HEARTBEAT.md fallback |
Per-heartbeat scheduling and active hours are configured via YAML frontmatter (schedule, agent, active fields) β see Heartbeats below.
| 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.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. |
| 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:
- Direct SSH from your laptop into any sandbox
- Sandbox-to-sandbox communication (agents orchestrating other agents)
- CI/CD integration β run commands inside sandboxes from pipelines
-
Enable the
sshdoverlay 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/configso 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 yesForwardAgent yespasses 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.internalThis 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/ # Heartbeat task definitions (frontmatter .md 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. A TypeScript daemon watches workspace/heartbeats/ and runs each heartbeat's prompt via an AI agent CLI on its configured schedule.
Each heartbeat is a markdown file in workspace/heartbeats/ with YAML frontmatter:
---
schedule: "*/30 * * * *"
agent: claude
active: 9-21
---
# Build Health Check
Run a quick health check on the project...| Field | Required | Default | Description |
|---|---|---|---|
schedule |
Yes | β | Standard 5-field cron expression (min hour dom mon dow) |
agent |
No | claude |
Which CLI to use (claude, codex, pi) |
active |
No | (always) | START-END hour range (24h) β heartbeat only fires in this window |
Comment out the schedule field (prefix with #) to disable a heartbeat without deleting it.
Use the /heartbeat skill inside the sandbox to create a new heartbeat:
/heartbeat check build health every 30 minutes during business hours
This writes the .md file and the daemon auto-detects it within 500ms β no manual sync needed.
From the host, use the openharness CLI:
openharness heartbeat status <sandbox-name> # Show running schedules and recent logs
openharness heartbeat sync <sandbox-name> # Force re-read of all heartbeat files
openharness heartbeat stop <sandbox-name> # Remove all heartbeat schedulesInside the sandbox, use the heartbeat-daemon binary directly:
heartbeat-daemon status # Show running schedules and recent logs
heartbeat-daemon sync # Force re-read of all heartbeat filesThe daemon starts automatically on container boot, watches the heartbeats/ directory for file changes (fs.watch), and performs differential sync β only restarting jobs whose schedule or config actually changed.
Logs are written to workspace/heartbeats/heartbeat.log.
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
devcontainer.json # Dev Container metadata
docker-compose.yml # Base sandbox service
docker-compose.*.yml # Compose overlays (postgres, sshd, slack, etc.)
entrypoint.sh # Container bootstrap script
init-env.sh # Seed .devcontainer/.env on first provision (SANDBOX_NAME, GIT_COMMON_DIR)
.example.env # Environment variable template
install/ # Provisioning scripts
onboard.sh # First-time auth wizard
entrypoint.sh # Late-stage container setup (starts heartbeat daemon)
setup.sh # Environment setup utilities
tmux-agent.sh # tmux session management for agents
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.