Isolating Parallel Claude Code Agents in Phoenix

Elixir/Phoenix mechanics for parallel Claude Code agents: templating PORT per worktree, one Postgres DB per branch, and asset-watcher collisions nobody covers.

TL;DR: A git worktree gives an agent its own file tree and branch. It does not give it its own port, its own database, or its own asset watcher — and on a Phoenix + Ecto app, those three collisions are exactly where a promising parallel-agent setup falls apart. This is the Phoenix-specific plumbing: templating PORT per worktree through config/runtime.exs, why a shared Postgres database breaks the moment two branches diverge on a migration, why esbuild/tailwind watchers fight each other, and the honest math on when the isolation is worth building versus just running agents one at a time.

The bug that already lives in this site’s CLAUDE.md

This site’s own repo has a rule that exists because of a real afternoon lost to it: always start the dev server with PORT=4001 mix phx.server, never the bare command. An unrelated process on this machine — a different project entirely — already squats on port 4000. Run the default command and one of two things happens: Phoenix fails to bind and you get an obvious error, or worse, something else is already listening and your curl or health check quietly hits the wrong app, returning 404s for routes that are completely fine. That rule is the kind of CLAUDE.md entry that only gets written after it costs you something — and it’s among the highest-value lines in the file, precisely because it’s a fact about the machine, not the code, and nothing in the repo would ever tell you to look for it.

One port conflict, one dev server, one human debugging it: that’s a bad afternoon. Run that same conflict across four Claude Code sessions each trying to bind their own copy of the app, and it’s not an afternoon anymore — it’s four agents stepping on each other, on a machine, silently, while you’re doing something else.

Why the generic worktree tutorials don’t help here

Search “git worktrees Claude Code parallel agents” today and you’ll find Anthropic’s own worktree docs, a YouTube walkthrough, and at least half a dozen near-identical blog posts (MindStudio alone has run the topic twice) covering the same three commands: git worktree add, point a new Claude Code session at the directory, done. That content is fine as far as it goes — a worktree genuinely does give each agent an isolated working directory and branch, sharing one .git history with no stashing and no file collisions.

But “fine as far as it goes” stops well short of a real Phoenix app. A worktree isolates your files. It does nothing about the three things a Phoenix + Ecto app also needs isolated to run two copies side by side: the HTTP port, the database connection, and the asset build watchers. Every generic worktree post treats “now run the dev server” as a throwaway last step. For a Rails or Django toy app that might be close enough. For a Phoenix app with Ecto migrations and an esbuild/tailwind watcher pair, it’s where the setup actually breaks — and it’s the part nobody writes about, because it’s framework-specific and framework-specific content doesn’t rank as well as “how to use git worktrees.”

So: not another worktree tutorial. The mechanics underneath it, for the stack most of the generic content ignores.

Port templating: one PORT export per worktree

The fix for the port problem generalizes past the one-conflict story above. If you’re running N parallel agent sessions, each in its own worktree, each needs its own port — and in a stock Phoenix app, that’s one environment variable away from working, if your config actually reads it.

Check where your app’s port comes from. A freshly generated Phoenix app hardcodes a port in config/dev.exs. This site’s config doesn’t — the HTTP port is resolved in config/runtime.exs, which Phoenix loads for every environment (not just prod), reading:

config :sublime_coding, SublimeCodingWeb.Endpoint,
  http: [
    port: String.to_integer(
      System.get_env("PORT", "4000")
    )
  ]

That one line is why PORT=4001 mix phx.server works at all here — there’s no port: 4000 sitting in dev.exs to override. If your app does have a hardcoded port in dev.exs (the default from mix phx.new), move it to read System.get_env("PORT", "4000") the same way before you try to run more than one worktree at once. It’s a one-line change and it’s the precondition for everything else in this post.

With that in place, each worktree gets a small .envrc or launch script that pins its own port:

# worktree-a/.envrc
export PORT=4001
export DATABASE_URL="$PG_BASE/myapp_wt_a"

# worktree-b/.envrc
export PORT=4002
export DATABASE_URL="$PG_BASE/myapp_wt_b"

If you use direnv, this is automatic on cd. If you don’t, a two-line bin/dev script per worktree that exports the vars before exec mix phx.server does the same job with no new dependency.

One thing you get for free once the port is templated: if you run Tidewave or another Phoenix-runtime MCP server, it mounts as a plug on the endpoint itself — at /tidewave/mcp on whatever port the app is bound to, not a separately configured service. Template the PORT, and the MCP endpoint follows it automatically; you never hand-maintain a second map of “which agent talks to which MCP URL.” Point each Claude Code session’s .mcp.json at http://localhost:<that worktree's port>/tidewave/mcp and the runtime introspection — logs, project_eval, whatever the server exposes — stays scoped to the right agent’s copy of the app.

One database per worktree, not one database shared

Ports are the visible collision. The database is the one that actually corrupts state, and it’s the part every generic worktree post skips entirely, because most of them aren’t written for anything with a persistence layer.

Here’s the failure mode if two worktrees share one Postgres database. Agent A is on a branch that’s three migrations ahead — it added a column, backfilled it, added a NOT NULL constraint. Agent B is on a branch that hasn’t seen those migrations yet, still inserting rows the old way. They’re not two isolated experiments anymore; they’re two different schemas fighting over one table, and whichever one runs mix ecto.migrate last wins, silently, until the other agent’s next query throws an error that has nothing to do with the actual bug it’s working on. Ecto’s Sandbox adapter solves a related but different problem — it wraps concurrent tests inside a single BEAM node in per-connection transactions so they don’t see each other’s writes. It does nothing for two entirely separate mix phx.server processes, in two different worktrees, both writing to the same database outside a test run. Two dev servers on shared state will fight, and the sandbox has no opinion about that at all.

The fix is boring and it’s the same one that already works for the analogous problem in CI: one database per worktree, named after the branch or worktree slot.

# per-worktree setup, run once per new worktree
export DATABASE_URL="$PG_BASE/myapp_wt_a"
mix ecto.create
mix ecto.migrate

A naming convention that survives more than a week: <app>_<worktree-slot>_dev, e.g. myapp_wt_a_dev, myapp_wt_b_dev — tied to the slot, not the branch name, since branches get deleted and worktree directories get reused for the next task. Seed data is the other half of this: if your priv/repo/seeds.exs assumes a fresh database, run it once per worktree after ecto.create, not once globally and hope every worktree inherited it.

The cost here is real and worth naming honestly: N worktrees means N Postgres databases sitting on disk, and N sets of migrations to keep current if a worktree sits stale for a week while its sibling branches move on. That’s a small, fixed cost against the alternative, which is an agent debugging a NOT NULL constraint violation that’s actually a stale schema from a different branch — the kind of failure that reads exactly like the confident-wrong-answer pattern I catch from agents every week, except this time the agent isn’t wrong, your test database is.

Asset watchers: the collision nobody budgets for

The third collision is the one that costs CPU and wall-clock time rather than correctness, and it’s the one I see people discover last. Phoenix’s dev watchers — esbuild and tailwind in config/dev.exs — run as OS processes alongside the BEAM, rebuilding priv/static/assets on file change. Run two worktrees’ dev servers at once and you have two independent watcher pairs, each polling its own copy of assets/, each recompiling into its own priv/static/. That part is actually fine — worktrees give each one a separate assets/ directory to watch, so they don’t literally clobber the same output file.

What does bite you: four or five worktrees each running their own esbuild --watch and tailwind --watch is four or five extra long-lived OS processes idling on a laptop that’s also running four or five Claude Code sessions and four or five Postgres connections. On a modern machine that’s a nuisance, not a wall. But it’s real enough that “how many worktrees can I actually run at once” has a practical ceiling closer to four or five than to “as many as I want” — past that, the fan noise and the context-switching cost between sessions both go up faster than the throughput does. If you’re not actively editing frontend assets in a given worktree, killing its watcher (mix phx.server without the watchers config, or a --no-watch env flag you wire in) is a legitimate way to claw back the overhead for agent sessions that are purely backend work.

A worktree isolates your files. It does nothing about the port, the database, or the watcher — and on a real Phoenix app, that’s where the setup actually breaks.

When the isolation is worth building

None of this is free, and the honest version of this post says so out loud instead of selling you the setup unconditionally. Standing up N worktrees with N ports, N databases, and N watcher pairs costs you real time up front — the .envrc templates, the per-worktree ecto.create, the discipline to keep migrations in sync across siblings that are about to merge back into the same branch. It costs disk: N copies of node_modules-equivalent build artifacts and N Postgres databases sitting around. And it costs a kind of mental overhead that doesn’t show up in any setup script — you now have to remember which worktree you’re in before you trust what a terminal or a browser tab is telling you, which is exactly the class of mistake the PORT-conflict story above already demonstrates once.

Where it earns that cost: genuinely independent, non-overlapping tasks that would otherwise serialize behind each other for no good reason — a backend migration in one worktree while an unrelated frontend polish pass runs in another, or three agents each closing out a separate ticket that don’t touch the same files. I’ve described what that looks like day to day, running four to seven parallel agent sessions through a normal engineering day, in my daily agentic AI workflow — the isolation problem in this post is the infrastructure underneath that workflow, not a replacement for it.

Where it doesn’t earn the cost: two agents that are going to touch the same models, the same migration, or the same feature from different angles. Isolating their file trees doesn’t remove the merge conflict, it just delays it and moves it from “now, while I understand both branches” to “later, when I’ve forgotten what either one was doing.” For genuinely coupled work, running agents sequentially — one at a time, each building on the last commit — is the lazier and correcter choice, even though it feels slower in the moment. And whichever mode you’re in, the tooling that decides how an agent works matters more than how many of it you’re running at once; I keep a fairly narrow set of plugins for exactly that reason, laid out in the Claude Code plugin stack I actually run.

The setup in this post — templated PORT, per-worktree DATABASE_URL, watcher discipline — is maybe an hour of one-time work per project. It pays for itself the first time it prevents an agent from spending twenty minutes debugging a 404 that was actually a port collision, or a constraint violation that was actually a stale schema from a sibling branch. It does not pay for itself as a default you reach for on every task, and treating it that way is how a genuinely useful pattern turns into ceremony.

If you’re standing up agent infrastructure for a team

This is exactly the kind of infrastructure decision that’s cheap to get right early and expensive to unwind once three engineers have built habits around whatever half-isolated setup shipped first. If you’re scaling a Phoenix codebase across a team that’s adopting parallel agent workflows and want a second set of eyes on the setup — ports, database isolation, CI implications, the works — that’s a conversation worth having as part of a fractional engineering engagement, before the workaround habits calcify.