Isolated Docker environment for running code agents (OpenCode, GitHub Copilot CLI, Hermes, Ollama, Claude Code, Jules, Qwen Code, Codex) on a real repository: git, gh, optional branch, commit, push, and optional pull request.
The sandbox supports eight executors:
| Executor | Description | Key Variable |
|---|---|---|
| opencode | OpenCode AI CLI | OPENAI_API_KEY |
| copilot | GitHub Copilot CLI | GITHUB_TOKEN |
| hermes | Hermes via OpenRouter | OPENROUTER_API_KEY |
| ollama | Ollama local (Jan/Qwen) | OLLAMA_HOST (default: http://host.docker.internal:11434) |
| claude | Claude Code (Anthropic) | ANTHROPIC_API_KEY |
| jules | Jules (Google) | GOOGLE_CLOUD_PROJECT |
| qwen | Qwen Code | DASHSCOPE_API_KEY |
| codex | Codex (OpenAI) | CODEX_API_KEY |
Having a reproducible sandbox where an agent:
- Operates only inside the container, with resource limits and reduced privileges.
- Sees code in
/workspace(your chosen host directory). - Can fetch, (optionally) create branch, commit, push and (optionally) open a PR to the default branch of
origin.
| Executor | Description | Default Model |
|---|---|---|
| opencode | OpenCode AI CLI | latest OpenCode |
| copilot | GitHub Copilot CLI | GPT-4o / Claude Sonnet |
| hermes | Hermes (Nous Research) | openrouter/nousresearch/hermes-3-llama-3.1-405b |
| ollama | Ollama local (Jan) | qwen3:latest (change with MODEL) |
| claude | Claude Code (Anthropic) | claude-opus-4-5 (change with MODEL) |
| jules | Jules (Google) | Google's default model |
| qwen | Qwen Code | qwen3.6-plus (change with MODEL) |
| codex | Codex (OpenAI) | claude-sonnet-4-20250514 (change with MODEL) |
- Docker running
- Git installed on the host
- SSH key for GitHub authentication (default:
~/.ssh/id_rsa)
| Executor | Required | How to Get |
|---|---|---|
OPENAI_API_KEY |
For OpenCode | platform.openai.com or opencode.ai |
OPENROUTER_API_KEY |
For Hermes/OpenRouter | openrouter.ai/keys |
ANTHROPIC_API_KEY |
For Claude Code | console.anthropic.com |
CODEX_API_KEY |
For Codex | platform.openai.com |
GITHUB_TOKEN |
For Copilot, push, and PR | gh auth token or create manually |
GOOGLE_CLOUD_PROJECT |
For Jules | Google Cloud Console |
DASHSCOPE_API_KEY |
For Qwen Code | Alibaba Cloud ModelStudio |
BAILIAN_CODING_PLAN_API_KEY |
For Qwen Code (Coding Plan) | Alibaba Cloud Coding Plan |
OLLAMA_HOST |
For Ollama (local) | Default: http://host.docker.internal:11434 |
- Hermes: Ollama running on the host (default at
http://localhost:11434). If Docker cannot uselocalhost, use the special namehost.docker.internalwhich Docker for Mac/Windows resolves to the host. For Linux:--network=hostor-e OLLAMA_HOST=http://172.17.0.1:11434. - Ollama: Ollama must be running on the host with the desired model downloaded (e.g.
ollama pull qwen3).
The token is resolved in this order:
- Environment variable
GITHUB_TOKEN - Output of
gh auth token - File
githubtokenin the sandbox-agents root (useful for testing; do not commit this to git)
- SSH key for pushing via SSH (default:
~/.ssh/id_rsa; change withSSH_KEY) - Optional:
~/.local/share/opencode/auth.jsonfor reusing OpenCode session (read-only mount)
Qwen Code supports two authentication methods:
If you've authenticated locally with qwen auth, your credentials are automatically reused in the sandbox via a read-only mount of ~/.qwen.
# Local setup (one-time)
qwen auth # Opens browser for login with your qwen.ai account
# Then use in sandbox - credentials are automatically mounted
TASK='Your task' sandbox-qwenThe OAuth credentials are stored in ~/.qwen/oauth_creds.json and mounted to the container automatically (read-only).
For higher request quotas, use Alibaba Cloud API keys:
# Dashscope API
export DASHSCOPE_API_KEY=sk-...
TASK='Your task' sandbox-qwen
# Or Coding Plan API (recommended for heavy usage)
export BAILIAN_CODING_PLAN_API_KEY=sk-...
TASK='Your task' sandbox-qwenGet your API keys at:
git clone https://github.com/your-org/sandbox-agents.git
cd sandbox-agentsexport OPENAI_API_KEY=sk-... # unless using Copilot exclusively
make buildmake check-dockerFor convenience, install the wrapper scripts that allow parallel execution:
# Create a symlink or copy to a directory in PATH
mkdir -p ~/.local/bin
ln -s /path/to/sandbox-agents/bin/* ~/.local/bin/
# Or copy them
cp /path/to/sandbox-agents/bin/* ~/.local/bin/
chmod +x ~/.local/bin/sandbox-*Make sure ~/.local/bin is in your PATH:
export PATH="$HOME/.local/bin:$PATH"
# Add to ~/.zshrc or ~/.bashrc for persistence
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc| Command | Executor | Description |
|---|---|---|
sandbox-opencode |
OpenCode AI | Run task with OpenCode |
sandbox-copilot |
GitHub Copilot | Run task with Copilot CLI |
sandbox-hermes |
Hermes (OpenRouter) | Run task with Hermes |
sandbox-ollama |
Ollama local | Run task with local Ollama |
sandbox-claude |
Claude Code (Anthropic) | Run task with Claude Code |
sandbox-jules |
Jules (Google) | Run task with Jules |
sandbox-qwen |
Qwen Code | Run task with Qwen Code |
sandbox-codex |
Codex (OpenAI) | Run task with Codex |
sandbox-agent |
Interactive menu | Interactive TTY menu |
sandbox-shell |
Interactive shell | Interactive bash shell |
Each command uses a unique container name (with timestamp) allowing parallel execution on the same repo.
# Navigate to your repo
cd ~/my-project
# Run a task with OpenCode (creates agent/task-opencode branch)
TASK='Fix authentication bug' sandbox-opencode
# Run another task in parallel with Copilot (creates agent/task-copilot branch)
TASK='Add unit tests' sandbox-copilot
# Run with Hermes in another terminal
TASK='Refactor user module' sandbox-hermes
# Run with local Ollama
TASK='Optimize queries' sandbox-ollama
# Run with Claude Code (Anthropic)
TASK='Add new API endpoint' sandbox-claude
# Run with Jules (Google)
TASK='Write unit tests' sandbox-jules
# Run with Qwen Code (Alibaba Cloud)
TASK='Optimize database queries' sandbox-qwen
# Run with Codex (OpenAI)
TASK='Refactor auth module' sandbox-codex
# Interactive menu
sandbox-agentImportant: Each wrapper automatically:
- Creates a unique container name (with timestamp)
- Creates a new branch for the task
- Mounts the current directory as workspace
From the sandbox-agents directory:
make runThis opens an interactive bash shell inside the sandbox with scripts available at /opt/sandbox/scripts.
From the root of the repo you want to modify:
cd /path/to/your-repo
export OPENAI_API_KEY=sk-... # if using OpenCode
make -f /path/to/sandbox-agents/makefile agent-uiThe script asks: executor (OpenCode, Copilot, Hermes, or Ollama), model (empty = CLI default), task, context, and whether you want a new branch and PR.
Flags before the menu (also via Make):
make -f /path/to/sandbox-agents/makefile agent-ui AGENT_UI_FLAGS='--no-branch'
make -f /path/to/sandbox-agents/makefile agent-ui AGENT_UI_FLAGS='--no-branch --no-pr'Or directly:
WORKSPACE=/path/to/your-repo /path/to/sandbox-agents/scripts/sandbox-docker.sh agent-ui --no-prcd /path/to/your-repo
make -f /path/to/sandbox-agents/makefile agent-task TASK='Describe the task'See Environment Variables for all available options.
Examples:
RUNNER=copilot make -f /path/to/sandbox-agents/makefile agent-task TASK='Fix the warning in X'
RUNNER=hermes make -f /path/to/sandbox-agents/makefile agent-task TASK='Refactor function Y'
RUNNER=ollama make -f /path/to/sandbox-agents/makefile agent-task TASK='Create tests'
SANDBOX_SKIP_BRANCH=1 SANDBOX_SKIP_PR=1 make -f /path/to/sandbox-agents/makefile agent-task TASK='Local commit only'WORKSPACE=/path/to/repo /path/to/sandbox-agents/scripts/sandbox-docker.sh run| Variable | Default | Description |
|---|---|---|
RUNNER |
opencode |
Agent executor to use: opencode, copilot, hermes, ollama, claude, jules, qwen, codex |
MODEL |
executor default | Model to use for the executor |
SANDBOX_MODEL |
- | Model passed to OpenCode -m or Copilot --model |
CONTEXT |
- | Additional context appended to the task prompt |
| Variable | Required | Description |
|---|---|---|
OPENAI_API_KEY |
For OpenCode | OpenAI API key for OpenCode executor |
ANTHROPIC_API_KEY |
For Claude Code | Anthropic API key for Claude Code executor |
OPENROUTER_API_KEY |
For Hermes | OpenRouter API key for Hermes executor |
CODEX_API_KEY |
For Codex | OpenAI API key for Codex executor |
GITHUB_TOKEN |
For Copilot & PR | GitHub token for Copilot CLI and push/PR operations |
GOOGLE_CLOUD_PROJECT |
For Jules | Google Cloud project ID for Jules executor |
DASHSCOPE_API_KEY |
For Qwen Code | Alibaba Cloud ModelStudio API key |
BAILIAN_CODING_PLAN_API_KEY |
For Qwen Code (Coding Plan) | Alibaba Cloud Coding Plan API key |
CURSOR_API_KEY |
Optional | API key for Cursor agent |
OLLAMA_HOST |
For Ollama | Ollama server URL (default: http://host.docker.internal:11434) |
| Variable | Default | Description |
|---|---|---|
GIT_USER_NAME |
agent |
Git user name for commits |
GIT_USER_EMAIL |
agent@example.com |
Git user email for commits |
SSH_KEY |
~/.ssh/id_rsa |
SSH private key for git operations |
GIT_SSH_PRIVATE_KEY |
- | SSH private key content (alternative to SSH_KEY) |
| Variable | Default | Description |
|---|---|---|
WORKSPACE |
current directory | Path to mount as /workspace in the container |
IMAGE |
sandbox-agents:latest |
Docker image to use |
CONTAINER_NAME |
sandbox-agents |
Name for the Docker container |
GITHUB_TOKEN_FILE |
./githubtoken |
Path to GitHub token file fallback |
AUTH_JSON |
~/.local/share/opencode/auth.json |
OpenCode session file (read-only mount) |
QWEN_AUTH_DIR |
~/.qwen |
Qwen Code OAuth credentials directory (read-only mount) |
SANDBOX_SKIP_BRANCH |
0 |
Set to 1 to skip branch creation |
SANDBOX_SKIP_PR |
0 |
Set to 1 to skip PR creation |
COPILOT_MODEL |
- | Specific model for Copilot CLI |
# API Keys
export OPENAI_API_KEY=sk-...
export ANTHROPIC_API_KEY=sk-ant-...
export OPENROUTER_API_KEY=sk-or-...
export CODEX_API_KEY=sk-...
export GITHUB_TOKEN=ghp_...
export GOOGLE_CLOUD_PROJECT=my-project
export DASHSCOPE_API_KEY=sk-...
export BAILIAN_CODING_PLAN_API_KEY=sk-...
# Ollama
export OLLAMA_HOST=http://host.docker.internal:11434
# Git Configuration
export GIT_USER_NAME="Agent Bot"
export GIT_USER_EMAIL="agent@example.com"
export SSH_KEY=~/.ssh/id_rsa
# Sandbox Options
export WORKSPACE=/path/to/repo
export RUNNER=opencode
export SANDBOX_SKIP_BRANCH=0
export SANDBOX_SKIP_PR=0
export MODEL=gpt-4oThe host mounts scripts/ to /opt/sandbox/scripts. No need to copy scripts to the product repo.
| Script | Usage |
|---|---|
agent-interactive.sh |
Interactive TTY menu |
agent-task.sh |
Minimal input → run-task.sh |
run-task.sh |
Assembles prompt + executes all executors |
lib/workflow-instructions.sh |
Git/PR workflow text (optional branch/PR) |
To add a new agent/executor, follow these steps:
Add a new runner function in scripts/run-task.sh:
run_newagent() {
# Verify required tools
command -v curl >/dev/null 2>&1 || {
echo "Error: 'curl' not installed."
exit 1
}
local model="${MODEL:-default-model}"
local prompt="${1:-$PROMPT}"
# Prepare API call or command
local json_payload
json_payload=$(jq -n \
--arg model "$model" \
--arg prompt "$prompt" \
'{
"model": $model,
"messages": [
{"role": "user", "content": $prompt}
],
"stream": false
}')
echo "Using NewAgent with model: $model"
local response
response=$(curl -s -X POST "https://api.newagent.example.com/chat" \
-H "Authorization: Bearer $NEWAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d "$json_payload")
if [[ -z "$response" ]]; then
echo "Error: No response received from NewAgent."
exit 1
fi
echo "$response" | jq -r '.choices[0].message.content // "Error: could not extract response"'
}Add the new agent to the case statement in scripts/run-task.sh:
case "$(printf '%s' "$RUNNER" | tr '[:upper:]' '[:lower:]')" in
opencode | code)
run_opencode
;;
copilot | gh-copilot | github-copilot)
run_copilot
;;
hermes | openrouter)
run_hermes
;;
ollama | local)
run_ollama
;;
newagent) # NEW: Add your new agent here
run_newagent
;;
*)
echo "Error: Unknown RUNNER: $RUNNER"
exit 1
;;
esacAdd the new agent option in scripts/agent-interactive.sh:
echo "Executors available:"
echo " 1) opencode — OpenCode AI CLI"
echo " 2) copilot — GitHub Copilot CLI"
echo " 3) hermes — Hermes via OpenRouter"
echo " 4) ollama — Ollama local"
echo " 5) newagent — New Agent" # NEW
read -r -p "Choose executor [1-5] (1): " choice
case "$choice" in
1 | opencode | "")
export RUNNER=opencode
;;
2 | copilot)
export RUNNER=copilot
;;
3 | hermes)
export RUNNER=hermes
;;
4 | ollama)
export RUNNER=ollama
;;
5 | newagent) # NEW
export RUNNER=newagent
;;
*)
echo "Invalid option."
exit 1
;;
esacIn scripts/sandbox-docker.sh, add environment variable passing to the Docker container if needed:
-e NEWAGENT_API_KEY="${NEWAGENT_API_KEY:-}" \Update the table in this README.md with the new agent information.
.cursor/skills/sandbox-git-agent-workflow/SKILL.md describes the standard workflow (fetch, optional branch, commit, push, optional PR) for the agent to replicate when working outside the container or extending the sandbox.
- Copilot in non-interactive mode uses limited permissions (
shell(git:*),shell(gh:*),read,write). Adjust inrun-task.shif you need more tools. - Hermes via OpenRouter uses Nous Research models. Available models:
openrouter/nousresearch/hermes-3-llama-3.1-405b,openrouter/nousresearch/hermes-3-llama-3.1-70b,openrouter/nousresearch/hermes-2-pro-llama-3-8b. - Ollama uses Ollama's local API. Requires Ollama running on the host with the model downloaded (e.g.
ollama pull qwen3). For Linux, you may need--network=hostor useOLLAMA_HOST=http://172.17.0.1:11434instead ofhost.docker.internal. - Copilot models: see
copilot helpor documentation. OpenCode:opencode models. originmust exist and the token/key must allow push and PRs if not usingSANDBOX_SKIP_*.
| Path | Role |
|---|---|
Dockerfile |
Sandbox image definition |
opencode.json |
OpenCode permissions |
entrypoint.sh |
SSH + git config setup |
scripts/sandbox-docker.sh |
docker run host script: run, agent-task, agent-ui |
scripts/run-task.sh |
Prompt assembly + executor execution |
makefile |
build, run, agent-ui, agent-task targets |
.cursor/skills/sandbox-git-agent-workflow/ |
Git/PR workflow skill |
The sandbox runs with limited resources for safety:
- Memory: 4GB limit
- CPUs: 2 cores
- PIDs: 512 process limit
- Capabilities: Dropped all (
--cap-drop=ALL) - Security: No new privileges (
--security-opt=no-new-privileges:true)
Create a .env file in your project root for common settings:
# API Keys
OPENAI_API_KEY=sk-...
OPENROUTER_API_KEY=sk-or-...
GITHUB_TOKEN=ghp_...
# Ollama (if using local models)
OLLAMA_HOST=http://host.docker.internal:11434
# Git Configuration
GIT_USER_NAME=Your Name
GIT_USER_EMAIL=your.email@example.com
# SSH Key (if pushing via SSH)
SSH_KEY=~/.ssh/id_rsa
# Claude Code (Anthropic)
ANTHROPIC_API_KEY=sk-ant-...
# Jules (Google)
GOOGLE_CLOUD_PROJECT=your-project-id
# Qwen Code (Alibaba Cloud)
DASHSCOPE_API_KEY=sk-...
# Or use Coding Plan for higher quotas
BAILIAN_CODING_PLAN_API_KEY=sk-...
# Codex (OpenAI)
CODEX_API_KEY=sk-...Load the environment before running commands:
set -a && source .env && set +a
make -f /path/to/sandbox-agents/makefile agent-task TASK='Your task'Create a Makefile in your project to simplify sandbox usage:
SANDBOX_DIR = /path/to/sandbox-agents
.PHONY: agent
agent:
@export OPENAI_API_KEY=$$OPENAI_API_KEY && \
export GITHUB_TOKEN=$$GITHUB_TOKEN && \
make -f $(SANDBOX_DIR)/makefile agent-ui
.PHONY: agent-task
agent-task:
@test -n "$(TASK)" || { echo "Usage: make agent-task TASK='Your task'"; exit 1; }
@export OPENAI_API_KEY=$$OPENAI_API_KEY && \
export GITHUB_TOKEN=$$GITHUB_TOKEN && \
make -f $(SANDBOX_DIR)/makefile agent-task TASK='$(TASK)'For Linux hosts where host.docker.internal doesn't work, create a custom network:
docker network create sandbox-net 2>/dev/null || true
# Get host gateway IP
HOST_IP=$(ip route | grep default | awk '{print $3}')
# Run with custom network and host IP
docker run --rm -it \
--network sandbox-net \
-e OLLAMA_HOST=http://$HOST_IP:11434 \
-v /path/to/repo:/workspace \
sandbox-agents:latest bash# OpenCode with default settings
make -f /path/to/sandbox-agents/makefile agent-task TASK='Add input validation to login form'
# Copilot with specific model
RUNNER=copilot SANDBOX_MODEL=claude-sonnet-4.5 make -f /path/to/sandbox-agents/makefile agent-task TASK='Refactor user auth module'
# Hermes (Nous Research)
RUNNER=hermes make -f /path/to/sandbox-agents/makefile agent-task TASK='Write unit tests for calculator'
# Qwen Code (uses OAuth if authenticated locally, or DASHSCOPE_API_KEY)
RUNNER=qwen make -f /path/to/sandbox-agents/makefile agent-task TASK='Fix authentication bug'
DASHSCOPE_API_KEY=sk-... RUNNER=qwen make -f /path/to/sandbox-agents/makefile agent-task TASK='Add new feature'# 1. Start with interactive menu
make -f /path/to/sandbox-agents/makefile agent-ui
# 2. Or chain multiple tasks
make -f /path/to/sandbox-agents/makefile agent-task TASK='Create feature branch for user dashboard'
# 3. Make changes, then commit
make -f /path/to/sandbox-agents/makefile agent-task TASK='Implement dashboard UI components'
# 4. Open PR
make -f /path/to/sandbox-agents/makefile agent-task TASK='Create pull request with changelog'# Process multiple files
make -f /path/to/sandbox-agents/makefile agent-task TASK='Fix TypeScript errors in src/utils/*.ts'
# Add context for better results
CONTEXT='We use ESLint with @typescript-eslint/recommended' \
make -f /path/to/sandbox-agents/makefile agent-task TASK='Fix linting errors'# Ensure Ollama is running
ollama serve
# Pull a coding model
ollama pull codellama:7b
ollama pull qwen2.5-coder:7b
# Use with sandbox
RUNNER=ollama MODEL=codellama:7b \
make -f /path/to/sandbox-agents/makefile agent-task TASK='Optimize database queries'# GitHub Actions example
- name: Run sandbox agent
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
docker run --rm \
-e GITHUB_TOKEN \
-e OPENAI_API_KEY \
-v ${{ github.workspace }}:/workspace \
sandbox-agents:latest \
bash /opt/sandbox/scripts/agent-task.sh"Docker daemon is not running"
# macOS
open -a Docker
# Linux
sudo systemctl start docker
# Verify
docker info"Cannot connect to Docker daemon"
# Add your user to docker group
sudo usermod -aG docker $USER
newgrp docker"Permission denied (publickey)"
# Verify SSH key is configured
ssh -T git@github.com
# Check which key is being used
GIT_SSH_COMMAND='ssh -v' git fetch origin
# Set correct SSH key
export SSH_KEY=~/.ssh/id_rsa # or your key path"Authentication failed"
# Verify GitHub token
gh auth status
# Re-authenticate if needed
gh auth login
# Or export token directly
export GITHUB_TOKEN=$(gh auth token)"fatal: could not read Username"
# Ensure HTTPS is used instead of SSH
git config --global url."https://github.com/".insteadOf git@github.com:
# Or use token in URL
git remote set-url origin https://x-access-token:$GITHUB_TOKEN@github.com/user/repo.git"OpenAI API key not valid"
# Verify key is set
echo $OPENAI_API_KEY | head -c 10
# Check format (should start with sk-)
# Export correct key
export OPENAI_API_KEY=sk-..."OpenRouter quota exceeded"
# Check usage at https://openrouter.ai/keys
# Generate new key if needed
export OPENROUTER_API_KEY=sk-or-..."Authentication required" or browser login prompt
# Option 1: Use OAuth (free tier - 100 requests/day)
# First authenticate locally:
qwen auth
# Your credentials (~/.qwen) are automatically mounted to the container
# Option 2: Use API key (higher quotas)
export DASHSCOPE_API_KEY=sk-...
# or
export BAILIAN_CODING_PLAN_API_KEY=sk-..."OAuth credentials not found"
# Ensure ~/.qwen directory exists and is accessible
ls -la ~/.qwen/
# If you haven't authenticated yet, run:
qwen auth"API key not valid"
# Verify your API key is set correctly
echo $DASHSCOPE_API_KEY | head -c 10
# Check at Alibaba Cloud ModelStudio
# https://modelstudio.console.alibabacloud.com/"connection refused" on Linux
# Option 1: Use host network
docker run --network=host ...
# Option 2: Set explicit host IP
export OLLAMA_HOST=http://$(hostname -I | awk '{print $1}'):11434
# Option 3: For Docker Desktop, use host.docker.internal
# Already configured by default"model not found"
# Pull the model
ollama pull qwen3:latest
# List available models
ollama list
# Create custom model
ollama create my-code-model -f Modelfile"Cannot allocate memory"
# Increase Docker memory in Docker Desktop > Settings > Resources
# Or limit sandbox usage:
docker run --memory=2g --cpus=1 ..."too many open files"
# Check current limits
ulimit -n
# Temporarily increase
ulimit -n 4096
# Permanently (macOS)
echo "ulimit -n 4096" >> ~/.zshrc"Permission denied" in workspace
# Check directory permissions
ls -la /path/to/workspace
# Fix ownership
sudo chown -R $(id -u):$(id -g) /path/to/workspace
# Or use a directory you own
WORKSPACE=~/projects/my-repo make -f /path/to/sandbox-agents/makefile agent-task TASK='...'Enable verbose output
# Run with bash -x
bash -x /path/to/sandbox-agents/scripts/sandbox-docker.sh agent-task
# Or check Docker logs
docker logs <container-id>Verify sandbox setup
# Run interactive shell
make run
# Inside container, verify:
git status
gh auth status
opencode --version # or copilot --version