Git - Worktree Hooks

git
worktrees
hooks
bash
just
direnv
Author

Neil Shephard

Published

July 25, 2026

Git worktrees are one of the lesser known and under-appreciated features of Git that I discovered a couple of years ago but didn’t fully adopt into my work flow until recently. In doing so I found there are some friction points to adopting Git Worktrees but as with most things there are ways to smooth over these. This post explains how to use Git hooks to manage setting up just configuration files and create virtual environments with uv and configure direnv to activate these.

The problem(s)

When working on Python projects I’ll use virtual environments to provide an isolated set of packages specific to the package being developed, typically Python. I used to use virtualenvwrapper to create and manage my environments but have recently been trying out uv where the source of the environment is created within the project (by default in the .venv/ directory). When using the traditional Git workflow of a single folder and switching branches this was quite convenient, as installing packages with [uv] pip install -e . meant that switching branches the files within the project are “editable” and by switching branches I also get the changes between branches reflected in the behaviour of the software.

However, with the switch to using Git Worktrees each branch lives in its own directory and it’s not possible to have a single virtual environment that you can easily switch between the development versions of the software that resides in each directory. Instead you have to create a virtual environment in each directory and activate it. For a long time I’ve used direnv to carry out the task of activating a given virtual directory when I cd into it (or load a project in Emacs thanks to the emacs-direnv package).

I’ve also started using the Just runner system to provide convenient short-cut commands for tasks I run regularly such as running different pre-commit hooks, installing the package itself, updating tests and so forth (see my justfile template for a growing list of examples).

The solution

Regular readers of this blog, if any exist, will know that I’m a big fan of pre-commit which is a framework for leveraging Git hooks. Hooks are not limited to run prior to commits though, there are a whole host of other stages at which they can run such as post-commit, pre-push and pre-pull. For the challenge at hand we can leverage the post-checkout hook to run some commands after we checkout a branch which is part of the steps carried out when creating a worktree.

The real trick though is doing so only on the first time we checkout a branch and I found this solution courtesy of a short snippet Using Git Hooks When Creating Worktrees which provides the framework/scaffolding which I’ve bulked out for my use case. The script…

  1. Sets the BASE_PATH to the directory two levels higher. This is because I use a convention of <username>/<issue-##>-<short-description> when naming branches and this carries over nicely to naming my worktrees.
  2. Checks to see if there is a main/.justfile, if not it creates one using echo to append the required commands to main/.justfile. This example only adds two runners, for more examples see my justfile template
  3. Checks for main/.envrc and if not creates one which will source .venv/bin/activate which is the path uv venv installs to by default.
  4. Checks if this is a new worktree, this is the “magic” I found in Mark Skelton’s article if so then…
  5. Copy main/.justfile and main/.envrc from main/ to the current directory.
  6. If there is no virtual environment directory (.venv/) then…
  7. Create a virtual environment using uv venv.
  8. Run uv sync to install the project dependencies.
  9. Install the current package in editable form using the just pip_install command to ensure all dev group dependencies are installed.
  10. Allow direnv in the current directory.
  11. If pre-commit hooks are not present then initialise pre-commit.
#!/bin/bash
#
# Inspiration from : https://mskelton.dev/bytes/20230906143340
#
if [[ "$1" == "0000000000000000000000000000000000000000" ]]; then
  # Base directory for bare repository
  BASE_PATH="$(git rev-parse --git-dir)/../../"
  MAIN_PATH="${BASE_PATH}/main"
  # If main/.justfile doesn't exist create it
  if [ ! -f "${MAIN_PATH}/.justfile" ]; then
    echo "No .justfile creating a basic skeleton"
    printf "
@default:
    @just --list
" > ${MAIN_PATH}/.justfile
    printf "
# uv pip install with group dev dependencies
pip_install:
    uv pip install --editable . --group dev
" >> ${MAIN_PATH}/.justfile
  fi
  # If main/envrc doesn't exist create it
  if [ ! -f "${MAIN_PATH}/.envrc" ]; then
    echo "No .envrc creating a basic skeleton"
    printf "#!/bin/bash
source .venv/bin/activate
export PATH=${VIRTUAL_ENV}/bin:${PATH}" > ${MAIN_PATH}/.envrc
  fi
  # Copy main/{envrc,justfiles} to current directory
  FILES=(".justfile" ".envrc")
  for FILE in "${FILES[@]}"; do
    echo "Copying main/'${FILE}' to $(pwd)/${FILE}"
    cp "${MAIN_PATH}/${FILE}" "$(pwd)/${FILE}"
  done
  # Create a virtual environment using uv and install the current Python package
  if [ ! -d ".venv/" ]; then
    uv venv
    uv sync
    just pip_install
    # Allow direnv so venv can be activated
    if [ -f ".envrc" ]; then
      direnv allow
    fi
  fi
  # Enable pre-commit if not already enabled
  if [ ! -f "${BASE_PATH}/hooks/pre-commit" ]; then
    pre-commit install
  fi
fi

With Worktrees you typically, but not always, will have cloned a bare repository which contains all the files that normally live in a repositories ~/.git/ directory at the top level. The above should code should be placed in the hooks/post-checkout file and will then run after every checkout.

Gotchas

There are a few gotchas to be aware of if you want to use the above script directly. Setting the BASE_PATH variable above assumes that you are creating your worktrees in a nested directory. I alluded to this above when I wrote that I use <username>/<issue-##>-<short-description> for my branch names and use the same convention for worktree names. If you only use <branch-name> for the worktree directory then you will have to modify the BASE_PATH and remove one of the ../ as you will only need to go up one directory level.

If you don’t use direnv then you should remove those sections that create/copy .envrc and activate the directory. Similarly if you don’t use pre-commit you should remove the lines that activate that.

Side-stepping creation

If you already have either .justfile or .envrc files that you use in a given repository and don’t want to create them from scratch then you do not need to worry because if they are in the main branch they will be used.

Summary

There is a small amount of friction to switching to a Git Worktrees based workflow but they greatly reduce the friction involved in working on multiple branches simultaneously as it reduces to cd <worktree_path>. With a post-checkout hook its easy to create just and direnv configuration files on initial creation of a worktree and branch as well as create and activate uv based virtual environments, allow direnv and initialise the pre-commit framework in the new directory.

[uv_env]

No matching items

Reuse

Citation

BibTeX citation:
@online{shephard2026,
  author = {Shephard, Neil},
  title = {Git - {Worktree} {Hooks}},
  date = {2026-07-25},
  url = {https://blog.nshephard.dev/posts/git-worktree-hooks/},
  langid = {en}
}
For attribution, please cite this work as:
Shephard, Neil. 2026. “Git - Worktree Hooks.” July 25, 2026. https://blog.nshephard.dev/posts/git-worktree-hooks/.