How isolation works
This page explains what gets isolated between worktrees and how. The goal: several agents running in parallel behave like several independent developers in separate branches — none stepping on another’s files, database, ports, or environment.
What a git worktree gives you (and what it doesn’t)
Section titled “What a git worktree gives you (and what it doesn’t)”A git worktree is a separate working directory checked out to a different branch,
sharing one underlying .git object store. That gives you code isolation for
free: each agent edits its own files on its own branch, and conflicts move to
merge time where normal git tooling handles them.
What a worktree does not give you:
.env(gitignored) — absent in a fresh worktreeAPP_KEY— would be shared if you copied.envnaivelyvendor/andnode_modules/— not created- a database — shared with the base checkout unless you isolate it
- storage symlinks — missing
- ports — every dev server wants the same ones
deskhand provisions each of these per worktree.
Database isolation
Section titled “Database isolation”This is the dimension that matters most, especially for event sourcing.
Default: full isolation. Each worktree gets its own database, plus its own numbered databases for parallel Pest runs. Two agents rebuilding projections, running migrations, or appending events never touch each other’s data.
Why full isolation is the default for event-sourced apps specifically: two agents appending events to a shared event store would interleave streams and corrupt each other’s aggregates; two agents rebuilding read models against a shared database would clobber each other’s projections. Isolation makes each desk a clean, private world.
--shared-db exists for genuinely read-only sessions (e.g. an agent that only
investigates the codebase). It points the worktree at the base database and skips
DB creation. Use it only when no writes will happen.
Engines. SQLite is the default — a database is just a file, so creation and
teardown are instant and isolation is perfect. --db=mysql is available for
projects whose behaviour depends on MySQL specifics (certain JSON, locking, or
full-text behaviour); deskhand then creates uniquely-named MySQL databases per
worktree. (MySQL only — not MariaDB.)
Environment isolation
Section titled “Environment isolation”- The base
.envis copied into each worktree, never symlinked, so edits in one desk don’t propagate to others. - A fresh
APP_KEYis generated per worktree — sessions and encryption are never shared across desks. - Per-worktree overrides are injected: the database name/connection, an
APP_NAMEtag (<app> [<slug>]) so environments are distinguishable in logs/mail/browser, the assigned ports, and (conditionally) Redis namespacing. .env.testingalways forces safe drivers (array cache/session, sync queue), so the suite is hermetic regardless of what the runtime.envuses.
Port isolation
Section titled “Port isolation”Every dev server defaults to the same ports, so two running worktrees would collide. deskhand assigns each worktree deterministic, slug-derived ports — a hash of the slug mapped into configured ranges — so the same branch always gets the same ports (no scan, stable across runs). If a derived port is occupied by a foreign process, deskhand surfaces it rather than silently reassigning.
Redis isolation (conditional)
Section titled “Redis isolation (conditional)”Most test suites mock Redis, so isolation is only relevant for the running app, not the suite. deskhand therefore makes Redis isolation conditional:
- It activates only if real Redis use is detected in
.env(redis cache/queue/session drivers orREDIS_*present). - When active, it injects a per-slug key prefix (the primary, effectively
unlimited mechanism) and assigns a logical DB index derived deterministically
as
hash(slug) % 16(a bonus wall for low worktree counts). Because only 16 indices exist, index collisions are tolerated, never an error — the prefix already guarantees isolation; deskhand does not scan for a free index, keeping the index stable per branch like ports. .env.testingforces safe drivers regardless, so tests never depend on shared Redis.--no-redis-isolationopts out.
Process-level isolation (a separate Redis instance per worktree) is intentionally not done — it’s overkill for local dev.
Dependency isolation
Section titled “Dependency isolation”composer install runs in each worktree (its own vendor/). When a frontend is
detected, a JS install runs too, giving each worktree its own node_modules. The
package manager is chosen from the committed lockfile — yarn.lock → yarn,
package-lock.json → npm (npm fallback when ambiguous, overridable via
js_package_manager) — using the lockfile-respecting install (npm ci /
yarn --immutable/--frozen-lockfile) when a lockfile exists, and the plain
install otherwise. deskhand does not symlink node_modules across worktrees:
concurrent installs can corrupt a shared directory, and branches may differ in
dependencies. --prefer-offline keeps installs fast by reusing the shared cache.
What isolation does not cover
Section titled “What isolation does not cover”Isolation is the floor for parallel agent work, not the whole story. deskhand
gives each agent a clean, private substrate; it does not decide which agent
works on what. To avoid merge pain, partition work by file ownership up front
(one agent owns the domain layer, another the Filament panel, another the HTTP
layer) and record that map in AGENTS.md/CLAUDE.md. That coordination layer is
deliberately outside deskhand’s scope.