Creating worktrees with Claude Code in a custom directory
Claude Code recently got support for a native --worktree flag which creates a git worktree where Claude works in.
The worktrees get created in the .claude/worktrees directory, which makes it a bit hard to use something like Laravel Valet (or Herd) to actually serve the project using NGINX because your parked directory might be ~/www for example.
Lucky for us, 2 hooks have been added with regards to worktree management:
- WorktreeCreate
- WorktreeRemove
By hooking into the WorktreeCreate and WorktreeRemove hook, we can customize the directory the worktree gets created.
"hooks": {
"WorktreeCreate": [
{
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/worktree.sh",
"timeout": 30
}
]
}
],
"WorktreeRemove": [
{
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/worktree.sh",
"timeout": 15
}
]
}
]
}.claude/settings.json
#!/bin/bash
#
# Claude Code passes a JSON payload via stdin:
#
# WorktreeCreate:
# { "hook_event_name": "WorktreeCreate", "cwd": "/Users/.../project", "name": "my-feature" }
#
# WorktreeRemove:
# { "hook_event_name": "WorktreeRemove", "cwd": "/Users/.../project", "worktree_path": "/Users/.../project-my-feature" }
INPUT=$(cat)
if ! command -v jq &>/dev/null; then
echo "jq is required but not installed" >&2
exit 1
fi
HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name')
CWD=$(echo "$INPUT" | jq -r '.cwd')
# Logging
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_DIR="$SCRIPT_DIR/../logs"
LOG_FILE="$LOG_DIR/worktree.log"
mkdir -p "$LOG_DIR"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$HOOK_EVENT] $*" >> "$LOG_FILE"
}
# Create a git worktree at ../{project}-{name} branching off the current branch.
# Outputs the worktree path to stdout (Claude Code reads this to set the working directory).
worktree_create() {
local NAME
NAME=$(echo "$INPUT" | jq -r '.name')
local PROJECT_NAME PARENT_DIR WORKTREE_DIR BRANCH_NAME
PROJECT_NAME=$(basename "$CWD")
PARENT_DIR=$(cd "$CWD/.." && pwd)
WORKTREE_DIR="$PARENT_DIR/$PROJECT_NAME-$NAME"
BRANCH_NAME="worktree-$NAME"
log "Creating worktree: name=$NAME project=$PROJECT_NAME"
log " path=$WORKTREE_DIR branch=$BRANCH_NAME"
# Use current branch as base
local BASE_BRANCH
BASE_BRANCH=$(cd "$CWD" && git rev-parse --abbrev-ref HEAD 2>/dev/null)
: "${BASE_BRANCH:=master}"
log " base=$BASE_BRANCH"
# Create git worktree in sibling directory
cd "$CWD" || exit 1
git worktree add -b "$BRANCH_NAME" "$WORKTREE_DIR" "$BASE_BRANCH" >&2 || {
log "FAILED to create worktree: $NAME"
echo "Failed to create worktree: $NAME" >&2
exit 1
}
log "Created worktree successfully at $WORKTREE_DIR"
# Output the worktree path (the ONLY stdout Claude Code reads)
echo "$WORKTREE_DIR"
}
# Remove a worktree directory and delete its associated branch.
# Finds the main repo from the worktree's git metadata to run cleanup from there.
worktree_remove() {
local WORKTREE_PATH
WORKTREE_PATH=$(echo "$INPUT" | jq -r '.worktree_path')
log "Removing worktree: path=$WORKTREE_PATH"
if [ ! -d "$WORKTREE_PATH" ]; then
log "Worktree directory does not exist, skipping"
exit 0
fi
# Find main repo (first entry in worktree list)
local MAIN_REPO BRANCH_NAME
MAIN_REPO=$(git -C "$WORKTREE_PATH" worktree list --porcelain 2>/dev/null | head -1 | sed 's/^worktree //')
BRANCH_NAME="worktree-$(basename "$WORKTREE_PATH")"
log " main_repo=$MAIN_REPO branch=$BRANCH_NAME"
# Remove worktree and branch
cd "$MAIN_REPO" 2>/dev/null || exit 0
git worktree remove "$WORKTREE_PATH" --force 2>/dev/null || rm -rf "$WORKTREE_PATH"
git branch -D "$BRANCH_NAME" 2>/dev/null
log "Removed worktree successfully"
}
# Parse hook events
case "$HOOK_EVENT" in
WorktreeCreate) worktree_create ;;
WorktreeRemove) worktree_remove ;;
esac
.claude/hooks/worktree.sh
With this script in place, calling claude --worktree will create a worktree in a sibling directory, which means Valet is able to pick your project up so you can easily test it in the browser.
No spam, no sharing to third party. Only you and me.