Skip to content

Latest commit

 

History

History
730 lines (485 loc) · 22.9 KB

File metadata and controls

730 lines (485 loc) · 22.9 KB

Concepts: Git Worktrees + Dev Containers for Parallel AI Development

This guide explains the technical concepts behind using Git worktrees and VS Code Dev Containers together for parallel development workflows - particularly when orchestrating multiple AI coding agents.

If you're already familiar with the problem and just want to set things up, see the setup guide instead.


Table of Contents


Part 1: The Foundation

The Parallel Development Challenge

Modern development teams are experimenting with AI coding agents for tasks like:

  • Trying two or three possible bug fixes in parallel
  • Testing a safe refactor versus a bolder rewrite
  • Comparing dependency upgrades on separate branches
  • Splitting several small tickets across multiple agents
  • Testing code changes against different language versions

The obvious first step is to give each agent its own branch. That prevents code edits from colliding - no merge conflicts while work is in progress.

But branch isolation alone isn't enough.

Why This Is Hard

Once those branches need different runtime environments, things get messy fast.

Consider this scenario:

  • Agent A is testing a fix on Python 3.10
  • Agent B is testing the same fix on Python 3.12
  • Agent C is trying a dependency upgrade that changes package resolution

If all of that happens in one local environment, you get exactly the kind of chaos you want to avoid:

  • Runtime conflicts: Two Python versions fighting for control
  • Package version conflicts: Incompatible dependency trees
  • Broken shells and polluted caches: Environment state that persists between switches
  • "Works on my machine" nonsense: Agents debugging setup instead of solving the actual task
  • Wasted time: AI agents spending cycles fighting the environment instead of writing code

At that point, branch isolation is not enough. You also need environment isolation.

That's where the combination of Git worktrees and Dev Containers becomes powerful:

Git worktrees give each agent its own branch and folder. Dev Containers give each branch its own environment.

Let's understand each piece.


Part 2: The Tools

Git Worktrees 101

What they are:

Git worktrees let you check out multiple branches of the same repository simultaneously in separate folders. Instead of switching branches in one directory (which changes files under your feet), you have multiple directories, each with a different branch checked out.

Example:

Instead of this traditional workflow:

git checkout main           # work on main
git checkout feature-a      # switch to feature-a (files change)
git checkout feature-b      # switch to feature-b (files change again)

You can do this:

git worktree add ../feature-a -b feature-a main
git worktree add ../feature-b -b feature-b main

Now you have:

my-project/           ← main branch
feature-a/            ← feature-a branch
feature-b/            ← feature-b branch

All three folders are backed by the same Git repository history. No need to clone the repo three times.

How they work:

Worktrees are "linked" to a main repository. The main repository (usually your original clone) contains the full .git directory with all Git metadata. Linked worktrees contain a .git file (not a directory) that points back to the main repository's metadata.

Here's what .git looks like in a linked worktree:

gitdir: /path/to/main-repo/.git/worktrees/feature-a

This pointer lets Git commands in the worktree access the shared repository history.

When to use them:

Worktrees are a great fit for:

  • AI agent workflows: Give each agent its own folder
  • Parallel human development: Work on multiple features simultaneously
  • Testing multiple ideas side by side: Compare approaches without constant switching
  • Keeping task boundaries explicit: Each folder = one branch = one task

Benefits compared to multiple clones:

  • Lighter: One repository history instead of three
  • Cleaner: Shared Git metadata stays in sync automatically
  • Easier to manage: git worktree list shows all active branches
  • Disk efficient: Shared objects and refs

What they DON'T solve:

Worktrees isolate the code checkout. They do NOT isolate the runtime environment.

If one branch needs a different Python version, different Node version, or different dependency tree, separate folders are not enough. You also need separate environments.

That's where Dev Containers come in.


Dev Containers 101

What they are:

Dev Containers are a VS Code feature that runs your development environment inside a Docker container. The container provides an isolated, reproducible environment with:

  • Specific language runtimes (Python 3.10 vs 3.12, Node 18 vs 20, etc.)
  • Installed packages and dependencies
  • Tooling and CLI utilities
  • VS Code extensions
  • Process isolation and sandboxing

How they work in VS Code:

  1. You define the environment in .devcontainer/devcontainer.json
  2. VS Code reads that configuration
  3. When you open the folder, VS Code offers to "Reopen in Container"
  4. VS Code builds/pulls a Docker image and starts a container
  5. Your code folder is mounted into the container
  6. VS Code connects to the containerized environment
  7. Your terminal, code editor, and extensions all run inside the container

From your perspective, it feels like working locally, but everything runs in an isolated container.

Example configuration:

{
	"name": "Python 3.12",
	"image": "mcr.microsoft.com/devcontainers/python:3.12",
	"customizations": {
		"vscode": {
			"extensions": ["ms-python.python"]
		}
	}
}

What they solve:

  • Reproducibility: "Works on my machine" → "Works in this container"
  • Isolation: No conflicts between projects
  • Onboarding: New team members get the right environment automatically
  • Consistency: CI/CD can use the same container image

Integration with worktrees:

This is where it gets interesting. Each worktree can be opened in its own container, which means each branch can carry its own:

  • Language version
  • Package set
  • Tooling stack
  • Editor extensions
  • Process sandbox

This is the real value proposition for AI-assisted workflows:

Git worktrees give each agent its own branch and folder. Dev Containers give each branch its own environment.

Perfect, right?

Not quite. There's a hidden compatibility issue.


Part 3: The Integration Challenge

Why They Don't Work Together By Default

Here's the problem: linked worktrees often break inside Dev Containers.

Let's understand why.

The Technical Breakdown

How linked worktrees store Git metadata:

As we learned earlier, a linked worktree contains a .git file (not a directory) that points back to the main repository's metadata:

gitdir: /Users/kunal/projects/my-project/.git/worktrees/feature-a

This path is absolute. Git reads this file and follows the path to find the repository's history, branches, refs, and objects.

How VS Code mounts folders into containers:

When you open a folder in a Dev Container, VS Code normally mounts only that folder into the container. By default, it mounts your workspace at /workspaces/your-folder-name.

The breakage:

Here's what happens when you open a linked worktree in a container:

  1. You open feature-a/ in VS Code
  2. VS Code mounts only feature-a/ into the container at /workspaces/feature-a
  3. Git reads .git inside the container: gitdir: /Users/kunal/projects/my-project/.git/worktrees/feature-a
  4. Git tries to access /Users/kunal/projects/my-project/.git/worktrees/feature-a
  5. That path doesn't exist inside the container (because my-project/ wasn't mounted)
  6. Git fails: fatal: not a git repository (or any of the parent directories): .git

The .git pointer is valid on your host machine, but invalid inside the container.

Visualization:

Host filesystem:
/Users/kunal/projects/
├── my-project/              ← Contains .git directory
│   └── .git/
│       └── worktrees/
│           └── feature-a/
└── feature-a/               ← Linked worktree
    └── .git  (points to ../my-project/.git/worktrees/feature-a)

Container filesystem (default mount):
/workspaces/
└── feature-a/               ← Only this is mounted
    └── .git  (points to /Users/kunal/projects/my-project/... → DOESN'T EXIST!)

Why this is a problem:

Without access to Git metadata, you can't:

  • Run git status, git log, git commit, or any Git command
  • Use VS Code's Git integration
  • Let AI agents commit changes or create branches
  • Verify what branch you're on

The worktree becomes a dead checkout.


Part 4: The Solution

The Shared Parent Pattern

The fix is conceptually simple: mount the parent directory so the .git pointer remains valid inside the container.

The required directory structure:

Instead of having worktrees anywhere, organize them under a shared parent directory:

my-project-worktrees/          ← Shared parent
├── my-project/                ← Main repository (contains .git/)
│   ├── .git/                  ← Full Git metadata
│   └── .devcontainer/         ← Container configuration
├── feature-a/                 ← Linked worktree
│   ├── .git                   ← Points to ../my-project/.git/worktrees/feature-a
│   └── .devcontainer/         ← Same container configuration
└── feature-b/                 ← Linked worktree
    ├── .git                   ← Points to ../my-project/.git/worktrees/feature-b
    └── .devcontainer/         ← Same container configuration

Why this works:

When you open feature-a/ in a container, the .devcontainer configuration mounts the entire parent directory (my-project-worktrees/) at the same absolute path inside the container.

Now the .git pointer can be resolved:

  • .git points to ../my-project/.git/worktrees/feature-a
  • That path exists inside the container (because the parent was mounted)
  • Git commands work normally

Visualization:

Container filesystem (with parent mount):
/Users/kunal/projects/my-project-worktrees/     ← Entire parent mounted
├── my-project/
│   └── .git/                                   ← Git metadata is accessible!
│       └── worktrees/
│           └── feature-a/
└── feature-a/
    └── .git  (points to ../my-project/... → NOW EXISTS!)

Security consideration:

Because the entire parent directory is mounted, all siblings become visible to the container. This means:

  • feature-a/ container can see feature-b/, my-project/, and any other worktrees
  • Use a dedicated parent directory that contains ONLY the main repository and its worktrees
  • Do NOT use a parent that contains unrelated projects, sensitive data, or private repositories

Think of it as creating a "worktree workspace" boundary.


The .devcontainer Configuration

Here's how the repository's .devcontainer/devcontainer.json implements this pattern:

1. Mount the parent directory at the same absolute path:

"mounts": [
  "source=${localWorkspaceFolder}/..,target=${localWorkspaceFolder}/..,type=bind,consistency=cached"
]

What this does:

  • ${localWorkspaceFolder}/.. is the parent directory on the host
  • It's mounted at the exact same path inside the container
  • This keeps the .git pointer valid (absolute paths match)

2. Configure Git's safe.directory:

"postCreateCommand": "container_parent=\"$(dirname '${containerWorkspaceFolder}')\" && mirrored_parent=\"$(dirname '${localWorkspaceFolder}')\" && git config --global --add safe.directory \"${container_parent}/*\" && git config --global --add safe.directory \"${mirrored_parent}/*\""

What this does:

Git has a security feature that prevents operations on repositories owned by different users. When you mount your host files into a container, ownership can look suspicious to Git.

The postCreateCommand runs after the container is created and adds two safe.directory patterns:

  1. /workspaces/* (the default VS Code mount location)
  2. The mirrored host parent path (e.g., /Users/kunal/projects/my-project-worktrees/*)

This tells Git: "I trust all repositories under these parent directories."

Why both patterns?

VS Code may mount the workspace at different paths depending on the configuration. Adding both ensures Git works regardless of which mount path is active.

3. Use a non-root user:

"remoteUser": "vscode"

This avoids common bind-mount permission issues. The vscode user inside the container can read and write files that belong to your host user.

Platform support:

This configuration officially targets:

  • macOS: Full support
  • Linux: Full support
  • Windows via WSL: Full support
  • Native Windows: May require custom mount strategy (path models differ)

Part 5: Practical Guide

When To Use This Pattern

This pattern is ideal when you need:

Parallel development on multiple branches

  • Multiple AI agents working simultaneously
  • Human developers context-switching between tasks
  • Testing multiple approaches side by side

Environment isolation between branches

  • Different language versions (Python 3.10 vs 3.12)
  • Different dependency sets (testing upgrades)
  • Different tooling requirements

Working Git commands in all environments

  • Agents need to commit, push, and create branches
  • VS Code's Git integration must work
  • CI/CD integration requires Git operations

Quick setup and teardown

  • Create worktrees with one command
  • Delete worktrees when done
  • No complex cleanup or environment pollution

When NOT To Use This Pattern

This pattern may be overkill or inappropriate when:

You only need one active branch at a time

  • Just use regular branch switching (git checkout)
  • Dev Containers alone are sufficient

You don't need environment isolation

  • All branches use the same dependencies
  • Worktrees alone are sufficient (no containers needed)

Your team doesn't use VS Code or Docker

  • This pattern is specifically for VS Code Dev Containers
  • Consider other solutions: separate clones, Docker Compose, Vagrant, etc.

You have security concerns about mounting siblings

  • All worktrees and the main repository are visible to each container
  • If isolation between worktrees is critical, use separate clones in separate containers

Your repository is extremely large

  • Mounting the parent directory may have performance implications
  • Test the pattern with your specific repository size and file count

Example Workflows

Example 1: Testing Python Version Compatibility

Scenario: You want to test a bug fix on both Python 3.10 and Python 3.12 simultaneously.

Setup:

  1. Create the shared parent and move your repo into it:

    mkdir my-project-worktrees
    mv my-project my-project-worktrees/
    cd my-project-worktrees/my-project
  2. Copy this repo's .devcontainer/ folder into your project

  3. Create two worktrees:

    git worktree add ../python-310-test -b python-310-test main
    git worktree add ../python-312-test -b python-312-test main
  4. Edit .devcontainer/devcontainer.json in each worktree:

    // python-310-test/.devcontainer/devcontainer.json
    {
      "name": "Python 3.10 Test",
      "image": "mcr.microsoft.com/devcontainers/python:3.10"
    }
    
    // python-312-test/.devcontainer/devcontainer.json
    {
      "name": "Python 3.12 Test",
      "image": "mcr.microsoft.com/devcontainers/python:3.12"
    }
  5. Open each worktree in separate VS Code windows

  6. VS Code offers to "Reopen in Container" for each

Result: Two VS Code windows, two containers, two Python versions, zero conflicts.


Example 2: Parallel AI Agent Development

Scenario: You have three AI agents tackling the same issue with different approaches.

Setup:

  1. Create three worktrees:

    cd my-project-worktrees/my-project
    git worktree add ../agent-1-safe-refactor -b agent-1-safe-refactor main
    git worktree add ../agent-2-bold-rewrite -b agent-2-bold-rewrite main
    git worktree add ../agent-3-minimal-fix -b agent-3-minimal-fix main
  2. Launch three AI agent sessions, each connected to a different worktree folder

  3. Each agent works independently:

    • Edits code in its own worktree
    • Commits changes to its own branch
    • Runs tests in its own container
    • No interference with other agents
  4. Review results:

    • Open all three worktrees in separate VS Code windows
    • Compare approaches side by side
    • Merge the winning solution
    • Delete the unused worktrees:
      git worktree remove agent-2-bold-rewrite
      git worktree remove agent-3-minimal-fix
      git branch -d agent-2-bold-rewrite agent-3-minimal-fix

Benefit: Agents develop in true isolation, no environment conflicts, easy cleanup.


Example 3: Dependency Upgrade Testing

Scenario: Test a major dependency upgrade while keeping stable branch running.

Setup:

  1. Create a worktree for the upgrade:

    cd my-project-worktrees/my-project
    git worktree add ../upgrade-test -b upgrade-test main
  2. In upgrade-test/:

    • Edit package.json / requirements.txt / Gemfile with new versions
    • Update .devcontainer/devcontainer.json if the base image needs to change
    • Install dependencies in the container
    • Run tests
  3. Meanwhile, the main worktree still works with stable dependencies

  4. If the upgrade succeeds:

    cd my-project
    git merge upgrade-test
    git worktree remove ../upgrade-test
    git branch -d upgrade-test
  5. If the upgrade fails:

    git worktree remove ../upgrade-test
    git branch -d upgrade-test

Benefit: Risk-free experimentation without breaking the stable environment.


Troubleshooting

Git commands fail with "fatal: not a git repository"

Symptoms:

$ git status
fatal: not a git repository (or any of the parent directories): .git

Likely causes:

  1. The parent directory isn't mounted
  2. The .git pointer references an inaccessible path

Solutions:

  1. Verify the directory structure:

    # Inside the container
    cat .git
    ls -la $(cat .git | sed 's/gitdir: //')

    The path should exist and be accessible.

  2. Check the mount in .devcontainer/devcontainer.json:

    "mounts": [
      "source=${localWorkspaceFolder}/..,target=${localWorkspaceFolder}/..,type=bind,consistency=cached"
    ]
  3. Rebuild the container:

    • Cmd/Ctrl+Shift+P → "Dev Containers: Rebuild Container"

Git commands fail with "detected dubious ownership"

Symptoms:

$ git status
fatal: detected dubious ownership in repository at '/workspaces/my-project'

Cause: Git's safe.directory protection is blocking operations.

Solutions:

  1. Verify the postCreateCommand ran:

    # Inside the container
    git config --global --get-all safe.directory

    You should see entries like /workspaces/* and your host parent path.

  2. Manually add safe.directory:

    git config --global --add safe.directory /workspaces/*
    git config --global --add safe.directory "$(dirname '${localWorkspaceFolder}')/*"
  3. Rebuild the container to re-run postCreateCommand


Container can't see sibling worktrees

Symptoms: You can't access files in ../other-worktree/ from inside a container.

Cause: The parent directory wasn't mounted, or the mount path is wrong.

Solutions:

  1. Check that all worktrees and the main repo are siblings under one parent
  2. Verify the mount in .devcontainer/devcontainer.json
  3. Rebuild the container

Performance issues with large repositories

Symptoms: Container startup is slow, file operations are sluggish.

Cause: Mounting large directory trees can have performance overhead, especially on macOS.

Solutions:

  1. Use consistency=cached in the mount (already default in this repo's config)
  2. Exclude large folders that don't need to be mounted (e.g., node_modules/, .git/objects/)
  3. Consider using Docker volumes for large static data
  4. On macOS, ensure you're using the latest Docker Desktop (file system performance has improved significantly)

Windows native paths cause mount issues

Symptoms: Containers fail to start or Git commands fail on native Windows (not WSL).

Cause: Windows path models (C:\...) differ from Unix-style paths. The mirrored parent mount may not work.

Solutions:

  1. Recommended: Use WSL (Windows Subsystem for Linux) for full compatibility
  2. Alternative: Customize the mount strategy for Windows:
    • Use a fixed container path instead of mirroring the host path
    • Adjust the .git pointers to use the fixed path
    • Update safe.directory configuration accordingly

This repository officially targets WSL for Windows users.


Summary

You've learned:

  • The problem: AI agents need branch isolation + environment isolation
  • Git worktrees: Multiple branch checkouts from one repository
  • Dev Containers: Isolated containerized development environments
  • The challenge: Linked worktrees break inside containers by default (.git pointer becomes unreachable)
  • The solution: Mount the shared parent directory to keep the .git pointer valid
  • The implementation: This repo's .devcontainer configuration with parent mount + safe.directory config
  • When to use it: Parallel development with environment isolation
  • When not to use it: Single-branch workflows or when security between worktrees matters

Ready to set it up? → Follow the setup guide to configure your repository.

Want to see it in action? → Run bash .devcontainer/smoke-test.sh to see an automated verification.

Have questions or issues? → Check the troubleshooting section above or open an issue in the repository.