Back to OpenClaw

Subagent Workspace Isolation in OpenClaw: Ephemeral Git Worktrees, Branch Sandboxes, and Clean Merges

Isolate multi-agent coding tasks using ephemeral Git worktrees, isolated runtime sandboxes, and automated verification gates before merging code to main.

Subagent Workspace Isolation in agent runtime: Ephemeral Git Worktrees, Branch Sandboxes, and Clean Merges
Image credit: labs.zeroshot.studio

Contents


The Problem: Concurrent Agent Collisions

When you move from single-turn prompts to multi-agent coding swarms, the first bottleneck is rarely the intelligence of the LLM. It is the filesystem.

If two subagents operate concurrently in a shared repository root, they inevitably step on each other:

  1. Working Tree Contamination: Agent A modifies a database schema while Agent B runs an integration test suite. The test fails because Agent A left uncommitted syntax errors in a shared model file.
  2. Lock Contention: Both agents run package managers (npm install or uv pip install) simultaneously, triggering lock file collisions and corrupted node modules or wheel caches.
  3. Ghost Artifacts: One agent creates scratch debug scripts or temporary dumps in the working directory. When the supervisor agent attempts a git commit -a, it inadvertently commits broken scratch files and partial diffs to main.
  4. Unrecoverable Merge Collisions: If an agent hallucinates or crashes halfway through an 800-line refactor, rolling back changes requires destructive resets (git reset --hard) that wipe out legitimate concurrent work done by other processes.

Treating the primary working directory as a communal scratchpad fails quickly. To build resilient autonomous software workflows, every subagent must execute inside an ephemeral, isolated workspace that can be discarded without touching the developer branch.

For related runtime context on process execution and hooks, see our guides on Headless Terminal Execution in agent runtime and Fail-Safe Agent Hooks and Sandboxes.


Architecture: Shared Object Store vs Ephemeral Worktrees

The naive solution to agent isolation is cloning the entire repository for every task (git clone /path/to/repo /tmp/task-123). While this provides process isolation, it introduces severe operational overhead:

  • Disk Duplication: A 2GB repository with large commit histories consumes 20GB of disk space if you spin up 10 parallel subagents.
  • Cache Cold Starts: Each fresh clone forces a complete re-download and re-compilation of vendor dependencies (node_modules, .venv, or target/), turning a 10-second agent task into a 3-minute build cycle.
  • Synchronization Lag: Pushing and pulling between local directories adds network emulation overhead and git remote confusion.

The production pattern is native Git Worktrees (git worktree).

text
                    ┌─────────────────────────┐                    │   Primary Repository    │                    │   (/srv/apps/my-app)    │                    │   .git/ (Object Store)  │                    └────────────┬────────────┘                                 │         ┌───────────────────────┼───────────────────────┐         │                       │                       │         ▼                       ▼                       ▼┌──────────────────┐    ┌──────────────────┐    ┌──────────────────┐│  Worktree: T-101 │    │  Worktree: T-102 │    │  Worktree: T-103 ││  Branch: ag/101  │    │  Branch: ag/102  │    │  Branch: ag/103  ││  Isolated Code   │    │  Isolated Code   │    │  Isolated Code   ││  Symlinked Caches│    │  Symlinked Caches│    │  Symlinked Caches│└──────────────────┘    └──────────────────┘    └──────────────────┘

A Git worktree creates a new filesystem checkout directory linked directly to the parent .git directory. It shares the existing commit history, packfiles, and object store instantly without copying bytes on disk. Each subagent checks out its own task branch, edits files in complete isolation, and runs independent builds.


Step 1: Provisioning Ephemeral Subagent Worktrees

To implement this pattern, we wrap worktree provisioning in a deterministic lifecycle script.

Here is the operational bash harness (agent-worktree.sh) that provisions an isolated task directory:

Terminalbash
#!/usr/bin/env bashset -euo pipefailREPO_ROOT="$(git rev-parse --show-toplevel)"WORKTREES_DIR="${REPO_ROOT}/.worktrees"TASK_ID="${1:-}"if [[ -z "${TASK_ID}" ]]; then  echo "Usage: ./agent-worktree.sh create "  exit 1fiACTION="${2:-create}"BRANCH_NAME="agent/${TASK_ID}"WORKTREE_PATH="${WORKTREES_DIR}/${TASK_ID}"mkdir -p "${WORKTREES_DIR}"case "${ACTION}" in  create)    echo "[worktree] Creating isolated workspace for task: ${TASK_ID}"        # Prune stale worktree references    git worktree prune    # Ensure branch does not already exist    if git rev-parse --verify "${BRANCH_NAME}" >/dev/null 2>&1; then      git branch -D "${BRANCH_NAME}"    fi    # Create new branch off current HEAD in detached worktree path    git worktree add -b "${BRANCH_NAME}" "${WORKTREE_PATH}" HEAD    # Symlink shared build caches if available (prevents reinstalling)    if [[ -d "${REPO_ROOT}/node_modules" ]]; then      ln -s "${REPO_ROOT}/node_modules" "${WORKTREE_PATH}/node_modules"    fi    if [[ -d "${REPO_ROOT}/.venv" ]]; then      ln -s "${REPO_ROOT}/.venv" "${WORKTREE_PATH}/.venv"    fi    echo "[worktree] Isolated workspace ready at: ${WORKTREE_PATH}"    ;;  cleanup)    echo "[worktree] Cleaning up workspace for task: ${TASK_ID}"    if [[ -d "${WORKTREE_PATH}" ]]; then      git worktree remove "${WORKTREE_PATH}" --force || true    fi    git worktree prune    if git rev-parse --verify "${BRANCH_NAME}" >/dev/null 2>&1; then      git branch -D "${BRANCH_NAME}" || true    fi    echo "[worktree] Workspace ${TASK_ID} removed successfully."    ;;  *)    echo "Unknown action: ${ACTION}"    exit 1    ;;esac

Make the script executable and add .worktrees/ to your root .gitignore:

Terminalbash
chmod +x agent-worktree.shecho ".worktrees/" >> .gitignoregit add .gitignore && git commit -m "chore: ignore ephemeral agent worktrees"

Step 2: Scoping agent runtime Agent Runtime Boundaries

Once the worktree exists, the agent runtime subagent process must be constrained strictly to that path. An agent with unrestricted shell access might accidentally run cd .. or execute commands against files outside its sandboxed directory.

In your agent runtime agent manifest or supervisor configuration, specify the working directory parameter:

json
{  "agent_id": "refactor-worker-01",  "task_id": "T-4029",  "workspace_root": ".worktrees/T-4029",  "sandbox": {    "enforce_cwd": true,    "allow_parent_traversal": false,    "environment": {      "GIT_WORK_TREE": ".worktrees/T-4029",      "CI": "true"    }  },  "tools": [    "file_read",    "file_write",    "file_edit",    "bash_exec"  ]}

When the subagent invokes tool calls, all relative paths resolve within .worktrees/T-4029. If the agent crashes, gets stuck in a loop, or generates nonsensical code, the primary repository remains 100% clean.

For more details on designing multi-agent topologies, read Dynamic Subagent Dispatch in agent runtime.


Step 3: Automated Verification and Gatekeeper Review

Autonomous code should never merge automatically based solely on an agent claiming "task complete."

Before the supervisor agent or human triggers a merge, the changes in the worktree must pass through an automated gatekeeper verification script (verify-subagent.py):

python
#!/usr/bin/env python3"""ZeroLabs Gatekeeper: Automated Subagent Verification GateValidates syntax, formatting, types, unit tests, and file scopes before merge."""import subprocessimport sysimport osdef run_gate(command: list[str], cwd: str, stage_name: str) -> bool:    print(f"[*] Running Gate: {stage_name} ({' '.join(command)})")    res = subprocess.run(command, cwd=cwd, capture_output=True, text=True)    if res.returncode != 0:        print(f"[-] FAILED: {stage_name}")        print(res.stderr or res.stdout)        return False    print(f"[+] PASSED: {stage_name}")    return Truedef verify_worktree(worktree_path: str) -> bool:    if not os.path.isdir(worktree_path):        print(f"[-] Error: Directory {worktree_path} not found.")        return False    # 1. Scope Check: Did the agent modify unauthorized files (e.g., CI configs, root .env)?    diff_res = subprocess.run(        ["git", "diff", "--name-only", "HEAD"],        cwd=worktree_path,        capture_output=True,        text=True    )    changed_files = [f.strip() for f in diff_res.stdout.splitlines() if f.strip()]    restricted_prefixes = [".github/", ".env", "agent-worktree.sh"]    for cf in changed_files:        if any(cf.startswith(p) for p in restricted_prefixes):            print(f"[-] Security Gate Failed: Agent touched protected file: {cf}")            return False    # 2. Syntax & Lint Gate    if os.path.exists(os.path.join(worktree_path, "package.json")):        if not run_gate(["npm", "run", "lint"], cwd=worktree_path, stage_name="Linter"):            return False        if not run_gate(["npx", "tsc", "--noEmit"], cwd=worktree_path, stage_name="Typecheck"):            return False        if not run_gate(["npm", "test", "--", "--passWithNoTests"], cwd=worktree_path, stage_name="Unit Tests"):            return False    elif os.path.exists(os.path.join(worktree_path, "pyproject.toml")) or os.path.exists(os.path.join(worktree_path, "requirements.txt")):        if not run_gate(["ruff", "check", "."], cwd=worktree_path, stage_name="Ruff Lint"):            return False        if not run_gate(["pytest", "-q"], cwd=worktree_path, stage_name="Pytest Suite"):            return False    print("[+] All verification gates passed successfully.")    return Trueif __name__ == "__main__":    if len(sys.argv) ")        sys.exit(1)        target_path = sys.argv[1]    if not verify_worktree(target_path):        sys.exit(1)

Step 4: Atomic Merge and Ephemeral Cleanup

When the gatekeeper script returns an exit code of 0, the supervisor proceeds to integrate the task branch into main.

To maintain a clean, readable Git history without cluttered micro-commits from agent iterations, follow this squash-merge sequence:

Terminalbash
# 1. Switch to root repositorycd "${REPO_ROOT}"# 2. Rebase task branch onto latest main to detect upstream conflictsgit fetch origin maingit -C "${WORKTREE_PATH}" rebase origin/main# 3. Fast-forward or squash merge into maingit checkout maingit merge --squash "agent/${TASK_ID}"git commit -m "feat(agent): implement isolated task ${TASK_ID} [verified]"# 4. Remove ephemeral worktree and branch./agent-worktree.sh "${TASK_ID}" cleanup

If the verification script fails at any point:

  • No merge occurs.
  • The supervisor captures the error logs and re-prompts the subagent with the exact compiler or test failure.
  • If the task is abandoned, ./agent-worktree.sh cleanup deletes the worktree folder and deletes the task branch cleanly. Zero uncommitted changes remain on the main branch.

Failure Modes and Recovery

Failure ScenarioRoot CauseRecovery Action
fatal: '.worktrees/X' already existsPrevious agent crashed before cleanupRun git worktree remove --force .worktrees/X followed by git worktree prune.
fatal: Unable to create '.git/index.lock'Concurrent git commands executed against object storeAdd 250ms exponential backoff around git worktree add commands in the supervisor script.
Symlinked node_modules mutatedAgent executed npm install inside worktreeMark root node_modules read-only or configure package manager frozen lockfile flags (npm ci --prefer-offline).
Incomplete diff on abortContext window ran out mid-file editWorktree can be discarded with git worktree remove --force with zero risk of dirtying main.

FAQ

Does git worktree add duplicate the full .git directory?

No. Git worktrees share the parent repository's .git/objects and packfiles. The worktree folder only contains the checked-out files and a tiny .git pointer file (a few bytes) that references the parent .git/worktrees/ state.

How do I handle environment variables and .env files in subagents?

Do not copy root .env files with production secrets into agent worktrees. Instead, have the supervisor generate an ephemeral .env.test containing local mock credentials, scoped specifically to the subagent task.

Can multiple subagents work on the same branch?

No. Git prevents checking out the same branch in more than one worktree simultaneously. Each subagent must receive a unique branch name (e.g., agent/task-101, agent/task-102).

What is the maximum number of concurrent worktrees a Linux VPS can support?

The bottleneck is not Git, but available RAM and file descriptors. An 8GB VPS running lightweight Node.js or Python test runners can comfortably manage 6 to 10 concurrent agent worktrees without memory thrashing.

How do we clean up orphaned worktrees after system reboot?

Run git worktree prune on system startup. Git automatically scans .git/worktrees/, detects any paths that no longer exist on disk, and removes the stale tracking records.


For more technical runbooks, templates, and agent orchestration guides, check out the ZeroLabs Resources Hub.

Share