Set Up Git Worktrees for Laravel + Herd
You are setting up a git worktree system for a Laravel project served by Laravel Herd. Each worktree will get its own database, Herd domain with HTTPS, and Vite dev server port.
Before You Start
- Confirm you are in the root of a Laravel project (check for
artisan,composer.json) - Confirm
.envexists and hasAPP_URL,DB_DATABASE - Detect the database driver from
DB_CONNECTIONin.env(default:mysql). Supported:mysql,pgsql,sqlite - For
mysql/pgsql: confirm the.envhasDB_USERNAME,DB_HOST,DB_PORTand that the database server is reachable - Confirm Herd is available (
herd --version) - Detect the JS package manager from lockfile (
pnpm-lock.yaml→ pnpm,yarn.lock→ yarn,package-lock.json→ npm)
If anything is missing, tell the user what they need and stop.
What to Create
Create all scripts in .claude/scripts/. Create the directory if it doesn't exist.
Add .claude/worktrees/ to .gitignore if not already there.
Script 1: setup-worktree.sh
Full environment setup for a worktree. Takes a worktree path as argument. Must be idempotent (skip if .worktree-ready marker exists).
Steps:
- Read
APP_URLfrom the main worktree's.env. Strip the scheme (https://orhttp://) and any trailing path/port to get the bare hostname (e.g.,https://myapp.test→myapp.test). This is the base domain. The app name is the hostname without the TLD (e.g.,myapp) - Derive subdomain from the worktree folder name:
{folder}.{app-name}.test(e.g., folderfeature-billing+ app namemyapp→feature-billing.myapp.test) - Copy
.envfrom the main project if it doesn't exist in the worktree - Set
APP_URLtohttps://{subdomain}.test - Set
APP_HOSTNAMEto{subdomain}.test(if the key exists in.env) - Derive database name:
{base-app-name}_{folder_with_underscores}and setDB_DATABASE - Find a free Vite port in range 5100–5199 by checking other worktree
.envfiles AND active listeners (lsof). SetVITE_PORT - Copy PHP dependencies from main project using CoW (copy-on-write) for near-instant cloning:
- Detect platform: macOS uses
cp -cR(APFS CoW), Linux usescp --reflink=auto -R - Copy
vendor/from the main worktree to the new worktree using the appropriate CoW flag - Then run
composer install --no-interaction --quiet— this is near-instant since all packages are already present, but ensuresautoloadand any branch-specific deps are correct
- Detect platform: macOS uses
- Copy JS dependencies from main project using CoW (same flag detection as step 8):
- Copy
node_modules/from the main worktree to the new worktree using CoW - Then run the detected package manager's install command — near-instant since all packages are present, but ensures lockfile consistency
- Copy
- Generate application key:
php artisan key:generate --no-interaction --quiet - Create the worktree database based on
DB_CONNECTION:- mysql:
mysql -u $DB_USERNAME -p$DB_PASSWORD -h $DB_HOST -P $DB_PORT -e "CREATE DATABASE IF NOT EXISTS \$DB_DATABASE`"` - pgsql:
PGPASSWORD=$DB_PASSWORD createdb -U $DB_USERNAME -h $DB_HOST -p $DB_PORT $DB_DATABASE(ignore error if it already exists) - sqlite: set
DB_DATABASEto the worktree'sdatabase/database.sqliteabsolute path, thentouchthe file
- mysql:
- Run migrations:
php artisan migrate --seed --no-interaction --force - If Passport is installed, run
php artisan passport:install --no-interaction(don't fail if not installed) - Create storage link:
php artisan storage:link --no-interaction --force - Set permissions on
storage/andbootstrap/cache/(775) - Patch the
package.jsondev script to include--port {VITE_PORT}(use node to read/write JSON) - Link to Herd:
herd link {link-name}andherd secure {link-name} - After Herd link/secure, re-set
APP_URLandAPP_HOSTNAME(Herd sometimes overwrites.env) - Touch
.worktree-readymarker file - Print summary: branch, path, URL, database, Vite port, dev server command
Script 2: claude-worktree.sh
User-facing create command. Usage: ./claude-worktree.sh [branch-name]
- If no branch name, generate one:
wt-YYYYMMDD-HHMMSS - Sanitize for folder name (replace
/with-, lowercase) - If worktree exists and is set up, just cd and start Claude
- If worktree exists but not set up, show error
- Create git worktree (use existing branch if it exists, otherwise create new)
- Run
setup-worktree.sh - Start Claude Code in the worktree:
cd "$WORKTREE_DIR" && exec claude
Script 3: cleanup-worktree.sh
Standalone cleanup. Takes a worktree path as argument. Reads the worktree's .env to determine what to clean up.
Safety guard: Before dropping the database, verify that the given path is inside .claude/worktrees/. If it's not, refuse to run and print an error. This prevents accidentally dropping the main project's database.
- Drop the worktree database based on
DB_CONNECTIONfrom the worktree's.env:- mysql:
mysql -u $DB_USERNAME -p$DB_PASSWORD -h $DB_HOST -P $DB_PORT -e "DROP DATABASE IF EXISTS \$DB_DATABASE`"` - pgsql:
PGPASSWORD=$DB_PASSWORD dropdb -U $DB_USERNAME -h $DB_HOST -p $DB_PORT --if-exists $DB_DATABASE - sqlite:
rm -f $DB_DATABASE
- mysql:
- Unsecure Herd:
herd unsecure {link-name} - Unlink Herd:
herd unlink {link-name}
Script 4: claude-worktree-remove.sh
User-facing remove command. Usage: ./claude-worktree-remove.sh [name]
- If no argument, list available worktrees and exit
- Run
cleanup-worktree.shon the worktree - Remove git worktree:
git worktree remove --force - Delete the branch:
git branch -d(safe delete — will warn if the branch has unmerged changes; the user can force with-Dthemselves)
Script 5: ensure-worktree-setup.sh
Fast pre-check for Claude Code hooks. Two checks only:
- If
.gitis not a file (i.e. we're in the main repo, not a worktree), exit immediately - If
.worktree-readyexists, exit immediately - Otherwise run
setup-worktree.shon current directory
Script 6: detect-worktree-remove.sh
Claude Code hook for auto-cleanup. Receives $CLAUDE_TOOL_INPUT JSON.
- Extract
commandfield using python3 - Check if it matches
git worktree remove - Extract the worktree path (handle
--forceflag) - Run
cleanup-worktree.shon the path
Claude Code Hooks
Update .claude/settings.json to add (or merge into existing) PreToolUse hooks on the Bash matcher:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "MAIN=$(git worktree list 2>/dev/null | head -1 | awk '{print $1}') && [ -n \"$MAIN\" ] && \"$MAIN/.claude/scripts/ensure-worktree-setup.sh\" || true"
},
{
"type": "command",
"command": "MAIN=$(git worktree list 2>/dev/null | head -1 | awk '{print $1}') && [ -n \"$MAIN\" ] && \"$MAIN/.claude/scripts/detect-worktree-remove.sh\" \"$CLAUDE_TOOL_INPUT\" || true"
}
]
}
]
}
}
If .claude/settings.json already exists with other hooks, merge carefully — don't overwrite existing hooks.
Laravel Side: Vite Port
Check the Blade layout file that loads Vite assets. If there's a hardcoded port 5173 in a dev-only script tag or custom directive, replace it with:
$port = Env::get('VITE_PORT', '5173');
This ONLY affects the development code path. If the project uses Laravel's built-in @vite() directive, skip this step.
Important Rules
- All scripts must use
set -euo pipefail - All scripts must be
chmod +x - Never modify the main project's
.envor database - Database credentials must come from the worktree's
.env, never hardcoded - Use
--no-interactionon all Artisan commands - The setup script must be idempotent
- After creating all scripts, show the user how to use them
- For CoW copies, define a helper function in
setup-worktree.shthat detects the platform and uses the right flag:
If the CoW copy fails (e.g. filesystem doesn't support it), fall back to a regularcow_copy() { if [[ "$(uname)" == "Darwin" ]]; then cp -cR "$1" "$2" else cp --reflink=auto -R "$1" "$2" fi }cp -R