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
PORTper worktree throughconfig/runtime.exs, why a shared Postgres database breaks the moment two branches diverge on a migration, whyesbuild/tailwindwatchers 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.migrateA 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.