{"id":50909115,"url":"https://github.com/renchris/worktree-harness","last_synced_at":"2026-06-16T08:02:51.402Z","repository":{"id":362346133,"uuid":"1258634428","full_name":"renchris/worktree-harness","owner":"renchris","description":"Parallel coding-agent sessions in isolated git worktrees, with safe fast-forward-only merge-back. Zero deps beyond git + bash.","archived":false,"fork":false,"pushed_at":"2026-06-03T20:10:06.000Z","size":631,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-03T21:08:37.716Z","etag":null,"topics":["ai-agents","bash","claude-code","cli","coding-agents","developer-tools","git-worktree","parallel-agents"],"latest_commit_sha":null,"homepage":null,"language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/renchris.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":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-03T19:11:48.000Z","updated_at":"2026-06-03T20:10:10.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/renchris/worktree-harness","commit_stats":null,"previous_names":["renchris/worktree-harness"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/renchris/worktree-harness","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/renchris%2Fworktree-harness","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/renchris%2Fworktree-harness/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/renchris%2Fworktree-harness/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/renchris%2Fworktree-harness/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/renchris","download_url":"https://codeload.github.com/renchris/worktree-harness/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/renchris%2Fworktree-harness/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34396432,"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-16T02:00:06.860Z","response_time":126,"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":["ai-agents","bash","claude-code","cli","coding-agents","developer-tools","git-worktree","parallel-agents"],"created_at":"2026-06-16T08:02:50.482Z","updated_at":"2026-06-16T08:02:51.397Z","avatar_url":"https://github.com/renchris.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# worktree-harness\n\n**Run many coding agents in parallel — each in its own git worktree, each fast-forwarded back to your default branch safely — with nothing on your machine but `git` and `bash`.** Claude Code, Aider, Codex, whatever; the worktrees don't care.\n\n[![ci](https://github.com/renchris/worktree-harness/actions/workflows/ci.yml/badge.svg)](https://github.com/renchris/worktree-harness/actions/workflows/ci.yml)\n\n![worktree-harness demo: new → status → merge → gc](docs/demo.gif)\n\n▶ **[Watch the interactive player](https://renchris.github.io/worktree-harness/)** — crisp, selectable-text playback (asciinema).\n\n```console\n$ worktree-harness new add-auth\n→ fetch origin main\n→ git worktree add ~/.worktrees/myapp/add-auth -b add-auth origin/main\n✓ copied 1 gitignored file(s)\n→ install: pnpm install --frozen-lockfile\n✓ ports offset by 1 → ~/.worktrees/myapp/add-auth/.harness.env\n✓ worktree ready: ~/.worktrees/myapp/add-auth  (branch: add-auth)\n\n# ...work in the worktree, commit, then:\n$ worktree-harness merge        # rebase onto main, run your gate, fast-forward main. never pushes.\n$ worktree-harness gc --prune   # reap merged + idle worktrees (your branches are kept)\n```\n\nThree agents, three branches, three clean fast-forwards onto your local `main` — and nothing pushed until you say so:\n\n```\n time ──────────────────────────────────────────────────────►\n add-auth:   new·edit·commit──merge───────────────────────gc\n fix-123:        new·edit·commit────────merge─────────────gc\n refactor:           new·edit·commit──────────────merge───gc\n\n main:              A───────────B───────────────────C────►\n                    ▲           ▲                    ▲\n               ff-only      ff-only              ff-only      (never pushes)\n```\n\n---\n\n## Running parallel agents on one checkout breaks in five specific ways\n\nYou're running more than one coding agent at once — Claude Code in one pane, Aider in another — against the same repo. On a single shared checkout, five concrete things go wrong:\n\n- **One git index, shared.** One session's `git add -A` (or an agent committing \"all changes\") sweeps another session's half-staged work into the wrong commit.\n- **Ports collide.** Two dev servers want `:3000`; two inspectors want `:9229`.\n- **A fresh worktree won't run.** `git worktree add` gives you a tree with no `.env`, no `node_modules`, no local DB — it won't start without hand-wiring.\n- **Landing the branch back is where the damage happens.** A non-fast-forward clobber of `main`, a \"looked merged but never typechecked\" regression, or an accidental push to a stale remote.\n- **Existing tooling assumes `origin` is canonical** and silently no-ops on non-GitHub remotes — wrong for the solo / diverged-fork workflow where your *local* branch is the truth.\n\nThe fix is one idea — **give each session its own git worktree** — and four disciplines that follow from it. Each worktree has its own index, HEAD, and reflog, so the collision isn't *coordinated away*, it's *impossible*. `worktree-harness` is the small, dependency-free glue that makes that worktree runnable and safe to land — and nothing more.\n\n## Isolation is structural, not cooperative\n\nEach session gets its own worktree at `~/.worktrees/\u003crepo\u003e/\u003cbranch\u003e`, with its own index, HEAD, and reflog, all backed by the one shared object store. The staged-file collision isn't prevented by a lock — there is no shared staging area to collide on, so it cannot happen.\n\n```\n you, in N panes        (type `claude`, or `worktree-harness launch claude`)\n ┌───────────┬───────────┬───────────┐\n │  pane 1   │  pane 2   │  pane 3   │\n └─────┬─────┴─────┬─────┴─────┬─────┘\n       │           │           │   from the MAIN checkout (.git is a dir) → isolate\n       ▼           ▼           ▼\n ┌───────────┬───────────┬───────────┐\n │  wt: A    │  wt: B    │  wt: C    │  separate working tree\n │ branch A  │ branch B  │ branch C  │  separate index · HEAD · reflog\n │ base:main │ base:main │ base:main │  branched off the base at creation\n │ own ports │ own ports │ own ports │  .env copied · deps installed · .harness.env\n └─────┬─────┴─────┬─────┴─────┬─────┘\n       └───────────┼───────────┘\n                   ▼\n        one shared  \u003crepo\u003e/.git    (objects + refs; content-addressed, atomic)\n        refs/heads/{ main, A, B, C }\n```\n\nThat is why there is **no lock and no daemon**. The only shared mutable thing left is `refs/heads/*`, and git already serializes ref updates with a per-ref lock held for microseconds. (An earlier version of this idea carried a session-long writer lock; it blocked other sessions for zero safety benefit — the worktrees had already removed the collision it guarded — so it was deleted.)\n\nThe launcher follows the same principle: from your main checkout it isolates *before* running your agent, and if it can't, it **refuses to launch** rather than run un-isolated.\n\n```mermaid\nflowchart TD\n    A[type claude / worktree-harness launch] --\u003e B{in a git repo?}\n    B -- no --\u003e P[exec in place — nothing to isolate]\n    B -- linked worktree --\u003e P2[exec in place — already isolated, never nest]\n    B -- MAIN checkout --\u003e C[create a worktree off the base, provision, exec there]\n    C -- success --\u003e D[cd worktree → exec the agent]\n    C -- FAILURE --\u003e R[REFUSE to launch, exit ≠ 0 — never runs un-isolated]\n```\n\nA guard that silently does nothing when it fails is worse than no guard, because you *think* you're protected. Isolation here fails closed: no worktree, no launch (`WH_ISOLATION_SKIP=1` opts out explicitly).\n\n## A fresh worktree arrives runnable\n\n`git worktree add` hands you an empty tree; `new` makes it actually work, in the order you'd do it by hand:\n\n- **Copies your gitignored state** — the files you declare in `HARNESS_COPY` (`.env`, `.env.local`, …), with `chmod 0600` on anything matching `*.env*` so secrets aren't world-readable.\n- **Runs the frozen-lockfile install** for your package manager, auto-detected from the lockfile: pnpm, npm, yarn (classic + berry), bun, uv, poetry, pipenv, pip, cargo, go.\n- **Assigns non-colliding ports** by offset and writes them to `\u003cworktree\u003e/.harness.env` — `source` it and your dev server and inspector won't fight the other worktrees.\n- **Runs your setup** — whatever you put in `HARNESS_SETUP` (e.g. `pnpm db:setup`).\n\n`new` prints the worktree path on **stdout** (everything else is stderr), so you can capture it: `wt=\"$(worktree-harness new feat)\"`.\n\n## Landing back is fast-forward-only, gated, and never pushed\n\n`merge`, run from inside the worktree, makes the one boring, safe move:\n\n```mermaid\nflowchart TD\n    M[worktree-harness merge — from inside the worktree] --\u003e A{working tree clean?}\n    A -- no --\u003e A1[stop: commit explicit paths first]\n    A -- yes --\u003e B{already landed?}\n    B -- yes --\u003e B1[done — nothing to do]\n    B -- no --\u003e C[rebase the branch onto the base]\n    C -- conflict git cannot auto-resolve --\u003e C1[stop: how to resolve / abort — base untouched]\n    C -- clean --\u003e D{is default branch an ancestor of the rebased branch?}\n    D -- no --\u003e D1[REFUSE — base diverged; the move would not be a fast-forward]\n    D -- yes --\u003e E[run HARNESS_MERGE_GATE on the rebased tree]\n    E -- fails --\u003e E1[stop — base NOT updated]\n    E -- passes --\u003e F[fast-forward the LOCAL default branch to the branch tip]\n    F --\u003e G[done — NEVER pushes]\n```\n\nThree properties are deliberate:\n\n1. **Fast-forward only.** The branch is rebased onto the base, then an ancestry check confirms the move is a true fast-forward before anything is touched. A diverged base is refused loudly, not forced.\n2. **Gated.** A clean *textual* rebase can still be *semantically* broken, so `HARNESS_MERGE_GATE` (your typecheck/test command) runs on the rebased tree *before* the default branch moves — catching breakage a plain merge would hide.\n3. **Never pushes.** Landing is local; pushing stays a separate, explicit `git push` you run when you mean it. (This is a direct answer to a real footgun in a popular alternative, whose \"checkout\" auto-pushes to the remote.)\n\nConcurrent merge-backs serialize naturally — each is a fast-forward of one ref, ordered by git's per-ref lock in microseconds; a non-fast-forward is rejected, so the loser of a race just re-rebases and retries. Cleanup is conservative by construction: `gc` removes a worktree only if it's merged **and** clean **and** idle **and** not open by a live process — anything else is kept with a printed reason, and **your branches are always preserved.**\n\n## The only thing it adds to your machine is git and bash\n\n- **Zero runtime dependencies.** No daemon, no Go or Rust binary, no YAML parser. `.harnessrc` is sourced as bash — arrays and comments for free — and the trust model is identical to a `Makefile`: run it only in repos you trust.\n- **Agent-agnostic.** Claude Code, Aider, Codex, anything else — set `HARNESS_AGENT` or pass `launch -- \u003ccmd\u003e`, and list several in `WORKTREE_HARNESS_AGENTS` for the shell integration.\n- **First-class for the local-canonical workflow.** When `origin` is a stale fork and your *local* default branch is the truth, set `HARNESS_BASE_POLICY=local`: the tool branches and rebases off your local tip and never touches the network — exactly the case native `claude -w` silently mishandles ([#27947](https://github.com/anthropics/claude-code/issues/27947)).\n\nSee [`docs/DESIGN.md`](docs/DESIGN.md) for the full argument and [`docs/THREAT_MODEL.md`](docs/THREAT_MODEL.md) for what it trusts and protects.\n\n## Install\n\n**Homebrew** (macOS / Linux):\n\n```sh\nbrew install renchris/tap/worktree-harness\n```\n\n**Or** the zero-dependency installer:\n\n```sh\ncurl -fsSL https://raw.githubusercontent.com/renchris/worktree-harness/main/install.sh | sh\n```\n\nOr from a checkout:\n\n```sh\ngit clone https://github.com/renchris/worktree-harness \u0026\u0026 ./worktree-harness/install.sh\n```\n\nIt installs to `~/.local` by default (override with `WORKTREE_HARNESS_PREFIX`): files go under `~/.local/share/worktree-harness`, and the `worktree-harness` command is symlinked into `~/.local/bin`. No sudo, no global state. To uninstall, delete those two paths.\n\n**Requirements:** `git` and `bash` ≥ 3.2 (i.e. stock macOS `/bin/bash`) — that's it.\n\n## Quickstart\n\n```sh\ncd your-project\nworktree-harness init          # scaffold a .harnessrc (detects your package manager)\n$EDITOR .harnessrc             # tweak: which files to copy, ports, merge gate\n\nworktree-harness new fix-123   # runnable worktree at ~/.worktrees/your-project/fix-123\ncd ~/.worktrees/your-project/fix-123\nsource .harness.env            # if you configured ports\n# ...run your agent, commit...\n\nworktree-harness merge         # land it on local main (rebase + gate + ff; never pushes)\nworktree-harness gc --prune    # tidy up\n```\n\n### Auto-isolating launcher (optional)\n\nMake your agent command launch inside a fresh worktree automatically — from the\nmain checkout it isolates; from a linked worktree or outside a repo it runs in\nplace:\n\n```sh\n# in ~/.zshrc (or ~/.bashrc; fish supported too)\nexport WORKTREE_HARNESS_AGENTS=\"claude\"   # space-separated; e.g. \"claude aider\"\nsource ~/.local/share/worktree-harness/share/shell/zsh.sh\n```\n\nNow typing `claude` in a project root spins up an isolated worktree and launches\nthe agent there. Override once with `WH_ISOLATION_SKIP=1 claude`.\n\n### Shell completions\n\nThe Homebrew formula installs bash, zsh, and fish completions automatically. With\nthe `curl` installer they ship under\n`~/.local/share/worktree-harness/share/completions/` — source the one for your\nshell (e.g. add `source .../worktree-harness.bash` to `~/.bashrc`).\n\n## Configure once in `.harnessrc`\n\nA `.harnessrc` in your repo root, committed so every worktree inherits it. It's\n**sourced as bash** (zero parsing dependencies; arrays and comments for free).\nTreat it like a `Makefile`: only run `worktree-harness` in repos you trust.\nScaffold one with `worktree-harness init`. Every key is optional.\n\n| Key | Default | Meaning |\n|---|---|---|\n| `HARNESS_BASE_POLICY` | `origin` | `origin` = fetch and branch off `\u003cremote\u003e/\u003cdefault\u003e`; `local` = branch off the local default, never touch the network |\n| `HARNESS_REMOTE` | `origin` | Remote used when policy is `origin` |\n| `HARNESS_DEFAULT_BRANCH` | *(auto)* | Blank → auto-detect (`origin/HEAD`, then `main`/`master`) |\n| `HARNESS_WORKTREE_HOME` | `$HOME/.worktrees` | Worktrees live at `\u003chome\u003e/\u003crepo\u003e/\u003cbranch\u003e` |\n| `HARNESS_COPY` | `()` | Gitignored files copied (not symlinked) into each worktree, e.g. `( \".env\" \".env.local\" )` |\n| `HARNESS_PACKAGE_MANAGER` | `auto` | `auto` detects from the lockfile; or pin `pnpm`/`npm`/`yarn`/`bun`/`pip`/`poetry`/`uv`/`cargo`/`go`/`none` |\n| `HARNESS_INSTALL` | `auto` | `auto` derives the frozen-lockfile command from the PM; `none`; or a literal command |\n| `HARNESS_SETUP` | `()` | Commands run inside the worktree after install, e.g. `( \"pnpm db:setup\" )` |\n| `HARNESS_PORTS` | `()` | `\"NAME=BASE\"` → `NAME=BASE+offset`, written to `\u003cworktree\u003e/.harness.env` |\n| `HARNESS_MERGE_GATE` | `none` | Command that must pass before `merge` fast-forwards, e.g. `\"pnpm typecheck\"` |\n| `HARNESS_AGENT` | `claude` | What `launch` execs inside the worktree |\n| `HARNESS_GC_KEEP` | `()` | Worktree basenames `gc` must never reap |\n| `HARNESS_GC_IDLE_MIN` | `30` | `gc` keeps worktrees touched more recently than this (minutes; `0` disables) |\n\n\u003e **origin vs. local.** The default (`origin`) is right for the common case: a\n\u003e team repo where `origin/main` is canonical. If your `origin` is a diverged\n\u003e fork and your **local** default branch is the source of truth (a solo,\n\u003e rarely-push workflow), set `HARNESS_BASE_POLICY=\"local\"` and the tool branches\n\u003e and rebases off your local tip, never fetching. This is the case native\n\u003e `claude -w` silently mishandles.\n\nSee [`examples/`](examples/) for ready-to-copy configs (Node/pnpm, Python/uv,\nand a multi-service setup).\n\n## Commands\n\n| Command | What it does |\n|---|---|\n| `new \u003cbranch\u003e` | Create + provision a worktree off the base branch. `--dry-run` to preview. |\n| `launch [-- \u003ccmd…\u003e]` | Create an auto-named worktree and exec your agent in it; in a linked worktree or outside a repo, exec in place. |\n| `merge [--no-verify]` | Rebase the current worktree's branch onto the base, run the gate, fast-forward the local default branch. Never pushes. |\n| `status [--porcelain]` | List worktrees: branch, ahead/behind, dirty, assigned ports. |\n| `gc [--prune]` | Preview (default) or remove merged + clean + idle worktrees. Branches always preserved. |\n| `init` | Scaffold a `.harnessrc` with your detected package manager. |\n| `doctor` | Diagnose the environment and validate `.harnessrc`. |\n\nGlobal flags: `-C \u003cdir\u003e` (run as if in `\u003cdir\u003e`), `--dry-run`, `--no-color`.\n\n## How it compares\n\nThere's a healthy ecosystem here; `worktree-harness` deliberately owns the\n*lifecycle-safety* corner the others leave open. (Comparison reflects public\nbehavior as of mid-2026; details change — corrections welcome.)\n\n| | worktree-harness | [claude-squad] | [worktrunk] | native `claude -w` |\n|---|:---:|:---:|:---:|:---:|\n| Isolation via git worktrees | ✓ | ✓ (+tmux) | ✓ | ✓ |\n| Provision: copy env, install, **ports** | ✓ | ✗ ([#260]) | ✓ | partial¹ |\n| **Frozen-lockfile** install auto-detected | ✓ | ✗ | ✗ | ✗ |\n| Safe merge-back: **ff-only + verify gate** | ✓ | ✗ (manual) | partial² | ✗ |\n| **Never pushes** on merge | ✓ | ✗ (auto-push, [#122]) | n/a | n/a |\n| **Diverged-origin / local-main** first-class | ✓ | ✗ | ✗ | ✗ ([#27947]³) |\n| Runtime dependencies | git + bash | Go binary | Rust binary | Claude Code |\n| Agent-agnostic | ✓ | ✓ | ✓ | Claude only |\n| Headless / SSH / CI friendly | ✓ | ✓ | ✓ | partial |\n\n¹ `.worktreeinclude` copies files (desktop; CLI parity tracked upstream) — no\nports/DB. ² `worktrunk merge` runs hooks but doesn't enforce fast-forward-only\nor a typecheck gate. ³ native `--worktree` silently no-ops on non-GitHub remotes\nand assumes origin is canonical.\n\nIf you want a polished TUI/GUI to *watch* many agents, reach for\n[claude-squad]/[Conductor]. If you want a heavier config-driven Rust tool, see\n[worktrunk]. `worktree-harness` is the small, dependency-free shell tool for the\nsafe-lifecycle path — especially if your local branch is canonical.\n\n[claude-squad]: https://github.com/smtg-ai/claude-squad\n[worktrunk]: https://github.com/max-sixty/worktrunk\n[Conductor]: https://www.conductor.build/\n[#260]: https://github.com/smtg-ai/claude-squad/issues/260\n[#122]: https://github.com/smtg-ai/claude-squad/issues/122\n[#27947]: https://github.com/anthropics/claude-code/issues/27947\n\n## FAQ\n\n**Does `merge` ever push?** No. It lands on your *local* default branch by\nfast-forward and stops. Pushing stays an explicit `git push` you run when you\nmean it.\n\n**What if rebasing onto the base would conflict?** `merge` stops and tells you\nexactly how to resolve and re-run, or abort. Your default branch is never\ntouched until a clean fast-forward is proven.\n\n**Why is config bash and not YAML?** Zero dependencies — no `yq`, no fragile\npure-bash YAML parser. A sourced `.harnessrc` gives arrays and comments for\nfree. The trust model is identical to a `Makefile` or any tool that runs\nproject-defined hooks.\n\n**Does it work with Aider / Codex / others?** Yes — set `HARNESS_AGENT` (or pass\n`launch -- \u003ccmd\u003e`), and list multiple in `WORKTREE_HARNESS_AGENTS` for the shell\nintegration. Worktrees are agent-agnostic.\n\n**Claude Code already has `--worktree` — why this?** Native `-w` creates the\nworktree, but leaves provisioning (env, deps, ports, DB), merge-back, and the\ndiverged-origin case to you, and silently no-ops on non-GitHub remotes. This\nfills exactly those gaps. See [`SKILL.md`](SKILL.md) for Claude Code integration.\n\n## Contributing\n\nSmall, focused, dependency-free. See [`CONTRIBUTING.md`](CONTRIBUTING.md).\n`shellcheck` clean + `bats test/` green + bash-3.2-safe are the gates.\n\n## License\n\n[MIT](LICENSE) © Chris Ren\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frenchris%2Fworktree-harness","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frenchris%2Fworktree-harness","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frenchris%2Fworktree-harness/lists"}