Claude Code recently shipped a --worktree flag that creates isolated git worktrees for parallel AI sessions. It's great — spin up claude --worktree auth-refactor and you get a fresh copy of your repo on its own branch, completely isolated from your main working directory.
But here's the thing: a fresh worktree is empty. No .env files. No node_modules. No virtualenv. You have to set all that up before the agent can do anything useful.
We solved this with hooks. Here's how.
- Copy
worktree-create.shandworktree-remove.shinto your repo'sscripts/directory - Make them executable:
chmod +x scripts/worktree-create.sh scripts/worktree-remove.sh - Customize the env files, directories, and dependency install commands in
worktree-create.sh - Merge
.claude/settings.jsoninto your.claude/settings.json - Add
**/.claude/worktrees/to your.gitignore - Run
claude --worktree my-featureand watch the setup happen automatically
Say your project needs a few things before it can run:
.env # API keys, secrets
.env.local # Local overrides (dev server port, etc.)
node_modules/ # JS dependencies
.venv/ # Python virtualenv
Every time you spin up a new worktree, all of that is missing. You'd have to manually:
- Copy
.envfrom your main repo - Run
npm install - Set up your virtualenv
- Pick a port that doesn't collide with your other worktrees
If you're running 3 parallel Claude sessions, that's a lot of manual setup. And if you forget the .env, your agent will waste tokens debugging missing environment variables.
Claude Code has a hook system that lets you run shell commands on specific events. The one we want is WorktreeCreate — it fires when claude --worktree is invoked, before the TUI renders.
From the docs: "If you configure a WorktreeCreate hook, it replaces the default git behavior." Your script creates the worktree with git worktree add, runs any setup you need, and prints the worktree path on stdout. Claude then starts a session in that directory.
The docs frame this as a way to support non-git VCS (SVN, Perforce, Mercurial), but it works just as well for git — and it's the cleanest way to run setup during worktree creation, since your script has full control of the terminal before the TUI appears.
Create scripts/worktree-create.sh in your repo (or grab the template):
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
NAME=$(echo "$INPUT" | jq -r '.name')
REPO_PATH="$CLAUDE_PROJECT_DIR"
WORKTREE_PATH="${REPO_PATH}/.claude/worktrees/${NAME}"
BRANCH="worktree-${NAME}"
# Progress goes to /dev/tty — stdout is reserved for Claude (see below)
log() { echo "$*" > /dev/tty 2>/dev/null || true; }
log "Creating worktree (branch: $BRANCH)..."
# Create the git worktree — redirect git output away from stdout!
mkdir -p "${REPO_PATH}/.claude/worktrees"
git worktree add -b "$BRANCH" "$WORKTREE_PATH" HEAD >/dev/null 2>&1
# Copy env files from main repo
log " Copying env files..."
for f in .env .env.local; do
[ -f "${REPO_PATH}/$f" ] && cp "${REPO_PATH}/$f" "${WORKTREE_PATH}/$f"
done
# Install dependencies (customize for your stack)
log " Installing dependencies..."
(cd "${WORKTREE_PATH}" && npm install) >> /tmp/worktree-setup.log 2>&1 || true
log "Worktree ready."
# Tell Claude where the worktree is — THE ONLY THING ON STDOUT
echo "$WORKTREE_PATH"Make it executable: chmod +x scripts/worktree-create.sh
The docs explain the contract: "The hook must print the absolute path to the created worktree directory on stdout." In practice, the critical detail is that nothing else can go to stdout:
- stdout: The worktree path, and only the path. Any extra output (like
git worktree add's "HEAD is now at..." message) gets concatenated with your path. Claude can't parse it and hangs silently. - stdin: JSON with
name,session_id,cwd, and other fields. Read it withcatinto a variable — you can only read it once. - /dev/tty: Use this for progress output. It goes straight to the terminal, bypassing Claude's capture entirely.
Add this to your .claude/settings.json:
{
"hooks": {
"WorktreeCreate": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/scripts/worktree-create.sh"
}
]
}
]
}
}Add this to your .gitignore so worktree contents don't pollute git status in your main repo:
**/.claude/worktrees/
This is safe — the pattern is relative to the working tree root, so it won't affect anything inside the worktrees themselves.
If you run dev servers, you'll hit port collisions fast. Worktree A starts on port 3000, worktree B tries port 3000... boom.
The fix: hash the branch name into a deterministic port number. Same branch always gets the same port.
hash_port() {
local hash
hash=$(echo -n "$1" | md5sum | tr -d -c '0-9' | head -c 5)
echo $(( (hash % 6900) + 3100 ))
}
BRANCH="worktree-${NAME}"
DEV_PORT=$(hash_port "$BRANCH")
cat > "${WORKTREE_PATH}/.env.local" << EOF
DEV_PORT=${DEV_PORT}
EOFNow each worktree gets a stable port in the 3100-9999 range. Your dev server just reads DEV_PORT from .env.local and uses it. No collisions, no guessing.
Claude Code also has a WorktreeRemove hook that fires when a worktree is being deleted. It receives JSON on stdin with a worktree_path field. Use it to kill lingering processes and clean up the git worktree:
{
"hooks": {
"WorktreeRemove": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/scripts/worktree-remove.sh"
}
]
}
]
}
}#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
WORKTREE_PATH=$(echo "$INPUT" | jq -r '.worktree_path')
[ ! -d "$WORKTREE_PATH" ] && exit 0
# Kill processes on the worktree's dev port
if [ -f "${WORKTREE_PATH}/.env.local" ]; then
DEV_PORT=$(grep -oP 'DEV_PORT=\K\d+' "${WORKTREE_PATH}/.env.local" || true)
[ -n "$DEV_PORT" ] && lsof -ti :"$DEV_PORT" | xargs kill 2>/dev/null || true
fi
# Remove the git worktree and its branch
BRANCH=$(git -C "$WORKTREE_PATH" rev-parse --abbrev-ref HEAD 2>/dev/null || true)
git worktree remove "$WORKTREE_PATH" --force 2>/dev/null || true
[ -n "$BRANCH" ] && git branch -D "$BRANCH" 2>/dev/null || truegit worktree add prints to stdout. Its "HEAD is now at..." message gets concatenated with your path output. Claude can't parse it and hangs silently. Always redirect: git worktree add ... >/dev/null 2>&1.
Inline commands don't reliably receive stdin. Using a command string directly in settings.json with cat /dev/stdin | jq resulted in empty stdin. Switching to a script file fixed this.
stdin can only be read once. Don't try to pipe cat /dev/stdin twice. Read into a variable first: INPUT=$(cat).
Why not SessionStart? We initially used SessionStart with a "startup" matcher. It works, but the hook runs after the TUI renders, so your progress output interleaves with Claude's banner and prompt. WorktreeCreate runs before the TUI, giving you clean terminal output.
Here's the workflow:
claude --worktree my-feature
What happens behind the scenes:
WorktreeCreatefires, runsworktree-create.sh- Script creates
.claude/worktrees/my-feature/with branchworktree-my-feature - Script copies
.env, installs deps, assigns port 7342 - Script prints the worktree path on stdout
- Claude starts a session inside the worktree, TUI renders
- Claude is ready to work with a fully configured environment
When you're done:
- You exit the session
- Claude asks if you want to keep or remove the worktree
- If you remove it,
WorktreeRemovefires, kills the dev server on port 7342 - Worktree and branch are cleaned up
scripts/worktree-create.sh— Creates worktree + runs setup, with env copying, deterministic ports, and dependency installationscripts/worktree-remove.sh— Cleanup script for WorktreeRemove hook.claude/settings.json— Hook configuration to add to your.claude/settings.json
Customize the dependency installation section for your stack (npm, pip, cargo, etc.) and the list of env files to copy.