git worktree gotchas

Table of Contents

A couple of years ago I wrote 4 Strategies for Context Switching in Git, and a lot of the reaction I got was basically “that’s cool — probably not gonna use it”. Something has changed since then, and git worktrees are no longer a niche tool, but they still come with some gotchas.

"Larix decidua Aletschwald" by Sciadopitys is licensed under CC BY-SA 2.0 .

#Some terms

To make this a little less confusing, let’s define some terms around worktrees. What you may not realize, is that every git checkout is a worktree:

$ git worktree list

/home/olaf/github/oalders/www-olafalders-dot-com 6d11097 [main]

/home/olaf/.worktree/www-olafalders-dot-com/2026-05-14/dependabot-cooldown e22309a [dependabot-cooldown]

/home/olaf/.worktree/www-olafalders-dot-com/2026-09-15/gh-49 0858e51 [notes-on-git-worktree-and-coding-agents]

That first result is my main checkout — the dir that I cloned from GitHub. The next two are worktrees which I created. When we are talking about the main checkout, we’ll call it the “main working tree”. The other worktrees will be the “linked worktrees”.

#The root cause

All of these gotchas are rooted in one fact. In a linked worktree, .git is a

pointer file rather than a directory. Once you wrap your head around this, it

all makes a lot more sense.

#Finding the root of your repository

Sometimes you have some clever hack that decides whether or not you are in the

root of a git repository. Maybe that takes the form of code that traverses

up from the current directory looking for a directory named .git. That works

in the main working tree, but if you try this in a linked worktree, you’re in

for a surprise.

$ pwd

/home/olaf/github/oalders/www-olafalders-dot-com

$ file .git

.git: directory

$ cd /home/olaf/.worktree/www-olafalders-dot-com/2026-09-15/gh-49

$ file .git

.git: ASCII text

$ cat .git

gitdir: /home/olaf/github/oalders/www-olafalders-dot-com/.git/worktrees/gh-49

In the main working tree, .git is a directory with a lot of interesting

stuff in it. In a linked worktree, .git is a

file, the contents of which are

(of course?) the path to the main working tree. If you find yourself inside a

linked worktree and your logic is looking for a dir called .git, it’s going to

motor right past the top of your worktree. Now, if your linked worktree is a

subdirectory of the main working tree, it’ll stop there (in the main working

tree — not the linked worktree), which may not be what you wanted. It’ll also be confusing,

because things might look a lot like your linked worktree, but they won’t have your

changes.

Cue head scratching.

For this reason, I like to keep my linked worktrees in an entirely different path. It means I won’t accidentally end up totally confused and in the wrong place if I keep going up, up, up.

If you’d rather not roll your own solution and you don’t mind calling out to git, we’ve got you covered:

$ git rev-parse --show-toplevel

/home/olaf/.worktree/www-olafalders-dot-com/2026-09-15/gh-49

This command will do the right thing whether you are in a linked worktree or not.

#Finding the main working tree

Finding the main working tree is essentially just a different flavour of what we just discussed.

Maybe you have a gitignored .env or secrets file in the main working tree. To

find it from either the main working tree or a linked worktree, try this

one neat trick that sysadmins don’t want you to know,

--git-common-dir.

$ git rev-parse --git-common-dir

/home/olaf/github/oalders/www-olafalders-dot-com/.git

Note that this path is not directly to the main working tree, but to its

.git folder. Remember to chdir up one more level. In bash that might look like:

$ dirname $(git rev-parse --git-common-dir)

/home/olaf/github/oalders/www-olafalders-dot-com

#git hooks in a sandboxed linked worktree

Let’s say you’re on top of the latest developments and you keep your linked worktree

in a sandbox. You might use nono,

Docker, Landlock on Linux or

Seatbelt on macOS (both of which nono wraps) or

some other solution. (A common use case for sandboxing would be to ensure that your LLM agent cannot

easily go rogue). How does

that work with git hooks? It depends. In

my case, I restrict the sandbox to my linked worktree and some other resources that it

may need, but I don’t allow access to most dirs in the main working tree because I

like to keep them as separate as possible. That means my linked worktree cannot read

or write the .git/hooks directory.

This complicates things, because git hooks are a shared, common resource between the main working tree and the linked worktrees.

$ pwd

/home/olaf/.worktree/www-olafalders-dot-com/2026-09-15/gh-49

$ git rev-parse --git-path hooks

/home/olaf/github/oalders/www-olafalders-dot-com/.git/hooks

Oof. That’s in the main working tree, which is walled off from my sandbox. My linked worktree cannot read or write that directory. This is a problem.

The modus operandi in your pre-worktree days was to symlink your pre-commit

hook to the .git/hooks directory (.git/hooks/pre-commit), which was not a

checked-in directory. Now what you want to do is keep your hooks in a directory

that is checked in and then configure git to use a relative path to find

them.

mkdir .githooks

# Make sure this is a relative path.

git config core.hooksPath .githooks

Now your pre-commit hook is tracked by git and lives in .githooks/pre-commit. git chdir’s into each

worktree’s own root before it runs a hook, so a relative core.hooksPath

ensures that the path resolves to a directory inside the same worktree,

inside the sandbox and not to a directory you may not have the necessary

permissions for.

The

core.hooksPath

setting lives in the shared config (your common .git) of your repository,

which means that setting git config once is enough to cover every worktree

you currently have or will have in future on the machine you’re working on.

The nice thing about this is that your hooks are now also immediately available in a

new checkout, provided you run git config prior to using it. This is

slightly easier than having to create symlinks for your hook files.

#Wrap-up

All of the sharp edges we discussed are the result of .git in a linked

worktree not being what you first might expect. So if you remember one thing

from today, let it be “.git can be a directory or it can be a pointer and which

one you get depends on where you are”.

To summarize our findings:

These are not the only git worktree quirks, but this is a good start. Maybe we’ll get to more gotchas in a different post.

Related posts: