DEV Community

Mirek Hovorka
Mirek Hovorka

Posted on Originally published at mirekhovorka.cz

Claude Code and git worktrees: two sessions, one repo

Parallel Claude Code sessions only started to make sense to me once I stopped running them on the same checkout. Before that, for example, one session would be writing tests for a blog build while another was editing the CSS at the same time. But both were working on the same files. The first one would make a change and commit it, and the second one would carry on as if nothing had happened. Then, in the evening, instead of checking the results, I'd have to figure out which session each change actually came from. And it was completely unnecessary. Git has had a solution for this for years — worktrees: each session gets its own directory and its own branch, but they still work on the same repository. This way, the files don't overwrite each other, and both can commit independently of one another.

(Everything here runs locally on my machine. If I want to completely separate the task from the computer, I use a different approach — I describe it in the article about Claude Code in the cloud. And if you need a refresher on Git commands, I have a separate cheat sheet.)

How worktrees work

You can think of a Git worktree as another working directory connected to the same repository. It shares the history and remote with other worktrees, but its own working files and branch are separate. For details, see the Git documentation. One thing is essential for Claude Code: two sessions can run on a single repository, each in its own directory, and one cannot access the other's files.

When it pays off

For me, two things are decisive.

First: tasks must truly be able to be done independently of one another. One session can write tests while another works on CSS. One can refactor code while another updates the documentation. One can fix a bug while another prepares a new feature. But as soon as the second session needs the results from the first, it doesn't make sense. Instead of saving time, I'm just adding another branch of work that I have to keep track of.

Second: The number of sessions isn't limited by the machine, but by how many outputs I can keep up with. I can handle two sessions. With three, it's a mess: I just end up switching between contexts, rushing through reviews, and doing more harm than good. As a result, I just end up with a growing pile of half-read diffs that I have to go back to one by one.

Step by step

I'll start with one flag:

claude --worktree testy
Enter fullscreen mode Exit fullscreen mode

Claude Code creates a worktree at .claude/worktrees/testy/, creates a branch named worktree-testy, and starts the session within that branch. In a second terminal, I can open another session with a different name in the same way, and both sessions then run concurrently. If I don't specify a name, Claude generates one on its own, something like dark-chocolate-cookie. The shortcut is -w.

I recommend setting the following right away:

  • .gitignore: Add .claude/worktrees/ to .gitignore. Otherwise, the contents of the worktrees will appear in git status of the main checkout.
  • Gitignored files like .env: a new worktree is a clean checkout, so they are not there. If you need them in individual worktrees, create .worktreeinclude in the root directory. The syntax works similarly to .gitignore, except that here you specify the files to be copied into each new worktree.
  • The existing branch: --worktree branches from the default branch by default. However, if I need to run Claude on a specific branch I'm currently working on, I'll create a worktree manually using git and run Claude from within it:
git worktree add ../oprava-formulare fix-formular
cd ../oprava-formulare
claude
Enter fullscreen mode Exit fullscreen mode
  • Cleanup: After the interactive session is complete, Claude takes care of cleanup. If the worktree is empty, it deletes it automatically. If there is unfinished work left in it, it asks if I want to keep it. I can check the current status at any time via git worktree list and remove an unnecessary worktree using git worktree remove.

On Mac and Windows, the principle and set of commands are the same. On Windows, you'll just see paths with backslashes, such as .claude\worktrees\testy. And one thing that surprised me the first time I cleaned up: if there's an NTFS junction or a symlink inside the worktree pointing to a directory located elsewhere, deleting the worktree will only remove the link itself. The target directory remains in place. That's how it's supposed to work, so don't be surprised if the "folder is still there."

Traps and guardrails

  • Parallel sessions do not mean parallel subscriptions. They all draw from the same standard five-hour window and the same weekly limit. The more I run at the same time, the faster they use up the shared quota. This saves me time at the keyboard, not tokens. So running three large tasks in parallel when I'm nearing the end of the window doesn't really pay off for me — I'll easily end up with several sessions in progress at once, and when the window runs out, they'll all stop.
  • Every worktree starts as a new, clean checkout. That means no node_modules, no virtualenv, and no existing build cache. So I reinstall the dependencies for each new worktree. For a smaller project, this is a minor issue, but with a large monorepo, the cost of maintaining additional parallel environments can add up enough that I have to think carefully about the number of worktrees I use. .worktreeinclude helps with smaller configuration files, but of course, it won't install dependencies for me.
  • The new worktree doesn't see my work in progress locally. I ran into this when I first used it: the session told me the file didn't exist. Well, of course — it wasn't in its checkout yet. If I want the new session to be based on my current work, I have to commit the changes first. And to ensure the worktree is created from the commit I'm currently working on (including changes I haven't pushed yet), rather than from the default branch on the remote, I set worktree.baseRef to "head" in my settings.
  • Non-interactive runs via -p behave differently. If I run claude -p --worktree ..., the final interactive cleanup does not occur, and the created worktree remains active. Therefore, from time to time, I go to git worktree list and remove unnecessary items using git worktree remove. If Git reports that the worktree is locked, I first run git worktree unlock.

Conclusion

For me, worktrees are one of the easiest ways to get usable parallel work out of Claude Code. I don't need any additional infrastructure or complicated setup — all it takes is one flag, and each session has its own separate working files and branch.

Rules that have worked well for me: I only run tasks in parallel that aren't dependent on one another; I limit the number of concurrent sessions based on how many results I can thoroughly check; I use .claude/worktrees/ in .gitignore; and I use .worktreeinclude to transfer the necessary local configurations to new worktrees. Most importantly, I keep in mind that all sessions draw from the same subscription limit — the more there are, the faster I burn through tokens.

If a task doesn't require my computer at all — if it's small, clearly defined, and easy to set up — I prefer to send it straight to a cloud session. There, I don't have to worry about isolation or cleanup afterward, because I get a fresh sandbox every time. I mainly use worktrees when I want to keep my work locally and maintain continuous control over it.


Originally published at mirekhovorka.cz.

Top comments (9)

Collapse
 
reidmarlow profile image
Reid Marlow

Worktrees solve the dirty working directory race condition cleanly. The tricky part I ran into when doing this was shared local state outside Git, mainly untracked environment files that do not replicate automatically and port collisions when both sessions try to boot local test runners. Setting up a quick post-checkout hook to seed environment configs and assign isolated test ports made parallel worktree sessions significantly more reliable.

Collapse
 
mirekhovorka profile image
Mirek Hovorka • Edited

Good point about the post-checkout hook. So far, I’ve solved it more simply, only the dev server needs ports and env, and it runs in a single session. I assign tasks that don’t require a server to the second one: content, builds, or tests. For a solo project, this is completely sufficient without any additional configuration. If I ever needed to run both servers in parallel, this is exactly the hook I’d use.

Collapse
 
bert_programmer profile image
Bert Shim

The thread already has the general shape of this, but the one that cost us wasn't remappable. Two sessions on the same machine were both reaching for a single browser extension attached to that machine. There's no port to reassign and no path to seed, and I couldn't find a hook that helps. They take turns, or one of them drops mid-action, and scheduling around it didn't hold because neither session knows when the other is about to grab it.

We ended up giving each session its own physical machine. Heavier than a post-checkout hook, and I'd rather not have needed it.

Collapse
 
mirekhovorka profile image
Mirek Hovorka

I know this from personal experience - for me, the problem area is the browser via DevTools MCP. Both the extension and the browser are machine-level singletons, so there’s nothing to remap there. I handle this purely through workflow organization: only one session communicates with the browser at a time, while the other receives tasks that don’t require it. It works well for one person. In a team, this model would be too fragile, so I understand why you ultimately ended up using a second machine.

Collapse
 
bert_programmer profile image
Bert Shim

Serializing by hand held for us too, right up until it didn't, and the failure was quiet. A person who steps on your browser session notices something odd and says so. An agent doesn't. It takes a generic disconnect, retries, and carries on.

That asymmetry is most of why the second machine won. The discipline isn't the hard part. Nothing tells you when it slipped.

Collapse
 
vinhnguyenthanhdn profile image
Vinh Nguyen

Worktrees isolate the checkout and the branch, but not what the two sessions reach outside it: a dev server or headless browser on a fixed port, a lock file at an absolute path, hooks and settings under the home directory. Two sessions with cleanly separate diffs still serialize or overwrite each other there, and the symptom looks nothing like a git conflict; it is a port already in use, or one session's hook rewriting a file the other just read. The cheap check before trusting parallelism is to run the same task in two worktrees at the same time, since anything that binds a port or writes a fixed path fails on the second run immediately. I keep a lock keyed on the port for exactly that reason.

Collapse
 
mirekhovorka profile image
Mirek Hovorka • Edited

That test running the same task in parallel across two worktrees is clever: it’s inexpensive and quickly reveals what’s bound to a specific port, path, or other shared state. For a solo project, I’ve found that the simplest way to achieve isolation so far is to divide the work so that sessions don’t compete with each other in the surrounding environment at all. However, one shared resource always remains: the quota. Once the usage quota is exhausted, all sessions stop and at that point, no lock or worktree can help.

Collapse
 
crdtcto profile image
Kane Lim

This is exactly the kind of practical constraint that becomes obvious only after running multiple agent sessions against a real codebase.

The key insight for me is that parallelism needs isolation at the filesystem and Git-state level, not just at the process level. Running multiple Claude Code sessions against the same checkout creates an implicit race condition: each agent has its own context, but they share mutable state. Worktrees remove that ambiguity while preserving the benefits of a single repository history.

I also strongly agree with your point that the real bottleneck isn't the number of sessions you can launch it's the number of independent outputs you can reliably review. Increasing agent concurrency without increasing review capacity can turn productivity gains into integration debt very quickly.

One additional guardrail I’d consider for larger teams is treating each worktree as an isolated execution unit with explicit ownership and lifecycle metadata: task ID, base commit, expected scope, test command, and cleanup status. That makes it much easier to automate validation and safely merge agent-generated changes.

There’s also an interesting next step here: once worktree isolation, automated tests, linting, and deterministic checks are combined, an orchestration layer can assign independent tasks to agents, validate each result, and only surface changes that pass predefined gates for human review.

That moves the workflow from “multiple Claude sessions” → “controlled parallel software delivery.”

Really useful write-up. Git worktrees are old technology, but combining them with coding agents makes their value much more apparent.

I’m part of a small Canada-based remote development team working on software development and AI automation, and I’m always interested in connecting with developers who are experimenting seriously with agentic workflows.

Collapse
 
mirekhovorka profile image
Mirek Hovorka

Thanks! That’s exactly what I ran into as well, I can run multiple parallel sessions without any problems, but the review process is still a bottleneck because I have to go through the outputs myself. Automated gates and orchestration make sense as the next step, but for now, I’m fine with keeping the entire process under manual control.