1 unstable release
Uses new Rust 2024
| new 0.1.0 | May 7, 2026 |
|---|
#359 in Command line utilities
115KB
3K
SLoC
isolenv
A development environment manager that doesn't conflict when run in parallel.
isolenv hands each task its own independent ports / env file / DB / Redis /
Docker Compose project. When humans, AI coding agents, and CI all hammer
the same project at the same time, ports, databases, and .env.local files
stop fighting over each other.
isolenv up issue-123 # 1 task = 1 environment
isolenv up issue-456 # gets different ports, different DB, different compose project — automatically
日本語版: README.ja.md
Problems this solves
- Two dev servers fight over port
3000 - A stray
.env.localquietly redirects your app to staging / production DB - Migrations from parallel jobs collide and seed data gets cross-contaminated
- Someone runs
docker compose down -vand wipes a teammate's working volume - AI coding agents working in multiple git worktrees clash on ports
- Frontend and backend both want
PORT=3000when run on the host
Installation
From crates.io
cargo install isolenv
Pre-built binaries
Download the archive for your platform from the
GitHub Releases page and place
the isolenv binary somewhere on your PATH.
| Platform | Asset |
|---|---|
| macOS (Apple Silicon) | isolenv-aarch64-apple-darwin.tar.gz |
| macOS (Intel) | isolenv-x86_64-apple-darwin.tar.gz |
| Linux (x86_64) | isolenv-x86_64-unknown-linux-gnu.tar.gz |
| Linux (aarch64) | isolenv-aarch64-unknown-linux-gnu.tar.gz |
| Windows (x86_64) | isolenv-x86_64-pc-windows-msvc.zip |
# macOS / Linux example (replace the target triple)
curl -L https://github.com/2ero20ne/isolenv/releases/latest/download/isolenv-aarch64-apple-darwin.tar.gz | tar xz
sudo mv isolenv /usr/local/bin/
mise
mise can install via either backend:
# Build from crates.io
mise use -g "cargo:isolenv"
# Or download a pre-built binary from GitHub Releases via ubi
mise use -g "ubi:2ero20ne/isolenv"
Nix
Without a flake in this repo, the simplest path is cargo install inside a
Rust toolchain shell:
nix-shell -p cargo rustc --run 'cargo install isolenv'
Build from source
git clone https://github.com/2ero20ne/isolenv
cd isolenv
cargo install --path .
Try it in 30 seconds
mkdir my-app && cd my-app
isolenv init # generate isolenv.yaml + docker-compose.isolenv.yml
isolenv up issue-1 # allocate ports, write env, bring up compose
isolenv status # see what's running
isolenv exec issue-1 dev # run a configured command with the task env injected
isolenv clean issue-1 --yes # tear it all down (volumes included)
.isolenv/issue-1/.env will hold things like FRONTEND_PORT, DATABASE_URL,
and project commands run via isolenv exec issue-1 ... automatically point
at the ports and DB allocated to that task.
Typical workflows
1. Run the same repo as two parallel tasks
isolenv up issue-123 # → FRONTEND_PORT=3100, DB_PORT=33300, compose project = ..._issue_123
isolenv up issue-456 # → FRONTEND_PORT=3101, DB_PORT=33301, compose project = ..._issue_456
isolenv exec issue-123 dev:frontend # serves on PORT=3100
isolenv exec issue-456 dev:frontend # serves on PORT=3101 from another terminal — no conflict
Ports and compose projects are assigned independently, so the two environments fully coexist. Three or more works the same way.
2. Wire up AI coding agents
isolenv init --agents adds AGENTS.md and CLAUDE.md to your project
with rules like "don't run docker compose directly", "don't create
.env.local", and "read ports from the env file." Claude Code, Codex,
Cursor, and most other agents pick these up at the project root, which keeps
them from making isolation-breaking mistakes.
isolenv init --agents # for new projects
isolenv print-agents >> AGENTS.md # to append into an existing AGENTS.md
Existing files are never overwritten. Merging is left to you.
3. Use it without Docker (CI, lightweight tools)
Just remove compose: from isolenv.yaml. Port allocation, env injection,
and state management still work; docker compose is never invoked.
version: 1
project: my-tool
env:
output: .isolenv/{task}/.env
services:
app:
ports:
APP_PORT:
range: [3100, 3999]
isolenv up issue-1 # no docker
isolenv exec issue-1 ... # runs straight on the host
4. Run commands inside a container
Add commands.<name>.container: <service> and that command runs via
docker compose exec <service> ... instead of on the host. Handy for
running migrations against a containerized backend.
commands:
migrate:
container: backend
run: pnpm prisma migrate dev
isolenv exec issue-1 migrate # runs inside the container
Configuration: isolenv.yaml
version: 1
project: my-project
compose:
file: docker-compose.isolenv.yml # omit to skip Docker entirely
env:
output: .isolenv/{task}/.env
inherit: [PATH, HOME, SHELL, LANG] # only inherit this allowlist from the host env
services:
app:
ports:
APP_PORT:
range: [3100, 3999] # picks a free port from this range
env:
APP_URL: "http://localhost:${APP_PORT}"
mysql:
ports:
DB_PORT:
range: [33300, 33999]
env:
DATABASE_URL: "mysql://root:password@127.0.0.1:${DB_PORT}/app"
wait:
tcp: "127.0.0.1:${DB_PORT}" # block `up` until this port accepts TCP
timeoutSeconds: 30
commands:
test: pnpm test
dev:server:
run: pnpm dev:server
env:
PORT: "${APP_PORT}" # PORT is injected only for this command
migrate:
container: app # runs via `docker compose exec app ...`
run: pnpm prisma migrate dev
dev: # run multiple commands concurrently
parallel: [dev:server, dev:worker]
guards:
forbidEnvFiles:
- .env.local # block exec/run if these exist
- .env.production
forbidHostPatterns:
- rds.amazonaws.com # block up if any generated env contains these
- production
Service readiness (wait)
Each service can declare a probe that runs after up and blocks until the
service is reachable:
wait:
tcp: "127.0.0.1:${DB_PORT}" # ready when TCP accepts
# OR
http: "http://localhost:${PORT}/health" # ready when any HTTP/1.x response comes back
timeoutSeconds: 30 # default 60
tcp and http are mutually exclusive. https:// is not supported.
Probes poll every 500 ms.
Variable interpolation
${VAR} references in service env: values resolve transitively — they
can refer to ports or to other env vars. Undefined or circular references
fail loudly.
What ends up in the env file
.isolenv/<task>/.env always contains:
ISOLENV_TASK/ISOLENV_PROJECT/ISOLENV_COMPOSE_PROJECT- All allocated port vars (
FRONTEND_PORT=3100,DB_PORT=33300, …) - All
services.*.enventries (interpolated)
PORT is never set globally (so frontend and backend can both run on
the host without colliding). Inject it per-command via
commands.<name>.env: { PORT: "${FRONTEND_PORT}" } instead.
CLI reference
| Command | What it does |
|---|---|
isolenv init [--agents] |
Generate skeleton config files. --agents also writes AGENTS.md / CLAUDE.md. Existing files are never overwritten. |
isolenv up <task> [--no-start] |
Allocate ports, write env, start compose. --no-start skips docker compose up. |
isolenv down <task> |
Stop compose. Volumes are preserved. |
isolenv clean <task> --yes |
Stop + remove volumes + remove .isolenv/<task>/. |
isolenv clean --all --yes |
Clean every task in this project. |
isolenv exec <task> <name> [-- extra args] |
Run a command from commands with the task env. |
isolenv run <task> -- <command...> |
Run an arbitrary command with the task env. |
isolenv list-commands |
List commands and where each runs (host / container / parallel). |
isolenv print-env <task> [--export] |
Print the env file to stdout (--export for shell eval). |
isolenv print-agents |
Print the canonical AGENTS.md / CLAUDE.md snippet to stdout. |
isolenv status |
Show all tasks and their state. |
isolenv doctor |
Diagnose config / Docker / guards. |
Safety nets
isolenv stops commands when it detects a setup that would break isolation:
guards.forbidEnvFiles: refusesexec/runwhen files like.env.localexist (prevents frameworks from silently loading production credentials).guards.forbidHostPatterns: refusesupif any generated env value contains a forbidden substring (e.g.rds.amazonaws.com,production).- Compose is always invoked with
COMPOSE_PROJECT_NAMEset, andcleanrefuses to rundocker compose down -vwithout an explicit project name. There is no path that wipes volumes without naming a task. - Port allocation goes through an OS-level file lock, so two
isolenv upinvocations never assign the same port to different tasks. - If
upfails partway, ports do not leak —isolenv clean <task> --yesalways recovers.
isolenv doctor additionally warns if env.inherit allowlists obviously
risky vars like DATABASE_URL, AWS_*, or KUBECONFIG.
Layout
.
├── isolenv.yaml # config
├── docker-compose.isolenv.yml # compose file isolenv invokes
├── AGENTS.md / CLAUDE.md # generated by `init --agents` (optional)
└── .isolenv/
├── lock.json # global port reservations (don't edit by hand)
└── <task-id>/
├── .env # task env
└── state.json # task state
AGENTS.md and CLAUDE.md are generated with identical content by
init --agents (Claude Code reads CLAUDE.md; Codex / Cursor / etc. read
AGENTS.md). Delete whichever one your project doesn't use.
License
MIT OR Apache-2.0
Dependencies
~3.5–5.5MB
~99K SLoC