If you regularly juggle multiple branches, you know the pain of stashing changes, checking out a new branch, and waiting for your IDE to re-index the world. Git worktrees solve this by allowing you to check out multiple branches simultaneously in separate directories, all backed by a single local repository.
However, the default worktree experience can lead to a messy directory structure and confused IDEs. Here is the persistent workflow I use to keep my workspaces organized, IDEs happy, and context-switching instantaneous.
Setting Up the Foundation: The Bare Clone
To prevent having a "main" working directory that acts differently from your other worktrees, the best approach is to start with a bare clone. A bare clone contains the repository data but no working files.
- Initialize a bare repository in your desired project directory:
git init --bare .git - Add your remote origin:
git remote add origin REMOTE_URL - Configure fetching to map all remote branches properly (since bare repos don't do this by default):
git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*' - Fetch the repository data:
git fetch
Handling Brand New Repositories
Git worktrees require at least one valid branch to exist. If you are initializing a brand-new repository without any remotes or branches, the standard worktree creation commands will fail.
To fix this, you can simulate an initial empty commit to establish a main branch without touching the working tree. Run the setup steps above, and then execute this command:
git update-ref refs/heads/main $(git commit-tree $(git write-tree) -m "Initial commit")
Enabling Relative Paths
By default, Git links worktrees to the bare repository using absolute paths. If you ever move or rename the parent directory, these internal links will break, and your worktrees will become inaccessible. To prevent this, it is highly recommended to configure Git to use relative paths globally (available in Git 2.47 and later):
git config --global worktree.useRelativePaths true
The Persistent Worktree Strategy
With the bare repository set up, you can start creating worktrees using commands like git worktree add ./primary feature/blah or git worktree add ./primary -b feature/blah main. However, constantly spinning up and deleting worktrees creates problems.
Many IDEs tie their workspace configuration, indexing data, and local history to the directory name. If you delete and recreate directories for every branch, you lose that context. Furthermore, generic names like "primary" or "feature" become completely unreadable when you have multiple workspaces open across different repositories.
My solution is to use three permanent worktrees, explicitly prefixed with the repository name:
- REPO_NAME.0: The reference worktree. Used for code reviews, reading the main branch, or referencing upstream changes without disturbing active work.
- REPO_NAME.1: The primary worktree. Dedicated to active feature development or major bug fixes.
- REPO_NAME.2: The secondary worktree. Spun up for minor hotfixes, quick experiments, or isolated tasks that interrupt primary work.
Because these directories are permanent, IDE metadata is perfectly preserved. When you finish a task, you simply checkout a different branch inside the existing REPO_NAME.1 directory, rather than deleting the worktree itself.
This structure also leaves room for automated tools like Copilot to spin up and tear down their own ad-hoc worktrees in the background without interfering with your primary workspaces.
Final Directory Structure
Following this approach, your project folder will look clean, organized, and strictly compartmentalized:
REPO_NAME/
├── .git/ (The bare repository data)
├── REPO_NAME.0/ (Reference)
├── REPO_NAME.1/ (Primary work)
├── REPO_NAME.2/ (Minor tasks)
└── additional worktrees/ (Optional ad-hoc tool worktrees)
By treating worktrees as persistent slots rather than ephemeral branch folders, you get all the benefits of parallel branch checkouts with none of the IDE friction.