ai-agents #parallel-agents #git-worktree #lease-contract #agent-ops #concurrency

Parallel AI Agents on a Shared Checkout — The Lease Contract You Need

Running multiple coding agents on one Git working tree without a lease contract is a merge race, not parallelism. Here's the isolation and ownership pattern that prevents silent corruption.

Two agents. One checkout. No lease. The merge race starts the moment the second agent writes.

Your team adopts Claude Code for the backend, Codex for the frontend, and a Hermes subagent for the migration script. They all target main. The first agent creates a feature branch, stages changes, commits. The second agent, unaware, checks out the same branch, stages its own changes, commits. The third agent runs a rebase. The result: orphan commits, staging pollution, a force-push that rewrites history, and three hours of forensic git archaeology to recover what was lost. This is not a hypothetical. GeekNews has documented parallel agent failure patterns including branch hijacking, orphan commits, staging pollution, and double implementation [Source: https://news.hada.io/topic?id=26120].

Staging pollution: one commit, two agents, zero intent

The question is not whether this demos well; it is whether it survives maintenance, handoff, and local constraints. In the Modoo Laravel SaaS projects we maintain across Jakarta and Ho Chi Minh City, parallel agent runs without isolation have caused more silent data loss than any deployment rollback. The pattern repeats: teams treat “run agents in parallel” as a speed button. It is a migration contract — isolation, ownership, and a named integration gate.

Parallel agents on shared checkout — merge race illustration

The Anatomy of a Shared-Checkout Merge Race

A shared-checkout merge race occurs when two or more autonomous coding agents operate on the same Git working tree without explicit coordination. Unlike traditional parallel development where humans communicate via PRs and Slack, agents operate at machine speed with no implicit coordination protocol.

GeekNews identifies four primary failure modes [Source: https://news.hada.io/topic?id=26120]:

Failure ModeMechanismDetection Difficulty
Branch hijackingAgent B force-moves the branch tip while Agent A is still writingLow (visible in reflog)
Orphan commitsAgent A commits; Agent B resets hard; Agent A’s commits become unreachableMedium (requires git reflog)
Staging pollutionAgent A stages files; Agent B stages different files; git commit captures bothHigh (silent, looks intentional)
Double implementationBoth agents implement the same feature independently in different filesVery high (discovered at PR review)

The critical insight: staging pollution is the most dangerous because it produces a single commit that looks correct but contains changes from two unrelated agents. No test fails. No conflict marker appears. The corruption ships.

Why Git Worktree Is the Isolation Primitive

Git’s worktree command (stable since Git 2.5, 2015) creates additional working directories linked to the same repository. Each worktree has its own checked-out branch, index, and HEAD — but shares the object database. This is exactly the isolation boundary parallel agents need.

 1# Main repo at /home/developer/project
 2cd /home/developer/project
 3
 4# Create isolated worktree for Agent A (backend)
 5git worktree add ../project-agent-a feature/agent-a-backend-api
 6# Creates /home/developer/project-agent-a with feature/agent-a-backend-api checked out
 7
 8# Create isolated worktree for Agent B (frontend)
 9git worktree add ../project-agent-b feature/agent-b-frontend-ui
10# Creates /home/developer/project-agent-b with feature/agent-b-frontend-ui checked out
11
12# Create isolated worktree for Agent C (migration)
13git worktree add ../project-agent-c feature/agent-c-migration
14# Creates /home/developer/project-agent-c with feature/agent-c-migration checked out
15
16# List all worktrees
17git worktree list

Git worktree isolation — separate index, separate HEAD, shared object DB

Each agent now operates in its own directory with its own branch. No staging pollution. No branch hijacking. No orphan commits from a sibling’s reset --hard.

Git worktree isolation architecture

Worktree Lifecycle Management

Worktrees are lightweight but not free. Each consumes disk space for the working tree (not the .git objects). Cleanup is essential.

 1# /opt/agents/cleanup-worktrees.sh
 2#!/usr/bin/env bash
 3# Runs daily via systemd timer. Removes worktrees for merged/deleted branches.
 4
 5set -euo pipefail
 6
 7REPO_ROOT="/home/developer/project"
 8MAX_AGE_DAYS=7
 9
10cd "$REPO_ROOT"
11
12# Prune dead worktree administrative files
13git worktree prune
14
15# Remove worktrees for branches that have been merged to main
16for wt in $(git worktree list --porcelain | grep '^worktree ' | cut -d' ' -f2); do
17    branch=$(git -C "$wt" branch --show-current 2>/dev/null || echo "")
18    [[ -z "$branch" ]] && continue
19    
20    # Skip main branch worktree
21    [[ "$branch" == "main" ]] && continue
22    
23    # Check if branch is merged to main
24    if git merge-base --is-ancestor "$branch" main 2>/dev/null; then
25        echo "Removing merged worktree: $wt (branch: $branch)"
26        git worktree remove "$wt" --force
27        continue
28    fi
29    
30    # Check age of last commit on branch
31    last_commit_date=$(git -C "$wt" log -1 --format=%ct "$branch" 2>/dev/null || echo 0)
32    if [[ $last_commit_date -gt 0 ]]; then
33        age_days=$(( ($(date +%s) - last_commit_date) / 86400 ))
34        if (( age_days > MAX_AGE_DAYS )); then
35            echo "Removing stale worktree: $wt (branch: $branch, age: ${age_days}d)"
36            git worktree remove "$wt" --force
37        fi
38    fi
39done

The Lease Contract: Ownership Before Access

Isolation via worktrees solves the structural problem. But you still need a coordination protocol: who owns what, for how long, and what happens when the lease expires.

Digital Thought Disruption’s control plane series describes the pattern: designing a safe AI agent execution runtime with idempotency, isolation, post-condition verification, compensation, rollback, and evidence controls [Source: https://digitalthoughtdisruption.com/2026/07/25/execute-verify-rollback-agent-actions/]. The lease contract is the isolation + ownership layer.

 1# /etc/agent-leases/agent-a-backend.yaml
 2# Lease contract for Agent A (backend API work)
 3agent:
 4  id: "claude-code-backend"
 5  role: "backend-api"
 6  capabilities: ["php", "laravel", "sql", "redis"]
 7  
 8lease:
 9  resource: "git-worktree"
10  path: "/home/developer/project-agent-a"
11  branch: "feature/agent-a-backend-api"
12  max_duration_hours: 4
13  heartbeat_interval_seconds: 60
14  
15ownership:
16  primary:
17    name: "Platform Team"
18    slack: "#platform-oncall"
19    phone: "+62-812-XXXX-XXXX"
20  escalation:
21    - after_minutes: 30
22      action: "alert_owner_slack"
23    - after_minutes: 60
24      action: "page_oncall"
25    - after_minutes: 120
26      action: "revoke_lease_and_cleanup"
27      
28proof:
29  type: "composite"
30  heartbeat_file: "/var/lib/agent-leases/agent-a-backend.json"
31  verification_probe: "/opt/agents/verify_agent_a.py"
32  max_age_hours: 5
33  
34integration_gate:
35  required_reviews: 1
36  required_checks: ["phpstan", "pest", "lint"]
37  auto_merge: false
38  merge_strategy: "squash"

The lease contract says: Agent A owns this worktree for up to 4 hours. It must heartbeat every 60 seconds. If the heartbeat stops, the lease is revoked and the worktree cleaned up. Integration requires human review and passing checks.

Lease lifecycle: request → heartbeat → verify → integrate → cleanup

Heartbeat Implementation

 1# /opt/agents/lease_heartbeat.py
 2#!/usr/bin/env python3
 3"""
 4Lease heartbeat writer. Called by the agent wrapper every 60 seconds.
 5Writes a machine-readable heartbeat file with agent status.
 6"""
 7import json
 8import os
 9import sys
10import time
11import subprocess
12from pathlib import Path
13from datetime import datetime, timezone
14
15LEASE_DIR = Path("/var/lib/agent-leases")
16LEASE_DIR.mkdir(parents=True, exist_ok=True)
17
18def write_heartbeat(agent_id: str, worktree_path: str, branch: str, status: str, details: dict = None) -> None:
19    heartbeat = {
20        "agent_id": agent_id,
21        "worktree_path": worktree_path,
22        "branch": branch,
23        "status": status,  # "running", "completed", "failed", "revoked"
24        "timestamp": datetime.now(timezone.utc).isoformat(),
25        "pid": os.getpid(),
26        "hostname": os.uname().nodename,
27        "details": details or {}
28    }
29    
30    heartbeat_file = LEASE_DIR / f"{agent_id}.json"
31    heartbeat_file.write_text(json.dumps(heartbeat, indent=2))
32    
33    # Also write simple timestamp for dead-man's-switch checks
34    timestamp_file = LEASE_DIR / f"{agent_id}.last_heartbeat"
35    timestamp_file.write_text(str(int(time.time())))
36
37def check_lease_expiry(agent_id: str, max_age_seconds: int) -> bool:
38    """Returns True if lease is expired (no heartbeat within max_age)."""
39    timestamp_file = LEASE_DIR / f"{agent_id}.last_heartbeat"
40    if not timestamp_file.exists():
41        return True
42    last_heartbeat = int(timestamp_file.read_text().strip())
43    return (time.time() - last_heartbeat) > max_age_seconds
44
45if __name__ == "__main__":
46    # Example: python lease_heartbeat.py claude-code-backend /home/dev/project-agent-a feature/agent-a-backend-api running
47    if len(sys.argv) < 5:
48        print("Usage: lease_heartbeat.py <agent_id> <worktree_path> <branch> <status> [details_json]")
49        sys.exit(1)
50    
51    agent_id = sys.argv[1]
52    worktree_path = sys.argv[2]
53    branch = sys.argv[3]
54    status = sys.argv[4]
55    details = json.loads(sys.argv[5]) if len(sys.argv) > 5 else {}
56    
57    write_heartbeat(agent_id, worktree_path, branch, status, details)

Post-Condition Verification: Trust But Verify

TianPan’s decision provenance work makes this explicit: “When autonomous agents take consequential actions, having logs is not the same as having accountability.” The article provides a practical guide to designing decision provenance for production agentic systems — covering event schemas, ownership handoffs, hallucination attribution, and compliance requirements [Source: https://tianpan.co/blog/2026-04-19-decision-provenance-agentic-systems].

The lease contract defines who owns what. The verification probe defines what proves the work happened.

 1# /opt/agents/verify_agent_a.py
 2#!/usr/bin/env python3
 3"""
 4Verification probe for Agent A (backend API).
 5Runs AFTER the agent claims completion. Queries actual side effects.
 6"""
 7import os
 8import json
 9import subprocess
10from pathlib import Path
11
12WORKTREE_PATH = "/home/developer/project-agent-a"
13BRANCH = "feature/agent-a-backend-api"
14EXPECTED_FILES = [
15    "app/Http/Controllers/Api/V1/UserController.php",
16    "app/Services/UserService.php",
17    "tests/Feature/Api/UserApiTest.php"
18]
19MIN_TEST_COVERAGE = 80
20
21def check_files_exist() -> dict:
22    missing = []
23    for f in EXPECTED_FILES:
24        if not Path(WORKTREE_PATH, f).exists():
25            missing.append(f)
26    return {
27        "ok": len(missing) == 0,
28        "missing_files": missing
29    }
30
31def check_tests_pass() -> dict:
32    result = subprocess.run(
33        ["./vendor/bin/pest", "--coverage", "--min=80"],
34        cwd=WORKTREE_PATH,
35        capture_output=True,
36        text=True,
37        timeout=300
38    )
39    return {
40        "ok": result.returncode == 0,
41        "output": result.stdout[-2000:] if result.stdout else "",
42        "error": result.stderr[-2000:] if result.stderr else ""
43    }
44
45def check_static_analysis() -> dict:
46    result = subprocess.run(
47        ["./vendor/bin/phpstan", "analyse", "--level=5"],
48        cwd=WORKTREE_PATH,
49        capture_output=True,
50        text=True,
51        timeout=180
52    )
53    return {
54        "ok": result.returncode == 0,
55        "output": result.stdout[-2000:] if result.stdout else ""
56    }
57
58def check_git_status() -> dict:
59    result = subprocess.run(
60        ["git", "status", "--porcelain"],
61        cwd=WORKTREE_PATH,
62        capture_output=True,
63        text=True
64    )
65    # Should be clean (all changes committed)
66    return {
67        "ok": result.stdout.strip() == "",
68        "uncommitted_changes": result.stdout.strip().split('\n') if result.stdout.strip() else []
69    }
70
71if __name__ == "__main__":
72    results = {
73        "files": check_files_exist(),
74        "tests": check_tests_pass(),
75        "static_analysis": check_static_analysis(),
76        "git_status": check_git_status()
77    }
78    
79    overall_ok = all(r["ok"] for r in results.values())
80    results["overall"] = overall_ok
81    
82    print(json.dumps(results, indent=2))
83    sys.exit(0 if overall_ok else 1)

This probe runs independently of the agent. It doesn’t trust the agent’s self-report. It queries the effect — files created, tests passing, static analysis clean, git status clean.

Post-condition verification: trust the effect, not the agent

Lease contract flow: heartbeat → verification → integration gate

Integration Gate: The Human-in-the-Loop Merge

The final stage is the integration gate. Digital Thought Disruption’s control plane emphasizes: “execute, verify, rollback” [Source: https://digitalthoughtdisruption.com/2026/07/25/execute-verify-rollback-agent-actions/]. Most teams stop at verify. The integration gate is where the lease contract pays off.

 1# Extended integration gate in lease contract
 2integration_gate:
 3  required_reviews: 1
 4  required_checks: ["phpstan", "pest", "lint"]
 5  auto_merge: false
 6  merge_strategy: "squash"
 7  pre_merge_hooks:
 8    - "verify_agent_a.py"
 9    - "check_conflicts_with_main.py"
10  post_merge_actions:
11    - "cleanup_worktree.py"
12    - "notify_downstream_agents.py"
13    - "update_deployment_manifest.py"

Conflict Detection Before Merge

 1# /opt/agents/check_conflicts_with_main.py
 2#!/usr/bin/env python3
 3"""
 4Pre-merge conflict check. Runs in the agent's worktree.
 5Detects if the agent's branch has conflicts with main before attempting merge.
 6"""
 7import subprocess
 8import sys
 9from pathlib import Path
10
11WORKTREE_PATH = "/home/developer/project-agent-a"
12BRANCH = "feature/agent-a-backend-api"
13
14def has_conflicts_with_main() -> dict:
15    # Fetch latest main
16    subprocess.run(["git", "fetch", "origin", "main"], cwd=WORKTREE_PATH, check=True)
17    
18    # Try merge in memory (--no-commit --no-ff)
19    result = subprocess.run(
20        ["git", "merge", "--no-commit", "--no-ff", "origin/main"],
21        cwd=WORKTREE_PATH,
22        capture_output=True,
23        text=True
24    )
25    
26    has_conflict = result.returncode != 0
27    
28    # Abort the test merge
29    subprocess.run(["git", "merge", "--abort"], cwd=WORKTREE_PATH, capture_output=True)
30    
31    return {
32        "ok": not has_conflict,
33        "conflicts": result.stdout if has_conflict else [],
34        "message": "Conflict detected with main" if has_conflict else "Clean merge possible"
35    }
36
37if __name__ == "__main__":
38    result = has_conflicts_with_main()
39    import json
40    print(json.dumps(result, indent=2))
41    sys.exit(0 if result["ok"] else 1)

Rollback and Compensation: When the Lease Expires

The lease contract must define what happens when things go wrong. Digital Thought Disruption’s control plane series defines the full loop: “execute, verify, rollback” [Source: https://digitalthoughtdisruption.com/2026/07/25/execute-verify-rollback-agent-actions/].

 1# Rollback section in lease contract
 2rollback:
 3  triggers:
 4    - condition: "heartbeat_expired"
 5      action: "revoke_lease_and_cleanup"
 6      cleanup:
 7        - "git worktree remove --force"
 8        - "delete_heartbeat_files"
 9        - "notify_owner"
10    - condition: "verification_failed"
11      action: "return_to_agent_with_feedback"
12      max_retries: 2
13      backoff_minutes: [15, 60]
14    - condition: "integration_conflict"
15      action: "create_conflict_resolution_task"
16      assign_to: "platform_team"
17    - condition: "max_duration_exceeded"
18      action: "force_checkpoint_and_extend_or_revoke"
19      checkpoint_command: "git commit -am 'WIP: lease checkpoint at $(date)'"
20      extend_hours: 2
21      max_extensions: 1

The key insight: the compensation action belongs in the lease contract, not in the agent code. The agent does the work. The contract defines what “work not done” looks like and how to fix it.

Compensation actions defined in contract, not agent code

Implementation Checklist: From Zero to Production

You don’t need all components on day one. Start here:

Week 1: Worktree Isolation

  • Audit current agent runs — identify all agents targeting the same repo
  • Create worktrees for each agent role (backend, frontend, migration, etc.)
  • Update agent wrappers to cd into their assigned worktree before running
  • Verify no staging pollution or branch conflicts in a test run

Week 2: Lease Contracts

  • Create /etc/agent-leases/ with one YAML per agent
  • Define: resource, max_duration, heartbeat_interval, ownership, proof requirements
  • Deploy heartbeat writer (systemd timer or agent wrapper integration)
  • Deploy dead man’s switch checker (runs every 2 minutes)

Week 3: Verification Probes

  • Write one verification probe for the highest-impact agent
  • Define expected files, test commands, static analysis rules
  • Deploy as a standalone check (not part of the agent run)
  • Add to lease contract’s proof.verification_probe

Week 4: Integration Gate + Rollback

  • Configure required reviews and checks in lease contract
  • Implement pre-merge conflict detection
  • Define rollback triggers and compensation actions
  • Run a game day: manually expire a lease, verify cleanup + owner notification
Common objections (and responses)

“This is overengineering for two agents.” If two agents on one checkout have ever caused a force-push recovery session, it’s not overengineering. The worktree pattern is 3 git commands. The lease contract is YAML. The heartbeat is 30 lines of Python. This is not Kubernetes.

“Our agents are polite — they use different branches.” Branches in the same worktree share the index. Staging pollution still happens. git commit captures everything staged. Worktrees give each agent its own index.

“We already have PR reviews.” PR reviews happen after the agent pushes. The lease contract prevents the corruption before it reaches the remote. The integration gate is the PR review, but with verified pre-conditions.

“Our agents are idempotent, so re-running is always safe.” Idempotency handles re-runs. It doesn’t handle silent corruption from staging pollution. If Agent A stages UserController.php and Agent B stages UserService.php, a single commit contains both. Idempotency doesn’t undo that.

“We don’t have time to add this to every agent.” Start with the agent pair that shares a repo. The pattern scales. You add leases incrementally.

What You Should Do Monday Morning

  1. Audit your agent fleet — List every coding agent (Claude Code, Codex, OpenCode, Hermes subagents, custom wrappers) that targets the same Git repository. For each pair, answer: “If they run simultaneously on the same checkout, what breaks?” If the answer includes “staging pollution” or “force-push recovery,” that pair needs worktree isolation this week.

  2. Create the first worktree pair — Pick the two highest-impact agents. Run git worktree add for each. Update their wrapper scripts to cd into the worktree. Run a test parallel execution. Verify zero staging pollution.

  3. Write the first lease contract/etc/agent-leases/agent-a.yaml with: resource (worktree path), max_duration_hours (4), heartbeat_interval_seconds (60), ownership (name + phone), proof (heartbeat file + verification probe path). Commit to git.

  4. Deploy the heartbeat writer — 30-line Python script, called by the agent wrapper every 60 seconds. Deploy the dead man’s switch checker — systemd timer every 2 minutes, alerts to Slack + PagerDuty.

  5. Write one verification probe — Choose the agent whose silent failure hurts most (API changes, migration scripts, schema edits). Write a 50-line script that queries actual side effects: files created, tests passing, static analysis clean, git status clean.

  6. Schedule a game day — Within two weeks, manually expire a lease heartbeat. Verify the cleanup runs, the owner gets alerted, the worktree is removed, and no orphan commits remain.

The goal is not perfect isolation. The goal is: when a parallel agent run produces silent corruption, a named human knows within minutes and has a documented path to fix it.

Further Reading


Related hubs: AI Agent Operations · Developer Tools · Start Here

See also: [[parallel-agent-shared-checkout]], [[zombie-task-detection]], [[cron-silent-failure-patterns-infra]], [[agent-edit-contract]], [[autonomous-agent-cron-pipelines]]