---
title: "My git-worktree setup using worktrunk and caddy"
description: "Deep in a feature but need to fix a quick bug? See how I use git-worktree, Worktrunk, and a dynamic Caddyfile to seamlessly switch contexts and environments."
source: "https://xata.io/blog/my-git-worktree-setup-using-worktrunk-and-caddy"
---

# My git-worktree setup using worktrunk and caddy

Imagine you are building a large feature and find yourself deep in the context of a problem when you receive a request to solve a minor bug.

While you know how to fix this bug instantly, this is still a major context switch as you have to

- stash or temporarily commit your current changes
- open a new branch
- make the “minor” change
- re-create the context

[git-worktree](https://git-scm.com/docs/git-worktree) aims to address this issue by allowing you to work on multiple branches in the same repository.

However, it only provides the ability to quickly switch a branch without losing context. To have a working project where you can test changes end to end, you need some additional things like

- the environment variables, as the .env files are usually git ignored, they aren’t copied over in the git-worktree folder.
- ability to run multiple versions of the codebase (if it is a web server for example)

## **Enter my setup**

### **Worktrunk**

Although, you can use git-worktree commands directly, [worktrunk](https://worktrunk.dev/worktrunk/) provides nicer APIs to achieve the same goals (copying .env files over, less typing).

It is a very thin wrapper, here is an example:

```
$ wt switch -c feat1
# gets translate to
# git worktree add -b feat ../repo.feat1 && \
# cd ../repo.feat1
```

See their [docs](https://worktrunk.dev/worktrunk/) for other examples but you get the idea.

### **Copying .env files**

This is very project specific so it would vary from project to project but usually my projects are powered by .env files. You can run a git hook using [lefthook](https://lefthook.dev/) to generate an env file (by getting the relevant secrets from 1password or locally encrypted env files using [dotenvx](https://dotenvx.com/) for example). In fact, this is what I was doing until a week or so ago before I ran into worktrunk.

Worktrunk has a concept of `.worktreeinclude` file which is a `.gitignore` style file for copying files over, here is what it looks like for a web project that I work on:

```
$ cat .worktreeinclude
.env
.dev.vars
```

Just this file is not enough, you need a way to run worktrunk hooks on each work tree, here is my config currently:

```
$ cat .config/wt.toml
[post-create]
install = "pnpm install"
env = "wt step copy-ignored"
```

So essentially, when a git-worktree is created via `worktrunk`, we install the dependencies and copy over the .env files (yes this works for nested files too). With this, you are already ready to work on the project and run a dev server (but what about port clashes?).

## Clash of the dev servers

You are usually testing things end to end in a web server, I usually like to even run projects with https on a custom URL. To power this, I use [caddy](https://caddyserver.com/), here is what my Caddyfile usually looks.

```
$ cat Caddyfile
trackfootball.localhost {
  reverse_proxy localhost:5173
}
```

But when using multiple git-worktrees, I didn’t want to stop the dev server so port 5173 (vite default) gets free. I wanted to create a predictable pattern so the next available port is used (and is addressable with https).

Luckily, `vite` does half the job of running the dev server at 5174, if 5173 is not available but depending on your application, you might have to get a random port and run your application there.

For the other half, I use caddy, here is the modified `Caddyfile`

```
$ cat Caddyfile
:80 {
	@default host trackfootball.localhost
	reverse_proxy @default localhost:5173

	@port2 header_regexp port2 Host ^([0-9]+)\.trackfootball\.localhost
	reverse_proxy @port2 localhost:{re.port2.1}
}
```

Essentially, this allows the following scheme to work:

```
trackfootball.localhost - localhost:5173 # hardcoded, existing setup keeps on working

5173.trackfootball.localhost - localhost:5173 # port derived from url
5174.trackfootball.localhost - localhost:5174 # port derived from url

# and so on...
```

You might have to change some other things in your application for this to work for you or change the URL scheme to suit your application needs.

For example, this setup also supports local ‘login’ locally for me because the cookie domain is set to `.trackfootball.localhost`.

### **Bonus - AI agents**

Is it 2026 without a post mentioning AI agents? While this post is 100% handwritten, you might have noticed by now that the above setup would make AI agents have access to isolated environment with git-worktree easily (depending on what all do you need to / want to isolate in your environment).

In fact, worktrunk supports running any binary command with a single command while creating the git-worktree. Here is how you would boot the [claude](https://github.com/anthropics/claude-code) in one go:

```
$ wt switch -c -x claude feat1
```

You might also need isolated instances of other things in your development environment like a database. Luckily, Xata provides ability to create a [Postgres database branches](https://xata.io/blog/what-is-database-branching-a-complete-guide-for-development-teams) quickly and it even supports [Agent Skills](https://xata.io/blog/teaching-claude-code-to-use-isolated-database-branches-with-agent-skills) for your AI agent to be able to create branches autonomously.

If you want to experiment with Xata branching in your own repo, the CLI docs and branching workflow are the [**place to start**](https://xata.io/get-access).

### **Conclusion**

This post described my current git-worktree setup, this hasn’t stood the test of time yet but I wanted to share it already to get feedback from the wider community.

Thanks for reading this and please share tips and your setup with me.
