David Hemphill wrote a great post about getting started with Conductor using Laravel — how to wire up per-worktree setup and archive scripts so each parallel agent gets its own clean environment. I run the same idea, but with ** Orca** instead of Conductor. This is my take, with the two scripts I actually use.
The moment you run more than one coding agent at a time, they start fighting over shared state. Two agents against the same database means one's migrate:fresh --seed
wipes the other's work mid-task. Two Laravel apps on the same local domain, the same queue, the same Reverb port — collisions everywhere.
Git worktrees fix the files part: each branch gets its own directory on disk. But a Laravel app isn't just files. It's a database, a signed local domain, a websocket port, a built frontend. If you don't isolate those too, "parallel" is a lie.
Orca is an agent development environment: it runs Claude Code, Codex, Gemini and other CLI agents in parallel, each in its own real git worktree. One worktree = one branch = one workspace. You review the diff, ship a PR, then archive the worktree in one click.
The part that makes it usable for Laravel is two lifecycle hooks in the repository settings:
Inside those scripts Orca hands you two environment variables I lean on heavily:
ORCA_ROOT_PATH
— the path to the main repo (the "real" checkout, where my root .env
lives).ORCA_WORKSPACE_NAME
— the name of the current workspace/branch.Both of my scripts fall back to plain git if those aren't set, so the exact same file also works when I run it by hand outside Orca.
My whole convention fits in one sentence: inside a worktree, the database is always an isolated SQLite file, never the project's shared database. A local file that gets created with the worktree and deleted with it. No seed collisions between parallel agents, ever.
Local serving is Laravel Herd, one isolated, HTTPS-secured site per worktree. So agent A works on myapp-feature-invoices.test
while agent B is on myapp-fix-webhooks.test
, each on its own PHP version, its own database, its own everything.
This runs on worktree creation. I keep the real thing in ~/.scripts/laravel/worktree-setup.sh
and reference it from each project with a tiny shim (more on that at the end). Here's what matters.
Find the main repo, copy its .env:
if [ -n "${ORCA_ROOT_PATH:-}" ]; then
main_root="$ORCA_ROOT_PATH"
else
main_root="$(dirname "$(cd "$(git rev-parse --git-common-dir)" && pwd)")"
fi
if [ -f "$main_root/.env" ]; then
cp "$main_root/.env" .env
else
cp .env.example .env
fi
set -a && source .env && set +a
The root .env
holds my real secrets and API keys, so every worktree inherits a working config with zero manual copy-paste. set -a
exports everything so I can use those values later in the script.
Build a unique, cert-safe site name:
workspace="${ORCA_WORKSPACE_NAME:-$(git rev-parse --abbrev-ref HEAD)}"
workspace_slug="$(echo "$workspace" | sed 's#[^a-zA-Z0-9]#-#g' | tr '[:upper:]' '[:lower:]')"
repo="$(basename "$main_root")"
site_name="${repo}-${workspace_slug}"
max_site_len=58
if [ "${#site_name}" -gt "$max_site_len" ]; then
ws_hash="$(printf '%s' "$workspace" | cksum | awk '{print $1}')"
keep=$(( max_site_len - ${#ws_hash} - 1 ))
site_name="$(printf '%s' "$site_name" | cut -c1-"$keep")-${ws_hash}"
fi
That 58-character cap is a gotcha I learned the hard way: the site name becomes the Common Name of the TLS certificate Herd generates, and a CN longer than 64 characters is invalid. Long branch names (feature/some-very-descriptive-thing
) blow past that, so past the limit I truncate and append a short cksum
hash to keep it unique.
Point .env at the isolated SQLite DB and neutralize mail:
sqlite_path="$(pwd)/database/database.sqlite"
set_env "APP_URL" "https://${site_name}.test"
set_env "DB_CONNECTION" "sqlite"
set_env "DB_DATABASE" "${sqlite_path}"
set_env "MAIL_MAILER" "log"
(set_env
is a small helper that updates a key if present or appends it — BSD sed
, since this is macOS.) Mail goes to the log so an agent can't accidentally send real email from a workspace.
A deterministic Reverb port, only if the project uses Reverb:
if [ -f config/reverb.php ] || grep -q '"laravel/reverb"' composer.json 2>/dev/null; then
hash_val="$(echo -n "$site_name" | cksum | awk '{print $1}')"
reverb_port=$((8100 + (hash_val % 100)))
set_env "REVERB_SERVER_PORT" "${reverb_port}"
set_env "REVERB_PORT" "${reverb_port}"
set_env "VITE_REVERB_PORT" "${reverb_port}"
fi
Two worktrees can't both bind 8080
. Deriving the port from a hash of the site name gives each worktree a stable, collision-resistant port without me tracking anything.
Link the site to Herd, isolate the PHP version, secure it:
php_ver="$(grep -oE '"php"[^,}]*' composer.json 2>/dev/null | grep -oE '8\.[0-9]+' | head -1 || true)"
herd link "$site_name"
if [ -n "$php_ver" ]; then
herd isolate "$php_ver" --site="$site_name" || true
fi
herd secure "$site_name"
herd restart
The PHP version is read straight from composer.json
, so a legacy project on 8.2 and a new one on 8.4 each get the right runtime automatically.
Fresh database, dependencies, and a built frontend:
mkdir -p "$(dirname "$sqlite_path")"
rm -f "$sqlite_path"
touch "$sqlite_path"
[ -f "$main_root/.mcp.json" ] && cp "$main_root/.mcp.json" .mcp.json
herd composer install --no-interaction --prefer-dist --optimize-auto
php artisan config:clear
php artisan key:generate --force --no-interaction
php artisan migrate:fresh --seed --force
php artisan storage:link 2>/dev/null || true
if [ -f yarn.lock ]; then
yarn && yarn build
elif [ -f pnpm-lock.yaml ]; then
pnpm install && pnpm build
elif [ -f package.json ]; then
npm install && npm run build
fi
A couple of details I care about: I copy the git-ignored .mcp.json
from the root so the agent inherits my MCP servers, and I detect the frontend package manager from the lockfile instead of hardcoding npm
. I also gate private Composer auth (Flux Pro) behind "is it actually a dependency and are the creds present" so the script stays generic across projects.
By the time this finishes, the agent opens onto a fully working https://myapp-feature-x.test
with its own seeded database. Zero manual setup.
This runs before Orca destroys the worktree. Its whole job is to undo what setup did — and to never block the deletion, so every command is best-effort with || true
.
#!/usr/bin/env bash
set -uo pipefail
if [ -n "${ORCA_ROOT_PATH:-}" ]; then
main_root="$ORCA_ROOT_PATH"
else
main_root="$(dirname "$(cd "$(git rev-parse --git-common-dir)" && pwd)")"
fi
workspace="${ORCA_WORKSPACE_NAME:-$(git rev-parse --abbrev-ref HEAD)}"
workspace_slug="$(echo "$workspace" | sed 's#[^a-zA-Z0-9]#-#g' | tr '[:upper:]' '[:lower:]')"
repo="$(basename "$main_root")"
site_name="${repo}-${workspace_slug}"
if [ -z "$workspace_slug" ]; then
echo "⚠️ Empty workspace name, cleanup aborted for safety."
exit 0
fi
herd unsecure "$site_name" || true
herd unisolate "$site_name" || true
herd unlink "$site_name" || true
rm -f "$(pwd)/database/database.sqlite" || true
herd restart || true
It rebuilds the exact same site_name
with identical logic, then tears the Herd site down (unsecure → unisolate → unlink), deletes the SQLite file, and restarts Herd to purge the config. The empty-name guard matters: if the branch name ever resolves to nothing, I'd rather abort than run herd unlink ""
and nuke something I didn't mean to.
I don't paste these scripts into every project. The source of truth lives once in ~/.scripts/laravel/
, and each repo carries a two-line shim:
#!/usr/bin/env bash
exec "$HOME/.scripts/laravel/worktree-setup.sh" "$@"
In Orca's repository settings I point the Setup Script at scripts/worktree-setup.sh
and the Archive Script at scripts/worktree-archive.sh
. Improve the shared script once and every project — and every future worktree — gets the fix. The scripts auto-detect PHP version, frontend manager, Reverb and Flux, so the same file really does work everywhere.
If you're running agents in parallel on Laravel, the isolation is the whole game: one SQLite file and one Herd site per worktree, created on setup and torn down on archive. Once that's in place, spinning up a third or fourth agent costs nothing, and none of them can corrupt each other's state.
Credit where it's due — the pattern is straight out of David Hemphill's Conductor post; I just adapted it to Orca and my stack. If you've got a sharper version of any of these scripts, I'd genuinely like to see it.
Baptiste — freelance web dev for 20 years, building in Laravel/PHP and shipping small open-source tools.