Instant Laravel worktrees with Laravel Herd. One command gives you a fully working parallel copy of your app: its own branch, its own cloned database, its own Herd site with TLS, its own pinned PHP version, dependencies installed, migrations run.
wt new sup-1234
# a few minutes later: https://sup-1234.myapp.test is a fully working app
wt rm sup-1234
# site unlinked, databases dropped, worktree removedUseful for reviewing PRs with a running app, testing risky migrations against a disposable copy of your dev data, and running parallel AI coding agent sessions without them fighting over one checkout's HEAD.
brew install wilburpowery/tap/wtOr drop the wt script anywhere on your PATH.
- macOS (APFS is used for copy-on-write dependency cloning)
- Laravel Herd serving your app (free tier is fine)
- A local database for the app — MySQL/MariaDB or Postgres (any provider: DBngin, Herd Pro services, Homebrew, …).
wtreadsDB_CONNECTION,DB_HOST,DB_PORT,DB_USERNAME, andDB_PASSWORDfrom the app's.envfor its own create/clone/drop operations, so wherever the app connects,wtconnects. SQLite apps work too: the database file is simply copied into the worktree. Client CLIs (mysql/mysqldumporpsql/pg_dump) must be on PATH for the server drivers.
- Resolves the branch — existing local branch, else fetch + track
origin/<branch>, else a new branch off the repo's default branch (read fromorigin/HEAD, somainandmasterrepos both work). Branch name defaults to<prefix><slug>. - Copies your git-ignored config (
.env, certs, keys) from the main checkout — worktrees only share tracked files. - Rewrites the env for isolation —
APP_URL,SESSION_DOMAIN, andDB_DATABASEpoint at slug-specific values. - Links the site in Herd as a subdomain of the main app (
APP_URL=https://myapp.testgiveshttps://<slug>.myapp.test— visually grouped, cookie-compatible, still isolated) (herd link, with--isolatewhen a PHP version is configured) — before installing anything, because unlinked directories float to the newest installed PHP and breakcomposer installon version-pinned apps. - Clones the database —
mysqldump --single-transactionorpg_dump, depending onDB_CONNECTION, into<db>_<slug>, so migrations in the worktree can't touch your main data. Connection details come from the app's.env. - Clones dependencies via APFS copy-on-write — every
node_modulesand composervendorin the main checkout is cloned withcp -c(instant, near-zero disk), then the real installers run as a fast reconcile against the worktree branch's lockfiles. - Builds frontend assets when the app has a
vite.config.*and abuildscript (Laravel 500s withoutpublic/build/manifest.json, which is git-ignored). - Runs migrations.
wt rm <slug> reverses all of it (the branch is deliberately kept). wt list shows every worktree plus any orphaned worktree databases left in MySQL.
Zero config works for a plain Laravel app served by Herd: app at the repo root, database derived from .env's DB_DATABASE, package manager detected by lockfile.
For everything else there are two bash config files, sourced in order: ~/.wtrc (global), then <repo>/.wtrc (per repo — keep it out of git via .git/info/exclude).
| Variable | Default | Purpose |
|---|---|---|
WT_APP_DIR |
. |
Where the Laravel app lives (e.g. apps/backend in a monorepo) |
WT_PHP |
none | PHP version for herd link --isolate |
WT_BRANCH_PREFIX |
git config wt.branchPrefix, else empty |
New branches become <prefix><slug> |
WT_DB_SOURCE |
derived from .env |
Database to clone (connection driver/host/port/credentials always come from the app's .env) |
WT_TEST_DB_SOURCE |
none | Optional second (test) database to clone; also writes .env.testing.local |
WT_COPY |
auto (.env files) |
Repo-relative paths to copy into new worktrees |
WT_DEFAULT_BRANCH |
from origin/HEAD |
Base for new branches |
Define bash functions in a .wtrc to replace any lifecycle step. Hooks run inside wt's shell, so helpers like free_port, wt_has_flag, wt_clone_deps, and wt_composer_bin are available.
| Hook | Default behavior |
|---|---|
wt_host <slug> |
echoes <slug>.<parent host> (parent host from the app's APP_URL, e.g. demo1.myapp.test) |
wt_env_extra <wt> <slug> <host> <db> |
no-op (extra env rewrites) |
wt_install <wt> |
CoW-clone deps, then npm/pnpm/yarn + composer reconcile |
wt_post_create <wt> |
build Vite assets if present, php artisan migrate |
wt_pre_remove <wt> |
no-op |
wt_list_extra <wt> |
no-op (extra wt list lines) |
An unknown subcommand wt foo dispatches to wt_cmd_foo if the .wtrc defines it. Unknown --flags passed to wt new are collected for hooks to inspect via wt_has_flag. Example: a monorepo can define a --with-dashboard flag that mints a TLS cert and a dedicated frontend port, plus a wt dashboard <slug> subcommand that boots it — without the core script knowing dashboards exist.
# <repo>/.wtrc for a monorepo
WT_APP_DIR="apps/backend"
WT_PHP="8.4"
WT_TEST_DB_SOURCE="myapp_test"
WT_COPY=(".env" ".cert" "apps/backend/.env")
wt_post_create() {
local wt="$1"
(cd "$wt/apps/backend" && pnpm exec turbo run build --filter="myapp^..." && pnpm dev:build)
(cd "$wt/apps/backend" && herd php artisan migrate --no-interaction)
}- Link before composer. Herd resolves PHP per site; unlinked dirs float to the newest PHP and locked dependencies explode.
- Never use Herd's
composer/barephpshims for this — they ignore site isolation.wtruns composer's phar underherd php, which resolves isolation from the working directory. - Git-ignored runtime artifacts don't exist in fresh worktrees — built assets, package
dist/folders. Anything your app needs at runtime that isn't tracked needs a build step (the defaults handle the common Laravel + Vite case). package.jsonwithout anamefield (Laravel's default!) makes npm rewritepackage-lock.jsonin every worktree, because npm infers the name from the directory.wtwarns about this at create time, andwt rmautomatically restores the lockfile when that rename is the only change. Adding anameto yourpackage.jsonis the real fix.
MIT