Running a couple of coding agents in parallel is great, right up until two of them are working in the same checkout. One is halfway through a refactor, the other switches branch underneath it, and now you're untangling a mess which neither of them made on purpose.
Most of what's written about parallel agents assumes a brand-new JavaScript app on a modern hosting platform. Plenty of us spend our days on something else: long-lived PHP applications with real content, real clients and years of history behind them. Agentic tools aren't just for greenfield projects. The rest of us can benefit from them too.
The fix for the code side is old news: git worktrees. The fix for everything around the code (databases, URLs, config) needed a little more care on our stack, so here's what we learned.
Worktrees: several branches checked out at once
A worktree is an extra checkout of the same repo in its own folder, sharing one .git. No stashing, no branch switching, no “hang on, let me just commit this WIP first”.
That was already handy for working with humans: develop in one worktree while reviewing a teammate's PR in another, without touching either. With agents, it stops being a nice-to-have. Each agent gets its own “lane” (its own folder and branch) and they can't trample each other's files.
DDEV: the same environment for everyone
We build and look after a lot of Silverstripe applications, each with its own PHP and MySQL versions and quirks. DDEV lets us capture all of that in one small .ddev/config.yaml per repo, so anyone on the team can clone a project and have an identical environment running in a few minutes.
DDEV is also perfectly happy running many projects at once, each with its own containers, its own *.ddev.site URL and automatically assigned ports. That's half of what parallel lanes need, for free.
Why don't worktrees and DDEV just work together?
This is where the stack matters. Platforms like Vercel and Supabase give you per-branch environments and database branching out of the box. On a traditional PHP stack, that's on you. Put the two together and a fresh worktree looks fine but doesn't work. The general problems:
- They're all the same project. The committed config pins a project name:, so every worktree tries to be the same DDEV project with the same URL.
- Missing files. git worktree add only copies tracked files, so the new lane has no .env and no secrets.
- No data. A new lane has no database and no uploaded assets. You can't just share one database either: Silverstripe builds its schema from code, so dev/build on one branch quietly reshapes the tables under every other branch. That's corruption, not a merge conflict.
- Git hooks that quietly stop running. Tools like husky generate their hooks into a gitignored folder, so a new worktree doesn't get one. Git treats a missing hook as “no hook”, so commits sail through without linting until CI catches them.
Then there are the Silverstripe-specific traps:
- Every request returns HTTP 400 “Invalid Host”. DDEV says everything's healthy, but Silverstripe's AllowedHosts list doesn't include the lane's new URL.
- The /admin jump. Our favourite. The copied .env points SS_BASE_URL at the main checkout's URL, so on a lane the CMS quietly redirects you back to main, and you don't realise you're editing content in the wrong lane.
Even if you're not on Silverstripe, you may have your own versions of these: Drupal's trusted_host_patterns, Laravel's APP_URL, WordPress's siteurl stored in the database. Every framework has its own host and database traps.
The fixes
The rule that made everything click: all per-lane config goes in untracked files, so nothing lands on the shared branch.

- Project name: each lane gets an untracked .ddev/config.local.yaml, which DDEV already gitignores and applies over the committed config:
name: myapp-lane2
web_environment:
- SS_BASE_URL=https://myapp-lane2.ddev.site DDEV's own guidance suggests deleting name: instead, so each worktree takes its name from its folder. We didn't, because our main URL matters: Auth0 callbacks, CSP and Playwright all depend on it, and deleting name: changes a committed file for the whole team.
- The /admin jump: handled by the SS_BASE_URL line above. Silverstripe's .env loader doesn't override variables that are already set, so DDEV's value wins without editing the .env.
- Missing files: list the untracked files every lane needs (.env, secrets) in a .worktreeinclude at the repo root, and the create script copies them into each new lane. It's the same file Claude Code uses for its own --worktree and subagent worktrees, so one list covers both.
- Git hooks: copy the main checkout's generated hooks folder into the lane. It's a few small scripts, and it saves regenerating them, which is fiddlier than it sounds: when run inside the DDEV container, the hook tool can't find the repo and quietly gives up.
- Database: one DDEV project per lane means one database container per lane, seeded from a shared database dump.
- “Invalid Host”: an untracked config fragment adds the lane's host to AllowedHosts, written before the site first boots. It has to load after the committed one, or Silverstripe quietly lets the committed list win and you're back to the 400.
- Assets: no single answer. Some applications are fine starting with no assets; others have gigabytes of them. So the skill asks: start blank, copy from main (near-instant on macOS with copy-on-write clones), or import an archive.
Keeping it sane
We've had eight or so DDEV projects running at once without the laptop catching fire, but around three lanes is a sensible limit, for your own sanity if nothing else. On some projects we tell agents in AGENTS.md to ask before creating a worktree, so the decision stays with a human.

Lanes can also be long-lived. Rather than creating and destroying one per task, we reset a lane onto a fresh branch between tasks, so slow setup like composer install happens once per lane, not once per task.
We made it a skill
We wrapped all of this into a reusable agent skill with three scripts: create a lane, reset it for the next task, and retire it (which removes the DDEV project too, because git worktree remove doesn't know DDEV exists and leaves its containers behind). It works across our Silverstripe applications without changing any of them.
Because every one of these traps fails silently, the skill finishes by proving the lane works: the site loads on the lane's own address, /admin stays there, and the git hooks are in place. If any of that fails, it says so loudly instead of reporting the lane as ready.
If you want to borrow it, or just read the reasoning behind each trap, it's silverstripe-worktree-lanes on GitHub. Even if you're not on Silverstripe, the general pattern (untracked overrides, a database per lane, long-lived lanes) should carry over.
Agentic tools are every bit as useful on mature, hard-working applications. They just need a little thoughtful plumbing first.
Frequently asked questions
-
Can you run parallel coding agents on a Silverstripe or other PHP project?
Yes. Give each agent its own git worktree and its own DDEV project, so no two lanes share a branch or a database. The extra work is in config the framework keeps outside the code, such as allowed hosts and base URLs.
-
Why can't parallel worktrees share one database?
Because the schema follows the code. In Silverstripe, running dev/build on one branch changes the tables every other branch is using. One database per lane, seeded from a shared dump, avoids that.
-
How do you keep per-lane config out of git?
Keep it in untracked files. DDEV applies an untracked config.local.yaml over the committed config, so each lane gets its own project name and base URL without touching anything the team shares.
-
How many parallel lanes are sensible on one laptop?
We've had about eight DDEV projects running at once, but around three lanes is a practical limit. Long-lived lanes, reset between tasks, keep setup costs down.








