{"id":50282660,"url":"https://github.com/hefgi/ecluse","last_synced_at":"2026-06-11T00:01:29.656Z","repository":{"id":357631593,"uuid":"1237836502","full_name":"hefgi/ecluse","owner":"hefgi","description":"Ephemeral local dev environments for coding agents — works whether your stack lives in Docker, on the host, or a mix. Run agents in parallel; each worktree gets isolated ports and data. No collisions, clean teardown.","archived":false,"fork":false,"pushed_at":"2026-06-08T16:36:45.000Z","size":1450,"stargazers_count":9,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-08T18:23:59.097Z","etag":null,"topics":["agentic-engineering","ai","coding-agent","ephemeral-environments","worktree-isolation"],"latest_commit_sha":null,"homepage":"https://hefgi.github.io/ecluse/","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hefgi.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-05-13T14:55:17.000Z","updated_at":"2026-06-08T16:36:49.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/hefgi/ecluse","commit_stats":null,"previous_names":["hefgi/ecluse"],"tags_count":17,"template":false,"template_full_name":null,"purl":"pkg:github/hefgi/ecluse","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hefgi%2Fecluse","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hefgi%2Fecluse/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hefgi%2Fecluse/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hefgi%2Fecluse/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hefgi","download_url":"https://codeload.github.com/hefgi/ecluse/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hefgi%2Fecluse/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34174148,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-10T02:00:07.152Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agentic-engineering","ai","coding-agent","ephemeral-environments","worktree-isolation"],"created_at":"2026-05-28T01:00:24.036Z","updated_at":"2026-06-11T00:01:29.649Z","avatar_url":"https://github.com/hefgi.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n\u003cimg src=\"banner.png\" alt=\"ecluse\" width=\"600\" /\u003e\n\n**Ephemeral local environments for coding agents — any stack.**\n\nEach git worktree gets its own slot — isolated ports, isolated services, isolated data.\nWorks whether your stack runs in Docker, on the host, or a mix. No collisions, clean teardown.\n\n[![CI](https://github.com/hefgi/ecluse/actions/workflows/ci.yml/badge.svg)](https://github.com/hefgi/ecluse/actions/workflows/ci.yml)\n[![Crates.io](https://img.shields.io/crates/v/ecluse.svg)](https://crates.io/crates/ecluse)\n[![Homebrew](https://img.shields.io/badge/homebrew-hefgi%2Ftap-orange)](https://github.com/hefgi/homebrew-tap)\n[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)\n[![Docs](https://img.shields.io/badge/docs-ecluse.ai-blue)](https://ecluse.ai/)\n\n---\n\n**Built for coding agents running tasks in parallel.**\n\n![Claude Code](https://img.shields.io/badge/Claude_Code-d97706?style=flat-square)\n![Cursor](https://img.shields.io/badge/Cursor-000?style=flat-square)\n![Codex](https://img.shields.io/badge/Codex-10a37f?style=flat-square)\n![OpenCode](https://img.shields.io/badge/OpenCode-6366f1?style=flat-square)\n![Pi](https://img.shields.io/badge/Pi-333?style=flat-square)\n\nand any agent that can run shell commands.\n\n\u003c/div\u003e\n\n## The problem\n\nYou're running 4 Claude Code sessions in parallel. Each agent finishes its task and wants to verify — run the test suite, spin up the app, hit the endpoints. But port 3000 is taken. Agent 2 kills agent 1's server. Agent 3 waits. The verification loop that was supposed to run in parallel is now sequential. You're paying for 4 agents and getting the throughput of one.\n\necluse gives each agent its own slot: isolated ports, its own services, its own infra. All 4 agents spin up, verify, and tear down independently. The full AI verification loop — build, migrate, test, e2e — runs in parallel, without collisions, without waiting.\n\n\u003cdiv align=\"center\"\u003e\n\n**Create worktree → Spin up env → Do work → Verify → PR → Teardown**\n\n\u003c/div\u003e\n\n```bash\necluse up feat-foo    # new worktree, isolated ports, isolated services\necluse up fix-bar     # parallel session, different slot, zero collisions\necluse down feat-foo  # clean teardown, nothing left behind\n```\n\n\u003e ecluse is French for \"canal lock\" — each session gets its own chamber, everything is isolated, nothing leaks between them.\n\n## Install\n\n[![Homebrew](https://img.shields.io/badge/Homebrew-FBB040?style=flat-square\u0026logo=homebrew\u0026logoColor=black)](https://github.com/hefgi/homebrew-tap)\n\n```bash\nbrew install hefgi/tap/ecluse\n```\n\n[![Crates.io](https://img.shields.io/badge/cargo-install-orange?style=flat-square\u0026logo=rust\u0026logoColor=white)](https://crates.io/crates/ecluse)\n\n```bash\ncargo install ecluse\n```\n\nThen install the agent skill:\n\n```bash\nnpx skills add hefgi/ecluse -g\n```\n\nRequires Rust 1.85+. For container and hybrid modes, [OrbStack](https://orbstack.dev) is recommended over Docker Desktop on macOS — faster, less memory.\n\n## Get started\n\n```bash\ncd my-project\necluse init              # detects mode, writes .ecluse.toml\necluse up feat-foo       # creates worktree + slot\necluse shell feat-foo    # drops into worktree with env loaded\nnpm run dev              # PORT already set — app binds to its own port\n```\n\n`ecluse init` writes a `.ecluse.toml` at repo root. Here's what a typical one looks like:\n\n```toml\nmode = \"hybrid\"          # container | host | hybrid\n\n[[services]]\nname = \"api\"\nbase_port = 3000         # slot 1 → PORT=3001, slot 2 → PORT=3002\ncommand = \"npm run dev\"  # ecluse spawns this; each session gets its own port\n\n[[services]]\nname = \"postgres\"\nrun = \"docker\"\nbase_port = 5432         # slot 1 → ECLUSE_POSTGRES_PORT=5433, slot 2 → 5434\n\n[[services]]\nname = \"redis\"\nrun = \"docker\"\nbase_port = 6379         # slot 1 → ECLUSE_REDIS_PORT=6380, slot 2 → 6381\n```\n\nEach `ecluse up` picks the next free slot, starts isolated services, and writes all ports to `.env.ecluse` in the worktree. Type `exit` (or `ecluse down`) to tear everything down.\n\n## Choosing a mode\n\n`ecluse init` detects the right mode automatically. You confirm before anything is written.\n\n| Mode | What `ecluse up` does | Best for |\n|---|---|---|\n| `container` | Runs all services in Docker (app + data) | Fully containerized stacks, devcontainer repos |\n| `hybrid` | Runs data services in Docker, writes env, optionally spawns app | Rails/Django/Node with a postgres+redis compose file |\n| `host` | Writes env vars, optionally spawns native services | Pure native stacks with no Docker |\n\n## How it works\n\nThe central concept is a **slot** — an integer from 1 to `max_slots`. Every resource is derived from the slot:\n\n- Per-service port: `base_port + slot` (e.g. `api` at `base_port=3000`, slot 1 → 3001, slot 2 → 3002)\n- Compose project name: `\u003cprefix\u003e_\u003cslug\u003e`\n- Named volumes: `\u003cvolume\u003e_\u003cprefix\u003e_\u003cslug\u003e`\n\nThree thin mode implementations share this slot primitive. Mode is selected once at `init` time and stored in `.ecluse.toml`.\n\n**How services are started depends on mode:**\n\n- `container` — everything runs via Docker Compose. ecluse generates a per-slot overlay and calls `docker compose up`.\n- `host` / `hybrid` — native services are spawned using your system's process manager. ecluse uses **tmux** if available (one detached session per slot, one window per service), falling back to **nohup** otherwise (background processes with logs at `.ecluse/logs/\u003cslug\u003e/`). Docker data services in hybrid mode still go through Compose. Set `command` on a `[[services]]` entry to opt in; services without `command` are not spawned.\n\n## Commands\n\n```\necluse init [--mode container|host|hybrid] [--explain] [--yes]\necluse up [\u003cslug\u003e] [--watch] [--json] [--reuse-worktree] [--port \u003cname\u003e=\u003cvalue\u003e] [--services \u003cname\u003e,...] [--force] [--skip \u003cname\u003e,...]\necluse sync [\u003cslug\u003e] [--json]\necluse shell \u003cslug\u003e\necluse env [\u003cslug\u003e]\necluse down [\u003cslug\u003e] [--keep-volumes] [--keep-branch] [--keep-worktree] [--delete-worktree]\necluse shutdown [--keep-volumes] [--keep-worktrees] [--delete-worktrees]\necluse flush [--yes]\necluse ls [--json]\necluse validate [--ports]\necluse status [\u003cslug\u003e] [--json] [--quiet]\n```\n\n**Env** — get the worktree path and all env vars for a running session as JSON:\n\n```bash\necluse env feat-foo          # full JSON: worktree_path, slot, all ECLUSE_* vars\necluse env                   # auto-detects session if run from inside a worktree\n```\n\n**Branch names as argument** — pass your git branch name directly; ecluse sanitizes it to a valid slug and uses the original as the branch:\n\n```bash\necluse up feat/add-auth   # slug=feat-add-auth, branch=feat/add-auth\necluse up                 # inside a git worktree → auto-detects branch from cwd\necluse up                 # in repo root / main worktree → prompts for branch name\n```\n\n**Auto-register existing worktrees** — running `ecluse up` from inside any git worktree (even one not created by ecluse) auto-detects the branch, registers the session, and starts services. No `--reuse-worktree` flag needed.\n\n**Idempotent up** — `ecluse up` is safe to run on an existing session. It reuses the worktree and slot, checks which services are running, and starts only the ones that are down. Slug is auto-detected from cwd:\n\n```bash\necluse up feat-foo    # existing session: starts only downed services, skips running ones\necluse up             # same, slug auto-detected from cwd\necluse up --force     # kill all running services on allocated ports, restart all\necluse up --skip api  # skip api; start everything else\n```\n\n**Soft restart** — tear down services without losing your worktree, then spin them up fresh:\n\n```bash\necluse down feat-foo --keep-worktree   # services torn down, worktree + branch kept\necluse up feat-foo --reuse-worktree    # new slot, fresh ports, worktree reused\n```\n\n**Port override** — pin a specific service to a port for this session (useful when the auto-assigned port conflicts with something ecluse can't detect):\n\n```bash\necluse up feat-foo --port api=4001 --port postgres=5444\n```\n\n## Configuration\n\n`.ecluse.toml` lives at repo root, written by `ecluse init`:\n\n```toml\nmode = \"hybrid\"\nmax_slots = 8\nprefix = \"ecluse\"\nworktree_dir = \".ecluse/worktrees\"\n\n# Env file inheritance — materialized from repo root into each worktree (default: both symlinked)\n# inherit_env = [\".env\", \".env.local\"]   # set to [] to opt out\n# inherit_env = [\".env\", { file = \".env.local\", mode = \"copy\" }]  # .env.local copied once,\n#                                                                  # per-worktree edits stay local\n\n# Port collision handling (both optional)\n# strict_port = false        # default: search for a free port on collision\n# port_search_range = 10     # how many alternatives to try (bump by max_slots each time)\n\n# One [[services]] block per service. port = base_port + slot.\n# Native services run on the host; docker services run in containers.\n# The first native entry also sets the PORT alias for framework compatibility.\n# Add command = \"...\" to have ecluse spawn the process on ecluse up.\n\n[[services]]\nname = \"api\"\nbase_port = 3000             # slot 1 → ECLUSE_API_PORT=3001 + PORT, slot 2 → 3002\ncommand = \"npm run dev\"      # optional — ecluse spawns this on ecluse up\n                             # omit for port-allocation-only (start process yourself)\n# port_env = \"DJANGO_PORT\"  # also inject the port under a custom var name\n# port_env = [\"DJANGO_PORT\", \"APP_PORT\"]  # or multiple aliases\n\n[[services]]\nname = \"postgres\"\nrun = \"docker\"\nbase_port = 5432             # slot 1 → ECLUSE_POSTGRES_PORT=5433, slot 2 → 5434\n\n# Optional: lifecycle hooks — run in the worktree with all env vars set\n[hooks]\non_up = \"npx prisma migrate deploy\"\non_down = \"npx prisma migrate reset --force\"\n```\n\n`ecluse init` writes `~/.config/ecluse/config.toml` with the detected process manager (`tmux` if installed, otherwise `nohup`). Services with `command` are spawned on `ecluse up` and killed on `ecluse down`. Set `process_manager = \"none\"` to opt out.\n\n**`[[services]]` for monorepos and multi-service stacks:** define one block per service. Each gets a stable, collision-free port per slot (`base_port + slot`). Omit `[[services]]` entirely for single-service projects — ecluse falls back to a single `PORT = 3000 + slot`.\n\n**Multiple compose files in a monorepo:** point each docker service at its own compose file with the `compose` field (path relative to repo root). Services without `compose` fall back to the root compose file. ecluse generates one overlay per compose file and brings them all up under the same project name.\n\n```toml\n[[services]]\nname = \"api\"\nbase_port = 3000               # native — no compose needed\n\n[[services]]\nname = \"postgres\"\nrun = \"docker\"\nbase_port = 5432               # uses root docker-compose.yml (default)\n\n[[services]]\nname = \"worker-queue\"\nrun = \"docker\"\nbase_port = 6379\ncompose = \"services/worker/docker-compose.yml\"   # its own compose file\n```\n\n**Port collision handling** — by default ecluse searches for a free port if the nominal one is taken, trying `nominal + i × max_slots` to stay out of other slots' territory. Set `strict_port = true` to fail immediately instead. Run `ecluse validate` to check your config and preview the full port allocation table.\n\nHooks run as shell commands inside the worktree directory with all `.env.ecluse` variables pre-loaded. Use them for migrations, seeding, or teardown. ecluse doesn't manage databases directly — your app's own tooling handles that via `on_up`.\n\n## Known limits\n\n**Ports are checked, not reserved.** ecluse finds a free port at `ecluse up` time and writes it to `.env.ecluse`. There is a small window between the check and when your process actually binds — if something else takes the port in between, the port in `.env.ecluse` will be wrong. The fix is to tear down and recreate the session:\n\n```bash\necluse down feat-foo --keep-worktree\necluse up feat-foo --reuse-worktree\n```\n\nOr pin a specific port manually:\n\n```bash\necluse up feat-foo --port api=4001\n```\n\n**Process management is spawn-and-kill only.** For `host` and `hybrid` modes, services with `command` are spawned on `up` and killed on `down`. ecluse does not monitor or restart crashed processes — `ecluse ls` warns if a nohup-managed process has died. For a fresh start, use `ecluse down feat-foo --keep-worktree \u0026\u0026 ecluse up feat-foo --reuse-worktree`.\n\n**`command` only works if the app reads its port from the environment.** ecluse injects the full `.env.ecluse` contents (all `ECLUSE_*` vars, `PORT`, `port_env` aliases) directly into the spawned process environment — no separate sourcing needed. It cannot help if:\n- The port is **hardcoded in source code** — the app must be changed to read `$PORT`.\n- The port is **set in a config file** (e.g. `config/puma.rb`, `vite.config.ts`, `.env`) — ecluse does not modify app config files; update the config to read from the environment instead.\n\nIf the app reads a custom env var, use `port_env` to inject it under that name:\n```toml\nport_env = \"DJANGO_PORT\"                  # single alias\nport_env = [\"DJANGO_PORT\", \"APP_PORT\"]    # multiple aliases\n```\n\nIf the framework accepts a CLI flag, pass the var through the command:\n```toml\ncommand = \"next dev --port $PORT\"\ncommand = \"bundle exec rails s -p $PORT\"\n```\n\n## Contributing\n\nIssues and PRs are welcome. Check the [open issues](https://github.com/hefgi/ecluse/issues) for ideas — good first issues are tagged. If you're adding a new isolation mode or provider, open an issue first to discuss the approach.\n\n## License\n\nApache 2.0. See [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhefgi%2Fecluse","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhefgi%2Fecluse","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhefgi%2Fecluse/lists"}