Git worktrees are underrated. They let you work on multiple branches simultaneously without stashing, committing, or switching context. But the vanilla git worktree command has friction — no auto-symlinks for shared directories, no branch auto-creation, no shell integration for cd.
I built gw, a thin shell wrapper that solves all of this in ~180 lines of bash.
The Problem
Every time you create a worktree for a Node.js project:
git worktree add ../worktrees/feature/auth feature/auth
cd ../worktrees/feature/auth
# Oops, no node_modules
npm install # 30 seconds wasted
# Oops, no .env
cp ../../.env .
# Oops, no .turbo cache
Multiply this by 5 branches and you’re spending more time on setup than coding.
The Solution: gw
gw add feature/auth
# Creates worktree, auto-creates branch, symlinks node_modules/.env/.turbo, cd into it
# Done in 2 seconds
What gw add Actually Does
- Auto-detects if branch exists — creates it if not (no
-cflag needed) - Creates worktree at
./worktrees/feature/auth - Reads
~/.worktree.yml(global) and./worktree.yml(project), merges them - Symlinks every listed path from repo root into the worktree
cds into the worktree automatically
All Commands
gw add feature/auth # create worktree + cd
gw add feature/auth -b dev # create from specific base
gw cd feature/auth # cd to existing worktree
gw root # cd back to repo root
gw remove feature/auth # remove worktree, cd to root
gw ls # list all worktrees
Configuration: Two-Level worktree.yml
Global: ~/.worktree.yml
Paths you want symlinked in every project:
symlinks:
- .env
- .env.local
Project: ./worktree.yml
Paths specific to this repo:
symlinks:
- node_modules
- .turbo
- packages/ui/node_modules
Both files are merged with deduplication. Missing sources are silently skipped — your global config can list node_modules even for Go projects without errors.
How It Works
The implementation is two files:
git-worktree.sh — the core logic. It resolves the real repo root (even when called from inside a worktree), parses both YAML configs, and manages worktree lifecycle.
The key trick for repo root resolution:
_git_common_dir=$(git rev-parse --path-format=absolute --git-common-dir)
REPO_ROOT=$(dirname "$_git_common_dir")
--show-toplevel returns the worktree root when you’re inside one. --git-common-dir always points to the real .git directory, so dirname gives you the actual repo root. This means gw root, gw cd, and gw remove all work correctly from inside any worktree.
git-worktree-wrapper.sh — a shell function that captures the script’s stdout (the worktree path) and cds into it:
gw() {
local script="$GW_SCRIPT_DIR/git-worktree.sh"
local cmd="${1:-}"
case "$cmd" in
add|cd|root|remove)
local output
output=$("$script" "$@") || return $?
local target
target=$(echo "$output" | tail -1)
if [[ -n "$target" && -d "$target" ]]; then
cd "$target" || return $?
echo "Now in: $(pwd)"
fi
;;
ls|list)
"$script" "$@"
;;
esac
}
The wrapper is sourced in .zshrc/.bashrc. The GW_SCRIPT_DIR variable is resolved at source time — not at call time — which avoids the classic zsh $0 problem where $0 inside a function returns the function name instead of the script path.
# Resolved at source time, works in both bash and zsh
GW_SCRIPT_DIR="${GW_SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]:-${(%):-%x}}")" && pwd)}"
Claude Code Integration: WorktreeCreate Hook
Claude Code has a built-in worktree feature (isolation: "worktree" in the Agent tool) that creates worktrees at .claude/worktrees/. The problem: it doesn’t know about your worktree.yml symlinks.
The fix: a WorktreeCreate hook that runs automatically when Claude Code creates a worktree.
settings.json:
{
"hooks": {
"WorktreeCreate": [
{
"hooks": [
{
"type": "command",
"command": "bash \"$HOME/.claude/hooks/worktree-symlink.sh\""
}
]
}
]
}
}
worktree-symlink.sh receives JSON on stdin with worktree_path and cwd, resolves the repo root, reads both global and project configs, and creates symlinks:
INPUT=$(cat)
WORKTREE_PATH=$(echo "$INPUT" | jq -r '.worktree_path // empty')
PROJECT_DIR=$(echo "$INPUT" | jq -r '.cwd // empty')
REPO_ROOT=$(cd "$PROJECT_DIR" && git rev-parse \
--path-format=absolute --git-common-dir | xargs dirname)
# Merge global + project configs, deduplicate, create symlinks
{ parse_yaml "$HOME/.worktree.yml"; parse_yaml "$REPO_ROOT/worktree.yml"; } \
| sort -u \
| while IFS= read -r path; do
[[ ! -e "$REPO_ROOT/$path" ]] && continue
ln -sf "$REPO_ROOT/$path" "$WORKTREE_PATH/$path"
done
echo "$WORKTREE_PATH"
Now when Claude Code spawns a subagent with isolation: "worktree", the worktree gets the same symlinks as gw add — node_modules, .env, everything.
Setup
Automatic (via install script)
./install.sh
# Adds `source ~/.claude/scripts/git-worktree-wrapper.sh` to your .zshrc
# Open a new terminal or: source ~/.zshrc
Manual
# Add to ~/.zshrc or ~/.bashrc
source /path/to/scripts/git-worktree-wrapper.sh
Create your global config
cat > ~/.worktree.yml << 'EOF'
symlinks:
- .env
- .env.local
EOF
Create a project config
cat > worktree.yml << 'EOF'
symlinks:
- node_modules
- .turbo
EOF
Don’t forget to gitignore the worktrees directory:
echo "worktrees/" >> .gitignore
Why Symlinks Instead of Copying?
- node_modules can be 500MB+. Symlinking takes 0 bytes and 0 seconds.
- .env stays in sync — edit once, all worktrees see the change.
- Removal is safe — deleting a symlink doesn’t delete the source.
gw removecleans up without risk.
The tradeoff: if you need different .env values per worktree, add .env to project-level config with copy semantics instead. But for most workflows, shared state is what you want.
Putting It All Together
My typical workflow:
# Morning: start working on auth feature
gw add feature/auth -b develop
claude # start Claude Code in the worktree
# Urgent bug comes in
gw root
gw add fix/login-crash
# fix the bug, commit, push, PR
# Back to auth
gw cd feature/auth
# everything is exactly where I left it
# End of day cleanup
gw remove fix/login-crash
gw ls # verify only auth worktree remains
Each worktree has its own branch, its own working state, and shared dependencies via symlinks. No stashing. No npm install. No copying .env. Just gw add and go.
The full source is on GitHub — scripts/git-worktree.sh and scripts/git-worktree-wrapper.sh.