laravel-herd-worktrees · git:20261020.8b6bb45 · 2026-10-20 · sha256 cd34a2485953f406

laravel-herd-worktrees git:20261020.8b6bb45A

Immutable. This exact content never changes and is served at /api/v1/blob/cd34a2485953f406.

---
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.
compatibility: "macOS with Laravel Herd installed"
---

# 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):

```bash
cd ~/Code           # parent of your laravel project
herd park
```

Verify:

```bash
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](./references/herd-commands.md).

### 2. Create a new worktree

Read the project's setup instructions and Composer hooks first, using the prerequisites
in [Bootstrap a bare worktree](#bootstrap-a-bare-worktree). When hooks require assets
that must be built before the normal install, use that ordered bootstrap instead of
the helper. Otherwise use the helper below: it creates the worktree, copies `.env`,
rewrites `APP_URL` + `DB_DATABASE` + cache prefixes, installs deps, generates app key,
and runs migrations. Afterwards complete the bootstrap's generated-asset, fast-test,
and proof checks; dependency installation alone does not prove the app is runnable.

```bash
bash scripts/worktree-new.sh <branch-suffix> <base-branch> [--driver sqlite|mysql|pgsql]
```

Example:

```bash
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](./scripts/worktree-new.sh) for the exact transformations applied to `.env`.

### 3. Remove a worktree

```bash
bash scripts/worktree-rm.sh <branch-suffix> [--drop-db]
```

Runs `git worktree remove`, optionally drops the per-worktree DB, and `herd unlink`s if a manual link exists.

### 4. Switch PHP version per worktree (optional)

```bash
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.

## Bootstrap a bare worktree

A worktree made with plain `git worktree add` (the PR, issue, and CI recipes in other skills, at `.claude/worktrees/<name>`) has no `vendor/`, `node_modules/`, or `.env`; nor does one the harness made (`claude --worktree`, a subagent with `isolation: worktree`). With the leek-skills plugin in Claude Code, a session that opens in such a worktree is told so at start; elsewhere, check for `vendor/` yourself before running anything. Make it runnable from inside the worktree, with `main` set to the main checkout (`git worktree list --porcelain | sed -n '1s/^worktree //p'`):

1. **Identity and environment.** Confirm `git rev-parse HEAD` is the commit the run means to test. Copy `.env` (and `.env.testing` when present) from `$main`, applying the isolation rules below before running application commands. When the calling skill gives the run its own database, set `DB_DATABASE` to that name in both. When the worktree lives under `.claude/worktrees/<name>` (not an immediate subdirectory of Herd's parked parent) and the run needs a `.test` URL for HTTP verification, run `herd link <name>` (and `herd secure <name>` when `APP_URL` uses `https://`), then `herd unlink <name>` when removing the worktree.
2. **Bootstrap prerequisites.** Read the project's worktree setup instructions, Composer scripts, package scripts, and runtime asset registration. Identify generated files needed by Composer hooks or rendered tests, including ignored editor/plugin bundles. Follow the project's build order; when assets require installed dependencies but Composer hooks require assets, install with `--no-scripts`, build the prerequisites, then run the normal install with scripts enabled.
3. **Dependencies and generated assets.** `cp -Rc "$main/vendor" vendor` (an APFS clone), confirm `test ! -L vendor`, then `composer install` so it matches this branch's lock, using step 2's order when needed. Do the same for `node_modules` when building assets. Each tree owns a real copy: a symlinked `vendor/` makes Pest run the main checkout's code. Build required assets inside this worktree. Clone existing ignored build outputs only after confirming their source inputs, lockfiles, and relevant build configuration match; otherwise rebuild.
4. **Fast-test setup.** When the project uses test-impact analysis (TIA) or another baseline/cache mechanism, run its documented worktree hydration/setup path. Check how it handles `.git` being a file, baseline identity, and per-worktree cache keys. Reuse only compatible baselines and keep writable caches isolated. Do not invent a project-specific cache path or disable tests to make setup appear successful.
5. **Proof.** Confirm Composer hooks complete, the assets needed by the selected flow exist, and the project's focused test command starts successfully. If a fast-test baseline is unavailable, report the missing prerequisite and use the project's documented fallback with an explicit test set. Setup is complete only when this worktree can exercise the intended flow.

## 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](./references/isolation.md).
DB strategy details (sharing seeds, schema dumps, fresh vs. migrate): [references/db-strategies.md](./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

```bash
herd parked              # confirm parent is parked
herd isolated            # list per-site PHP overrides
herd which-php           # PHP version Herd resolves for current dir
git worktree list        # all worktrees + branches
ls -1 ../ | grep <app>   # worktree dirs as Herd sees them
```

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.