Skip to content

Zoom-in: Git Worktree

Karify98·
Cover Image for Zoom-in: Git Worktree

Switching branches to fix a hotfix when your current working directory is dirty is a common developer pain point. The typical options are to create a messy temporary commit (with messages like "wip" or "temp") or use git stash to shelve unfinished changes — which are then easily forgotten or prone to conflicts when popped.

Fortunately, there is a much cleaner way: creating a separate, parallel working directory pointing to the same repository using git worktree. This is especially powerful in the current era of autonomous AI coding agents (such as Claude Code and Antigravity) that are deeply integrated into daily workflows. A developer can easily assign a build task or testing suite to an agent on a separate branch inside an independent worktree running in the background, while continuing to code in the main directory without any IDE conflicts or interruptions.

A concrete example: you're mid-way through feature A in the myapp directory (on main), but you need Claude Code to run the full test suite for a different pull request on feature/checkout-flow. Instead of stashing feature A, just spin up a separate worktree for the agent:

git worktree add ../myapp-agent-review feature/checkout-flow
cd ../myapp-agent-review && claude-code run-tests

The agent reads and writes files freely inside ../myapp-agent-review without touching the staging area or HEAD of the main myapp directory. Once it's done, clean up with git worktree remove ../myapp-agent-review.

Zooming in on that.


Layer 1 — Independent Working Tree and the Flat .git File

A standard Git repository has only one working tree tied to a single .git metadata folder. To work on multiple branches simultaneously without disturbing your current folder, Git needs a mechanism to isolate the source code in another directory while maintaining a connection to the central database.

When running git worktree add ../hotfix-branch hotfix, Git creates a new directory at ../hotfix-branch and checks out the hotfix branch there. This new directory is a completely independent working tree.

Instead of copying the entire, massive .git/ folder, Git creates a small, flat text file named .git in the new directory. This file contains a single line pointing to the actual metadata directory housed inside the main repository:

gitdir: /path/to/main-repo/.git/worktrees/hotfix-branch

This flat .git file acts as a special symlink for Git, telling any git command run inside this directory where to find its true database.

graph LR
    WT["📁 Child Working Tree\n(hotfix-branch)"] -->|".git file"| M["📂 Main Repo metadata\n(.git/worktrees/hotfix-branch)"]
    style WT fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
    style M fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
Remaining issue: The flat .git file helps the sub-directory find its metadata, but how do staging operations (add, commit) and branch switching inside the sub-directory avoid overwriting the state (index, HEAD) of the main repository?

Layer 2 — Isolated Metadata in .git/worktrees/

Every working tree must have its own staging area (the index file), active branch (HEAD), and temporary workflow state (such as BISECT_LOG or rebase state) to prevent direct conflicts with the main working directory.

To solve this, Git automatically creates a subdirectory inside the main repository's .git folder at .git/worktrees/<name>/.

.git/
└── worktrees/
    └── hotfix-branch/
        ├── HEAD       # Points to the local 'hotfix' branch
        ├── index      # The worktree's isolated staging area
        ├── gitdir     # Points back to the child worktree's .git file
        └── commondir  # Points to shared folders in the main repo

When running git status or git add in the sub-directory, Git reads and writes to the index and HEAD files located under .git/worktrees/hotfix-branch/. This isolates the staging area of each working tree. You can stage and commit files in the child directory without affecting the staging area of your main directory.

graph TD
    WT_Main["📁 Main Working Tree"] -->|uses| M_HEAD["Main HEAD"]
    WT_Child["📁 Child Working Tree"] -->|uses| C_HEAD["Child HEAD\n(.git/worktrees/child/HEAD)"]
    style WT_Main fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
    style WT_Child fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
    style M_HEAD fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
    style C_HEAD fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
Remaining issue: Although HEAD and index are isolated, the child directories still need access to the shared database of commits, blobs, refs, and repository configurations without duplicating gigabytes of storage.

Layer 3 — Shared Database via Commondir

Commit history (objects) and branch references (refs) represent the shared state of the repository. If each worktree kept its own copy of these databases, spinning up a worktree would be just as slow and storage-intensive as cloning a fresh copy of the repository.

Git handles this shared access using the commondir file located inside each worktree's metadata folder. This file contains a single path:

../..

This path instructs Git to route requests for shared folders like objects/, refs/, info/, hooks/, and the repository config file back to the root .git folder (two levels up from .git/worktrees/hotfix-branch/).

Thanks to this routing mechanism, any commit created in a worktree is immediately written to the main repository's database. When returning to the main working tree, that commit is already available without requiring any sync steps. To ensure history integrity, Git also prevents checking out the same branch on two active working trees simultaneously.

graph LR
    subgraph "Main Repo (.git/)"
        CommonDB["🗃️ Common Database\n(objects, refs)"]
    end
    
    subgraph "Worktree child metadata"
        C_Meta["📁 .git/worktrees/child/"] -->|commondir| CommonDB
    end

    style CommonDB fill:#1a3a2a,stroke:#22c55e,color:#86efac
    style C_Meta fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d

Full picture

Here is a high-level view showing how Git coordinates these parts to keep multiple working trees running in parallel on a single database:

graph TD
    subgraph "Working Trees (Source Directories)"
        MainWT["📁 Main Project Folder\n(/projects/my-app/)"]
        ChildWT["📁 Child Worktree Folder\n(/projects/my-app-hotfix/)"]
    end

    subgraph "Main Repository (.git/)"
        MainMeta["📂 Main .git/ Folder"]
        CommonDB["🗃️ Shared Database\n(objects/, refs/, config)"]
        
        subgraph "Worktrees Directory"
            ChildMeta["📂 .git/worktrees/my-app-hotfix/"]
            ChildHEAD["Local HEAD"]
            ChildIndex["Local index"]
        end
    end

    MainWT -->|contains| MainMeta
    ChildWT -->|contains flat .git file| ChildMeta
    ChildMeta -->|commondir| CommonDB
    MainMeta -->|contains| CommonDB
    ChildMeta -->|contains| ChildHEAD
    ChildMeta -->|contains| ChildIndex

    style MainWT fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
    style ChildWT fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
    style MainMeta fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
    style ChildMeta fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
    style CommonDB fill:#1a3a2a,stroke:#22c55e,color:#86efac

Takeaway

At its core, git worktree is not magic, but a clean exercise in data decoupling. By splitting runtime working states (HEAD, index) into local trees and sharing the persistent storage (objects, refs) via commondir, Git provides high-efficiency multitasking.

Understanding this design gives you the confidence to clean up broken worktrees (simply delete the folder and run git worktree prune) or manually repair paths in .git/worktrees if you ever move your project directories. A solid grasp of database design patterns always brings peace of mind when debugging complex setups.

Related Posts