I wanted to share a small open-source thing I made for myself and figured it might be useful to others doing AI-agent work on Phoenix apps. It’s a Claude Code skill, MIT-licensed:
Fair warning up front: this is deliberately low-tech. It solves one narrow, mechanical problem (giving each worktree its own ports and Postgres DBs) and nothing more. It pairs naturally with Tidewave’s brand-new Spaces feature, which sits a level above this (more on that below).
The problem
Claude Code sessions are bound to the folder they start in, so if you want two agent sessions working on the same repo in parallel, the natural unit is a git worktree: one checkout per branch, one session per checkout.
For a plain library that’s enough. For a Phoenix app it isn’t: two checkouts both try to bind port 4000, and they both point at the same app_dev / app_test Postgres databases. The dev-server collision is obvious (:eaddrinuse). The test collision is the nasty one; it surfaces as confusing DBConnection.OwnershipError sandbox failures, because both test suites are fighting over the same database while the Ecto sandbox assumes it owns it.
So a bare worktree gives you code isolation but not runtime isolation, and the runtime part is exactly what bites you when an agent runs the test suite.
How it works
The whole thing hangs on one mechanism: a conditional import at the bottom of config/config.exs.
if File.exists?("config/#{config_env()}.local.exs") do
import_config "#{config_env()}.local.exs"
end
with config/*.local.exs gitignored. Each worktree gets its own dev.local.exs / test.local.exs that override the Endpoint port(s) and the Repo database names.
The reason I like this: because those local files are untracked, “merge back to main minus the port change” happens automatically; the port change was never in git to begin with. There’s no cleanup step where you remember to revert ports before merging, because the ports only ever lived in an untracked file.
What the commands do
/worktree create <name>:
- Adds the import hook + gitignore entry if the project doesn’t have them yet (one-time, committed to main).
-
- Picks a free port block (
main port + 100·n), checking sibling worktrees and live listeners.
- Picks a free port block (
-
git worktree add ../<app>-<name> -b <name>.
-
- Writes the local config overrides (ports plus
<app>_dev_<name>/<app>_test_<name>), including any extra listeners your dev config binds (HTTPS, mTLS, MQTT, …), since any one you miss dies with:eaddrinuse.
- Writes the local config overrides (ports plus
-
- Copies gitignored-but-required runtime files the new worktree won’t have (local secrets,
mix phx.gen.certTLS certs, …).
- Copies gitignored-but-required runtime files the new worktree won’t have (local secrets,
-
mix setup+MIX_ENV=test mix ecto.create.
-
- Prints a handoff: quit the session,
cdinto the worktree, restartclaudethere.
/worktree destroy <name>does the reverse, with guardrails. It refuses if there’s uncommitted work, verifies no*.local.exswas ever committed on the branch (they can hold secrets), drops both databases, merges the branch intomain, then removes the worktree and branch.
- Prints a handoff: quit the session,
Limitations
- It assumes local Postgres + Ecto. No containers, no other databases.
-
- The skill is prose instructions executed by Claude, not code. There’s no binary to audit; it’s a single markdown file that Claude reads alongside your
mix.exsandconfig/dev.exsto fill in app names, ports, and Repo modules. That makes it easy to read and adapt, but it’s not a deterministic script.
- The skill is prose instructions executed by Claude, not code. There’s no binary to audit; it’s a single markdown file that Claude reads alongside your
-
- The underlying pattern is agent-agnostic. Nothing here is specific to Claude or to AI at all; if you run parallel worktrees by hand, the same conditional-import + per-worktree DB trick works identically. The skill just automates the bookkeeping.
-
- A few sharp edges, learned the hard way, that you’ll hit if you adapt the approach by hand:
git worktree addonly materializes tracked files, so gitignored secrets andphx.gen.certcerts have to be copied over, or boot/seeds fail with a confusing Cowboy:keyfileerror. Every listener in dev config has to shift ports, not just the Endpoint, or you’ll get:eaddrinusewhenever the main dev server is also running. Don’t pipe setup commands throughtail/grep, since the pipe masks failing exit codes and a broken setup looks like it succeeded. And port scanning usesfindrather than a../app-*/glob, because zsh aborts on an unmatched glob when no sibling worktree exists yet.
- A few sharp edges, learned the hard way, that you’ll hit if you adapt the approach by hand:
How this fits with Tidewave Spaces
This is meant as a temporary stopgap to fill a gap for Tidewave, though the underlying pattern is general enough that it may stick around. Tidewave just launched Spaces, a unified control plane: one Tidewave tab can connect to the same app on different ports, e.g. across worktrees. Valim was explicit that Tidewave doesn’t manage worktrees for you yet, and built Spaces to sit on top of whatever worktree approach you already use. This skill just does the mechanical worktree + port + DB provisioning underneath that. Credit to the Tidewave folks (Dashbit / José Valim) for the real work here; this composes with it rather than competing.
Feedback welcome
I’d genuinely like to hear:
- If you’re already running parallel agent sessions on a single Phoenix repo, what are you using for the worktree side? Plain worktrees with manual config, worktrunks, multiple checkouts, rift, containers, or something else, especially now that Spaces gives you a control plane to point at them.
-
- Whether the conditional-import-of-
*.local.exspattern steps on anyone’s existing config conventions.
PRs and issues welcome on the repo. Thanks for reading.
- Whether the conditional-import-of-






















