Tips & Patterns
Practical recipes for common Worktrunk workflows.
Alias for new worktree + agent
Create a worktree and launch Claude in one command:
Eliminate cold starts
Use wt step copy-ignored in a post-create hook to copy gitignored files (caches, dependencies, .env) between worktrees:
[]
= "wt step copy-ignored"
= "npm ci"
All gitignored files are copied by default. To limit what gets copied, create .worktreeinclude with patterns — files must be both gitignored and listed. See wt step copy-ignored for details.
Dev server per worktree
Each worktree runs its own dev server on a deterministic port. The hash_port filter generates a stable port (10000-19999) from the branch name:
# .config/wt.toml
[]
= "npm run dev -- --port {{ branch | hash_port }}"
[]
= "http://localhost:{{ branch | hash_port }}"
[]
= "lsof -ti :{{ branch | hash_port }} | xargs kill 2>/dev/null || true"
The URL column in wt list shows each worktree's dev server:
$ wt list
Branch Status HEAD± main↕ Remote⇅ URL Commit Age
@ main ? ^⇅ ⇡1 ⇣1 http://localhost:12107 41ee0834 4d
+ feature-api + ↕⇡ +54 -5 ↑4 ↓1 ⇡3 http://localhost:10703 6814f02a 30m
+ fix-auth ↕| ↑2 ↓1 | http://localhost:16460 b772e68b 5h
○ Showing 3 worktrees, 2 with changes, 2 ahead, 2 columns hidden
Ports are deterministic — fix-auth always gets port 16460, regardless of which machine or when. The URL dims if the server isn't running.
Database per worktree
Each worktree can have its own isolated database. Docker containers get unique names and ports:
[]
= """
docker run -d --rm \
--name {{ repo }}-{{ branch | sanitize }}-postgres \
-p {{ ('db-' ~ branch) | hash_port }}:5432 \
-e POSTGRES_DB={{ branch | sanitize_db }} \
-e POSTGRES_PASSWORD=dev \
postgres:16
"""
[]
= "docker stop {{ repo }}-{{ branch | sanitize }}-postgres 2>/dev/null || true"
The ('db-' ~ branch) concatenation hashes differently than plain branch, so database and dev server ports don't collide.
Jinja2's operator precedence has pipe | with higher precedence than concatenation ~, meaning expressions need parentheses to filter concatenated values.
The sanitize_db filter produces database-safe identifiers (lowercase, underscores, no leading digits, with a short hash suffix to avoid collisions and SQL reserved words).
Generate .env.local with the correct DATABASE_URL using a post-create hook:
[]
= """
cat > .env.local << EOF
DATABASE_URL=postgres://postgres:dev@localhost:{{ ('db-' ~ branch) | hash_port }}/{{ branch | sanitize_db }}
DEV_PORT={{ branch | hash_port }}
EOF
"""
Local CI gate
pre-merge hooks run before merging. Failures abort the merge:
[]
= "uv run ruff check"
= "uv run pytest"
This catches issues locally before pushing — like running CI locally.
Track agent status
Custom emoji markers show agent state in wt list. The Claude Code plugin sets these automatically:
+ feature-api ↑ 🤖 ↑1 ./repo.feature-api
+ review-ui ? ↑ 💬 ↑1 ./repo.review-ui
🤖— Claude is working💬— Claude is waiting for input
Set status manually for any workflow:
See Claude Code Integration for plugin installation.
Monitor CI across branches
Shows PR/CI status for all branches, including those without worktrees. CI indicators are clickable links to the PR page.
JSON API
Structured output for dashboards, statuslines, and scripts. See wt list for query examples.
Reuse default-branch
Worktrunk maintains useful state. Default branch detection, for instance, means scripts work on any repo — no need to hardcode main or master:
Task runners in hooks
Reference Taskfile/Justfile/Makefile in hooks:
[]
= "task install"
[]
= "just test lint"
Shortcuts
Special arguments work across all commands—see wt switch for the full list.
Stacked branches
Branch from current HEAD instead of the default branch:
Creates a worktree that builds on the current branch's changes.
Agent handoffs
Spawn a worktree with Claude running in the background:
tmux (new detached session):
Zellij (new pane in current session):
This lets one Claude session hand off work to another that runs in the background. Hooks run inside the multiplexer session/pane.
The worktrunk skill includes guidance for Claude Code to execute this pattern. To enable it, request it explicitly ("spawn a parallel worktree for...") or add to CLAUDE.md:
Tmux session per worktree
Each worktree gets its own tmux session with a multi-pane layout. Sessions are named after the branch for easy identification.
# .config/wt.toml
[]
= """
S="{{ branch | sanitize }}"
W="{{ worktree_path }}"
tmux new-session -d -s "$S" -c "$W" -n dev
# Create 4-pane layout: shell | backend / claude | frontend
tmux split-window -h -t "$S:dev" -c "$W"
tmux split-window -v -t "$S:dev.0" -c "$W"
tmux split-window -v -t "$S:dev.2" -c "$W"
# Start services in each pane
tmux send-keys -t "$S:dev.1" 'npm run backend' Enter
tmux send-keys -t "$S:dev.2" 'claude' Enter
tmux send-keys -t "$S:dev.3" 'npm run frontend' Enter
tmux select-pane -t "$S:dev.0"
echo "✓ Session '$S' — attach with: tmux attach -t $S"
"""
[]
= "tmux kill-session -t '{{ branch | sanitize }}' 2>/dev/null || true"
pre-remove stops all services when the worktree is removed.
To create a worktree and immediately attach:
Subdomain routing with Caddy
Clean URLs like http://feature-auth.myproject.lvh.me without port numbers. Useful for cookies, CORS, and matching production URL structure.
Prerequisites: Caddy (brew install caddy)
# .config/wt.toml
[]
= "npm run dev -- --port {{ branch | hash_port }}"
= """
curl -sf --max-time 0.5 http://localhost:2019/config/ || caddy start
curl -sf http://localhost:2019/config/apps/http/servers/wt || \
curl -sfX PUT http://localhost:2019/config/apps/http/servers/wt -H 'Content-Type: application/json' \
-d '{"listen":[":8080"],"automatic_https":{"disable":true},"routes":[]}'
curl -sf -X DELETE http://localhost:2019/id/wt:{{ repo }}:{{ branch | sanitize }} || true
curl -sfX PUT http://localhost:2019/config/apps/http/servers/wt/routes/0 -H 'Content-Type: application/json' \
-d '{"@id":"wt:{{ repo }}:{{ branch | sanitize }}","match":[{"host":["{{ branch | sanitize }}.{{ repo }}.lvh.me"]}],"handle":[{"handler":"reverse_proxy","upstreams":[{"dial":"127.0.0.1:{{ branch | hash_port }}"}]}]}'
"""
[]
= "curl -sf -X DELETE http://localhost:2019/id/wt:{{ repo }}:{{ branch | sanitize }} || true"
[]
= "http://{{ branch | sanitize }}.{{ repo }}.lvh.me:8080"
How it works:
wt switch --create feature-authruns thepost-starthook, starting the dev server on a deterministic port ({{ branch | hash_port }}→ 16460)- The hook starts Caddy if needed and registers a route using the same port:
feature-auth.myproject→localhost:16460 lvh.meis a public domain with wildcard DNS —*.lvh.meresolves to127.0.0.1- Visiting
http://feature-auth.myproject.lvh.me:8080: Caddy matches the subdomain and proxies to the dev server
Bare repository layout
An alternative to the default sibling layout (myproject.feature/) uses a bare repository with worktrees as subdirectories:
myproject/
├── .git/ # bare repository
├── main/ # main branch
├── feature/ # feature branch
└── bugfix/ # bugfix branch
Setup:
Configure worktrunk to create worktrees as subdirectories:
# ~/.config/worktrunk/config.toml
= "{{ branch | sanitize }}"
Create the first worktree:
Now wt switch --create feature creates myproject/feature/.