This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
A Python CLI that gives every git branch its own worktree, ports, tmux session and coding agent. It replaces a set of bash scripts that were copied by hand into three repos and then drifted apart.
Read docs/traps.md before changing anything. Every rule there is the result of
something going wrong once, and most of them have a test.
The short version:
- The main checkout is resolved with
git rev-parse --git-common-dir. Never read$WT_MAIN, it points at whichever worktree holds the default branch. - The push guard is a plain
.git/hooks/pre-pushwith everything baked in. Never a hook manager job, never reading a file from the repo. .env.worktreeis merged, never rewritten. Keys wtx does not own must survive.- Call
proc.is_dry_run(). Neverfrom .proc import DRY_RUN, that copies the value and a dry run then changes things for real. wtx.tomlcan only add to the permission baseline, never remove from it.- An
askrule beats everything. No bare interpreters in it. A rule that names a flag is written*-d *:* -d *misses the flag in first place. - Setup is idempotent. wt fires its hooks more than once.
- A hook never restarts its own pane. It records what it wants and a detached process does it, or the restart kills the hook halfway through.
- Every file wtx generates in a checkout is in both ignore lists. An untracked
generated file makes the worktree dirty and
wtx landrefuses it. - Setup reads everything (config, templates, seeds) from the main checkout. A feature branch must not be able to change the rules its agent runs under.
uv venv && uv pip install -e . pytest ruff
python -m pytest tests/ -q # all tests
python -m pytest tests/test_units.py -k guard -q # one test by name
python -m pytest tests/test_real_tmux.py --run-tmux -q # needs a real tmux server
ruff check src/ tests/
ruff format src/ tests/ # the repo is formatted
wtx --dry-run go feat/x # print, do not runTests that need a real tmux server are marked tmux and skipped unless
--run-tmux is passed.
No runtime dependencies, stdlib only (Python 3.11+). tomllib reads config.
src/wtx/the package.configandcontextare read by everything else.src/wtx/agents/one module per coding agent, behind the protocol inbase.py.src/wtx/orchestrate.pythe plan-then-build handoff, driven by the agent's ExitPlanMode hook. It resumes the planning conversation, never re-briefs.src/wtx/templates/the permission baselines, shipped as package data.src/wtx/skills/wtx-init/the guided setup skill, installed byinstall-machine.src/wtx/shell/wtx.bashthewtgo/wtdoneshell functions, printed bywtx shell-init.tests/real temp git repos, with wt, tmux and the agents faked. A test must never touch the developer's tmux server and never push anywhere real.
cli.pyparses, then builds onecontext.Ctxper command: main checkout, worktree root, branch, config, and the values of.env.worktree.wtx gocallswt, which runs the hooks listed in the consumer repo's.wt.toml. Those callwtx hook post-createand friends (hooks.py), which runsetup.run_setup: ports,.env.worktree, deps, push guard, agent settings. Thentmux.ensure_sessionbuilds the panes from[panes].- The agent backend (
agents/claude.py,agents/opencode.py) renders the per-worktree settings and gives the shell line the agent pane runs.RenderContextinbase.pydecides model, effort and permission mode, including the plan/build phase of orchestration. notify.pykeeps one state file per session under$XDG_RUNTIME_DIR/wtx, read bywtx tmux-statusandwtx monitor. Handoff records live next to them with a.handoffsuffix so they are not mistaken for state.- All mutating subprocess calls go through
proc.run, which honours dry run.
tests/conftest.pyputs recording stubs forwt,tmux,claude,npm,uv,gh... first on PATH. Every call lands in a log; read it withcalls_of(fake_bin, "tmux"). The fakewtdoes realgit worktreework.- The
repofixture is a temp repo shaped like the real ones (backend, frontend, a bare remote, adevbase branch). - When a trap is fixed, add a test that would catch it again. That is how the rules above stay true.
Simple English, short sentences, no em dashes. Comments and docstrings explain why a thing is done that way, usually by naming the trap it avoids.
Every branch has its own git worktree, tmux session and agent. The session is
wtx/<branch> and its panes are agent and shell. There are no dev servers
and no ports here.
- Work on this branch only. Never push
main. When the work is ready, say so and a human runswtx land <branch>from the main checkout. - A pre-push hook enforces this. If it refuses a push, that is the design, not a bug to work around.