Terminal workspace orchestrator for parallel multi-repo development with AI coding agents.
Dual manages isolated development environments — one full git clone per workspace, one Docker container per clone — so you can run multiple repos on multiple branches simultaneously, all with Claude Code sessions active, all running dev servers on default ports, with zero conflicts.
curl (macOS/Linux):
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/jeevanpillay/dual/releases/latest/download/dual-installer.sh | shPowerShell (Windows):
powershell -ExecutionPolicy ByPass -c "irm https://github.com/jeevanpillay/dual/releases/latest/download/dual-installer.ps1 | iex"From source:
cargo install --path .# 1. Initialize your repo as a dual workspace
cd ~/code/my-project
dual init
# 2. Create a branch workspace
dual create feat/auth
# 3. Launch the TUI and select a workspace
dualdual opens an interactive workspace browser. Select a workspace to launch it — Dual clones the repo, starts a Docker container, sets up transparent command routing, and drops you into a tmux session. Detach from tmux (Ctrl+b d) and you're back in the browser.
Running dual with no arguments opens the workspace browser:
dual workspace browser
┌──────────────────────────────────────┐
│▼ my-project │
│ main ● running │
│ feat/auth ○ stopped │
│ feat/billing ◌ lazy │
│▼ agent-os │
│ main ● running │
└──────────────────────────────────────┘
j/k navigate enter launch q quit
- j/k or arrow keys to navigate
- Enter on a workspace to launch it (clone + container + tmux)
- Enter on a repo header to expand/collapse
- q or Esc to quit
When you select a workspace, the TUI suspends, tmux takes over. Detach from tmux (Ctrl+b d) and the TUI resumes automatically with fresh status.
For quick access from any tmux session, add to ~/.tmux.conf:
# Prefix + Space to open Dual picker in a popup
bind-key Space display-popup -E -w 60% -h 60% "dual"
# Or without prefix — Alt+Space (Meta+Space)
# bind-key -n M-Space display-popup -E -w 60% -h 60% "dual"Prefix + Space opens the Dual picker in a popup overlay. Select a workspace and the popup disappears as tmux switches to it.
| Command | Description |
|---|---|
dual |
Open TUI workspace browser |
dual init [--name NAME] |
Initialize current git repo as a workspace |
dual create <branch> [--repo NAME] |
Create a new branch workspace |
dual launch [workspace] |
Launch a workspace (auto-detects from cwd) |
dual list |
List all workspaces with status (non-interactive) |
dual destroy [workspace] |
Tear down workspace (container, tmux, clone) |
dual open [workspace] |
Open workspace services in browser |
dual urls [workspace] |
Display workspace URLs |
dual sync [workspace] |
Sync shared config files across branch workspaces |
dual proxy |
Start reverse proxy for browser access |
Dual uses two config files: devcontainer.json for container configuration and .dual/settings.json for Dual-specific orchestration.
Primary source for container configuration. Lives in .devcontainer/devcontainer.json (or .devcontainer.json at project root). Compatible with the Dev Containers ecosystem.
{
"image": "node:20",
"forwardPorts": [3000, 3001],
"postCreateCommand": "pnpm install",
"containerEnv": {
"NODE_ENV": "development"
}
}| Field | Description | Default |
|---|---|---|
image |
Docker image for the container | node:20 |
build.dockerfile |
Build image from Dockerfile instead of pulling | None |
forwardPorts |
Ports that services bind to (for reverse proxy) | [] |
postCreateCommand |
Command to run after first container creation | None |
containerEnv |
Environment variables passed to the container | {} |
mounts |
Volume mounts (volume type, /workspace/* targets become anonymous volumes) |
[] |
Dual-specific settings that the devcontainer spec can't express. Lives in .dual/settings.json in your project root. Created automatically by dual init.
{
"devcontainer": ".devcontainer/devcontainer.json",
"extra_commands": ["cargo", "go"],
"anonymous_volumes": ["node_modules", ".next"],
"shared": [".vercel", ".env.local"]
}| Field | Description | Default |
|---|---|---|
devcontainer |
Path to devcontainer.json | ".devcontainer/devcontainer.json" |
extra_commands |
Additional commands to route to the container | [] |
anonymous_volumes |
Container volumes (e.g., node_modules) |
["node_modules"] |
shared |
Files/directories to share across branch workspaces | [] |
Managed by Dual. Tracks all registered workspaces.
workspace_root = "~/dual-workspaces"
[[workspaces]]
repo = "my-project"
url = "git@github.com:org/my-project.git"
branch = "main"
path = "/Users/you/code/my-project"
[[workspaces]]
repo = "my-project"
url = "git@github.com:org/my-project.git"
branch = "feat/auth"When you select a workspace (via dual or dual launch):
- Clone — Clones the repo into
{workspace_root}/{repo}/{branch}/(usesgit clone --localfrom main workspace for speed) - Shared files — Copies shared config files (
.env.local,.vercel, etc.) from~/.dual/shared/{repo}/ - Container — Creates and starts a Docker container with the clone bind-mounted
- Setup — Runs
setupcommand on first launch (e.g.,pnpm install) - Shell RC — Generates transparent command routing that intercepts runtime commands and routes them to the container via
docker exec - Tmux — Creates a tmux session in the workspace directory and attaches
Your editor, git, and credentials stay on the host. The container handles all runtime processes. Claude Code never knows it's running inside a container.
On first run, dual automatically appends a small snippet to your ~/.zshrc or ~/.bashrc:
# dual: shell interception (auto-generated)
if [ -n "$DUAL_ACTIVE" ] && [ -n "$DUAL_RC_PATH" ] && [ -f "$DUAL_RC_PATH" ]; then
source "$DUAL_RC_PATH"
fiThis ensures that when you split a pane (Ctrl+b %) or create a new window (Ctrl+b c) inside a Dual tmux session, the new shell automatically loads command interception. Without this, new panes would run commands on the host instead of in the container.
The snippet is a no-op outside Dual sessions — it only activates when DUAL_ACTIVE is set (which Dual configures via tmux set-environment).
Terminal
├── State A: Dual TUI (ratatui)
│ └── Select workspace → suspend TUI → launch pipeline → tmux attach
└── State B: tmux session
└── Detach (Ctrl+b d) → resume TUI
Host Container
+--------------------------+ +--------------------------+
| nvim, git, claude, ssh | | pnpm, node, python |
| file reads/writes | | curl localhost, tests |
| credentials, SSH keys | | port-binding processes |
+--------------------------+ +--------------------------+
| bind mount |
+------------------+
Browser --> {repo}-{branch}.localhost:{port}
--> reverse proxy
--> container
| Host | Container |
|---|---|
| git, cat, ls, vim | npm, pnpm, node, python |
| File reads/writes | Port-binding processes |
| SSH, credentials | curl localhost, tests |
cargo build # Build debug binary
cargo build --release # Build release binary
cargo test # Run tests (~150 tests)
cargo clippy # Run linter
cargo fmt # Format codeTargets: Linux, macOS (Intel + Apple Silicon), Windows.
MIT