---
name: laravel-herd-worktrees
description: Use when working with Laravel projects served by Laravel Herd alongside git worktrees on macOS. Triggers on "laravel herd worktree", "new worktree for laravel", "herd parked sites worktree", "multiple branches herd", "composer install per worktree", ".test url for branch", "isolate php version per worktree", or any task that creates, lists, or removes a git worktree inside a Herd-served Laravel project. Covers site routing, per-worktree `.env`/database/cache isolation, and fast dependency install strategy.
---

# 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

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

## 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.
