Worktrees and coding agents
Run the same project in several places at once (your checkout plus a
review worktree, or several coding agents on one laptop) without sharing fed-managed
runtime state. fed scopes containers, named volumes, and state to the directory. One
command, fed isolate enable, also gives every declared host-port parameter
a separate value.
Ports must be parameters
Isolation can only remap ports it knows about: the ones declared as type: port parameters and interpolated with {{...}}.
A literal 4321 in a command or healthcheck is invisible to fed and
will collide across workspaces. Write every port the fed way:
parameters:
APP_PORT:
type: port
default: 4321
DB_PORT:
type: port
default: 5433
services:
database:
image: postgres:16-alpine
ports: ["{{DB_PORT}}:5432"] # never "5433:5432"
environment:
POSTGRES_PASSWORD: postgres
app:
process: npm run dev -- --port {{APP_PORT}}
startup_message: 'http://localhost:{{APP_PORT}}'
environment:
DATABASE_URL: 'postgres://localhost:{{DB_PORT}}/app'
healthcheck:
http_get: 'http://localhost:{{APP_PORT}}/health'
entrypoint: app
In normal (non-isolated) mode fed prefers the configured defaults; if one is occupied,
it can allocate an alternative. Under isolation each workspace gets random ports from
the outset, but only for ports declared this way. fed warns once at startup when it
finds literal localhost:<port> references or literal host ports in the
config.
Directory scoping
Everything fed tracks is scoped to the directory containing your fed.yaml:
- Port allocations
- Docker container names
- Docker volumes
- The SQLite state file
Two directories are two independent stacks. There's nothing to configure, and it
doesn't matter who created the directory: a worktree made by fed ws new,
by hand, or by a coding agent behaves the same.
Isolation mode
Directory scoping keeps containers and state apart, but two checkouts of the same project still want the same default ports. Isolation mode fixes that. Run it once in the second checkout:
fed isolate enable # Random ports + unique container names (persisted)
fed isolate status # Show isolation state and port allocations
fed isolate rotate # Re-roll ports and isolation ID
fed isolate disable # Return to the directory's normal scope
fed isolate enable prints an isolation ID and the full port table, then
persists both. Every fed command you run in that directory afterwards (start,
scripts, tests, psql) uses the isolated ports and containers, across
restarts, until you fed isolate disable.
CLI output calls the normal namespace the shared scope. It is shared by runs in this directory, not by different worktrees: normal container names, volumes, and state are still directory-scoped.
Quick isolation
fed start --isolate # Enable isolation + start (one command)
On a new or stopped checkout, this has the same persistent result as
fed isolate enable && fed start. Use the explicit command when services
may already be running: it checks and offers to stop them before changing scopes.
What isolation does
- Ports: all port-type parameters get randomized values instead of defaults. Literal ports (a hardcoded
4321in a command or healthcheck) are not remapped. See Ports must be parameters - Direct Docker containers:
image:services get the isolation ID in their container names - Direct Docker named volumes: named volumes on
image:services are scoped to the isolation ID; bind mounts are unchanged
Compose-backed services are already directory-scoped through a project name derived
from the Compose file path. fed isolate enable does not replace that project
name with the isolation ID, and fed isolate rotate does not create fresh
Compose volumes.
Port management
fed ports list # Show current allocations
fed ports list --json # Machine-readable output
Allocations stay put until you fed isolate disable, re-roll them with fed isolate rotate, or wipe state with a full fed clean.
Git worktrees
Each worktree is a separate directory, so each worktree is a separate stack. Enable isolation right after creating one, before you run anything else:
# Main checkout: default ports
~/project $ fed start
# Second worktree: its own ports, containers, and volumes
~/project-review $ fed isolate enable
~/project-review $ fed start
Both stacks can run at the same time on the same machine when their host ports are
declared as parameters. This scales past two. A
docker ps on a busy laptop shows the main checkout on default ports and
three isolated worktree stacks beside it, each under its own isolation ID.
Cookies and other shared namespaces
Ports isolate; localhost cookies don't. They're port-agnostic, so two
stacks of the same app expire each other's sessions on every login. Scope the cookie
name with the built-in
FED_PROJECT_ID variable,
which embeds the isolation ID, and each stack keeps its own session. The same variable
namespaces anything else parallel stacks would share behind one hostname.
If you skip isolation, fed detects occupied defaults. An interactive start prompts for
a response; a non-interactive start can fall back to alternative ports. Explicitly
enabling isolation avoids that ambiguity and randomizes every declared port. Never use
fed start --replace in a worktree: it can take the port by killing the other
checkout's services.
Workspace management (beta)
fed ws (alias for fed workspace) automates git worktree
creation and switching:
fed ws setup # One-time: install shell integration (zsh or bash)
fed ws new my-feature -b # Create branch + worktree, cd into it
fed ws list # Show all worktrees with service status
fed ws cd main # Switch to another worktree
fed ws rm my-feature # Stop services and remove worktree
fed ws prune # Clean up worktrees for deleted branches
Worktrees are created as siblings to the repo in a <repo>-worktrees/ directory. Shell integration enables auto-cd. Without it, fed ws new and fed ws cd print the path instead.
Coding agents
When your coding-agent workflow uses one git worktree per task, each worktree is its own directory and can run its own full stack (database, backend, tests) without touching yours. One rule makes it predictable: isolate first.
Don't leave that to the agent's judgment. Put it in your AGENTS.md (or CLAUDE.md) so every agent starts every worktree the same way:
## Worktrees
Run `fed isolate enable` before any other fed command in a new worktree.
It persists: every fed command after it (start, tests, scripts) gets
the worktree's own ports, containers, and volumes.
Run `fed clean` before removing a worktree, so its containers and
volumes don't outlive it.
Why fed isolate enable rather than fed start --isolate?
Agents rarely open with fed start. The first fed command is usually an
install, a code generator, or a test run. fed isolate enable isolates the
directory once, for whatever comes next; the flag only helps when the first command
happens to be start.
The rule also protects the rest of your machine. An agent that skips it hits a port
conflict on fed start. An agent trying to fix that on its own can
reach for fed start --replace, which frees the port by killing another
checkout's services. Agents follow written instructions more reliably than they
recover from collisions.
No plugin or integration is needed. Isolation is directory-scoped, so it works the same
whether the worktree came from Cursor, fed ws new, or git worktree add.
Isolated scripts
Scripts can opt into a throwaway child context with isolated: true (fresh
ports, isolated direct image-backed containers and named volumes, and automatic cleanup).
Compose limitations are covered in Scripts.
Cleaning up volumes
Isolation trades a little disk for safety: every isolated stack gets its own Docker
named volumes (fed-<id>-…). Retire a stack without stopping it first
(fed isolate rotate, a deleted worktree, an agent that moved on), and those
volumes outlive it. Run a lot of parallel stacks and the leftovers add up to real disk.
Two commands keep it in check:
fed doctor # among its checks: how many orphaned fed volumes you have
fed prune # remove them: lists first, asks before deleting; --force to skip the prompt
fed prune only ever removes fed-managed volumes attached to no container,
running or stopped, so it can't touch a live stack, or a stopped one you mean to resume.
It shows you the list first; in a terminal it asks before deleting, and in a script or
agent it deletes nothing without --force.
Most of the time you don't have to think about it: fed stop followed by
fed clean removes a stack's declared volumes, and isolated: true
scripts remove their throwaway volumes on exit. fed prune handles the rest:
the stacks you rotated, abandoned, or deleted the worktree out from under.