DevTools Workflow 7 min read

gw: A Tiny CLI for Git Worktrees with Auto-Symlinks and Claude Code Hooks

Hoang Dang Tan Phat (Kane)

Hoang Dang Tan Phat (Kane)

Apr 9, 2026

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

  1. Auto-detects if branch exists — creates it if not (no -c flag needed)
  2. Creates worktree at ./worktrees/feature/auth
  3. Reads ~/.worktree.yml (global) and ./worktree.yml (project), merges them
  4. Symlinks every listed path from repo root into the worktree
  5. 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
  • 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 remove cleans 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 GitHubscripts/git-worktree.sh and scripts/git-worktree-wrapper.sh.

git-worktree claude-code shell-script developer-workflow productivity symlinks
Hoang Dang Tan Phat (Kane)

Hoang Dang Tan Phat (Kane)

Full-stack developer with 8+ years experience. Building scalable systems with Go, TypeScript, and React.