| name | laravel-herd-worktrees |
| description | Create, list, or remove git worktrees for Laravel projects served by Laravel Herd on macOS. Use when the user needs a branch worktree, .test URL, or per-worktree PHP/composer isolation. |
Laravel Herd + Git Worktrees
Run multiple branches of a Laravel project in parallel, each reachable at its own *.test URL through Laravel Herd, with isolated dependencies, env, database, and caches.
Mental Model
- A worktree is a sibling directory of the main checkout with its own
HEAD. Composer/Node deps, vendor/, node_modules/, storage/, and bootstrap/cache/ are per-worktree: never share via symlink.
- Herd's parked directory auto-serves every immediate subdirectory as
<dir>.test. Park the parent of all worktrees once and every new worktree gets a URL for free.
.env is untracked, so it must be re-created in each worktree. APP_URL, DB_DATABASE, cache prefixes must differ per worktree to prevent cross-branch bleed.
When to Use This Skill
- User asks to create a worktree for a Laravel app served by Herd.
- User reports
vendor/, migrations, cache, or .env problems after switching worktrees.
- User wants per-branch URL like
myapp-feat.test without manual Nginx/Valet config.
- User wants the same Laravel project on multiple PHP versions simultaneously.
Workflow
1. One-time setup
Park the parent directory that holds the main checkout (worktrees become siblings of the main checkout, so they land in the same parked dir):
cd ~/Code
herd park
Verify:
herd parked
Each immediate subdir is now <name>.test. Adding a worktree at ~/Code/myapp-feat makes myapp-feat.test resolve immediately.
If parking the parent is not desired (other non-Laravel dirs live there), use explicit herd link per worktree instead: see references/herd-commands.md.
2. Create a new worktree
Use the helper script. It creates the worktree, copies .env, rewrites APP_URL + DB_DATABASE + cache prefixes, installs deps, generates app key, and runs migrations.
bash scripts/worktree-new.sh <branch-suffix> <base-branch> [--driver sqlite|mysql|pgsql]
Example:
bash scripts/worktree-new.sh feat-billing main --driver sqlite
Result: ../myapp-feat-billing/ exists, myapp-feat-billing.test works, isolated DB.
See scripts/worktree-new.sh for the exact transformations applied to .env.
3. Remove a worktree
bash scripts/worktree-rm.sh <branch-suffix> [--drop-db]
Runs git worktree remove, optionally drops the per-worktree DB, and herd unlinks if a manual link exists.
4. Switch PHP version per worktree (optional)
cd ../myapp-legacy
herd isolate php@8.1
Site uses 8.1; sibling worktrees keep their isolated or default version. herd isolated lists all overrides. herd unisolate reverts.
Per-Worktree Isolation Rules
| Concern | Rule |
|---|
vendor/ | Always per-worktree. Never symlink. |
node_modules/ | Always per-worktree. Never symlink. Use pnpm for fastest installs (content-addressable store). |
.env | Copy from main, rewrite APP_URL, DB_DATABASE, CACHE_PREFIX, SESSION_COOKIE, REDIS_PREFIX. |
APP_KEY | Run php artisan key:generate in each worktree. |
| Database | SQLite: per-worktree file. MySQL/Postgres: per-worktree DB name <app>_<suffix>. |
| Redis | Set unique REDIS_PREFIX (or different REDIS_DB index). |
storage/, bootstrap/cache/ | Per-worktree. Run php artisan optimize:clear after install. |
storage/app/public symlink | Re-run php artisan storage:link in the new worktree. |
Full rationale: references/isolation.md.
DB strategy details (sharing seeds, schema dumps, fresh vs. migrate): references/db-strategies.md.
Common Pitfalls
- Sharing
vendor/ via symlink: breaks when branches have different package versions; autoload class maps drift.
- Forgetting
php artisan key:generate: encrypted cookies/sessions fail silently across worktrees that share APP_KEY.
- Same
SESSION_COOKIE across *.test siblings: cookies leak between branches because they share the .test parent domain. Set a unique SESSION_COOKIE per worktree.
- Same Redis DB without prefix: cache keys collide; one branch invalidates another's cache.
- Running
composer install against a stale composer.lock after rebasing: always re-run install after switching base branches inside a worktree.
- Herd not picking up new worktree: happens if the parent isn't parked or if the worktree directory name contains characters Herd rejects (stick to
[a-z0-9-]).
Quick Diagnostics
herd parked
herd isolated
herd which-php
git worktree list
ls -1 ../ | grep <app>
If a .test URL 404s: check herd parked includes the parent, dir name is lowercase, and php artisan route:list works inside the worktree.
Out of Scope
- Valet: different tool. Herd-only here.
- Linux Herd: paths and CLI differ; this skill assumes macOS Herd.
- Docker / Sail: covered by other workflows. Worktrees still work but routing is via Docker, not Herd.