brainstorming
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or planning anything new, in software or out of it (a talk, a business, a renovation).
worktrunkSets up and drives Worktrunk (the `wt` CLI) so several branches, each in its own git worktree, can be worked on at once by people or AI coding agents. Use when a user says "run three agents in parallel on this repo", "give each task its own worktree", "set up worktrunk", "wt switch does not change directory", "add a hook that installs dependencies in new wo…
Independent GitHub snapshot · Automated classification · Original documentation governs
wt CLI) so several branches, each in its own git worktree,
can be worked on at once by people or AI coding agents. Use when a user says "run three agents
in parallel on this repo", "give each task its own worktree", "set up worktrunk", "wt switch
does not change directory", "add a hook that installs dependencies in new worktrees", "merge
this worktree back and clean it up", or asks why a hook wants approval. Covers installation,
the switch / list / merge / remove loop, the two config files and their real keys, hooks and
their approval step, launching agents with -x, JSON output for scripts, and safe cleanup.
license: Apache-2.0
compatibility: "Git 2.43+; Worktrunk 0.80+ on macOS, Linux or Windows; bash, zsh, fish, nushell or PowerShell for directory switching"
metadata:
author: terminal-skills
version: "2.0.0"
category: development
tags: [git, worktree, parallel-agents, cli, workflow]
repository: https://github.com/max-sixty/worktrunkA git worktree is a second checkout of one repository in another directory, on another branch.
That is the cheapest isolation available for parallel work: two agents editing two worktrees
cannot overwrite each other's files, yet they share history and can merge locally. Worktrunk's
wt binary wraps the bookkeeping. You name a branch and it derives the directory, runs the
project's setup hooks, optionally starts a program there, reports the state of every checkout in
one table, and lands a finished branch with tests run first and the leftovers removed.
Everything below was run against version 0.80.0 in a throwaway repository. Note that
.worktrunk.toml and keys such as on_create or [create] copy do not exist; the real files
and keys are in step 3 and step 4.
git --version is 2.43 or later and wt --version answers. On Windows the command may be
git-wt, because Windows Terminal owns the name wt.wt config show prints both config paths, pending hook approvals, and whether shell
integration is installed. Read it before changing anything.Package managers: brew install worktrunk (macOS, Linux), cargo install worktrunk (any Rust
toolchain), winget install max-sixty.worktrunk (Windows). Otherwise take the release archive
and verify it before unpacking:
base=https://github.com/max-sixty/worktrunk/releases/latest/download
curl -fsSLO "$base/worktrunk-x86_64-unknown-linux-musl.tar.xz"
curl -fsSLO "$base/worktrunk-x86_64-unknown-linux-musl.tar.xz.sha256"
sha256sum -c worktrunk-x86_64-unknown-linux-musl.tar.xz.sha256
tar -xJf worktrunk-x86_64-unknown-linux-musl.tar.xz
install -m 0755 worktrunk-x86_64-unknown-linux-musl/wt "$HOME/.local/bin/wt"
Then wt config shell install, once. A child process cannot change its parent's directory, so
this adds a shell function that does the cd. It edits the user's shell startup file: propose
it, do not run it unasked.
The default puts each worktree beside the repository: branch fix/csv-export of
~/code/ledger-api becomes ~/code/ledger-api.fix-csv-export. To change it, set one key in the
user config, ~/.config/worktrunk/config.toml (create it with wt config create):
worktree-path = "{{ repo_path }}/.worktrees/{{ branch | sanitize }}"
The default is {{ repo_path }}/../{{ repo }}.{{ branch | sanitize }}; a central folder would
be ~/worktrees/{{ repo }}/{{ branch | sanitize }}. sanitize turns / into -. With the
in-repo layout, add .worktrees/ to .gitignore; otherwise the main checkout shows it as
untracked.
Project settings go in .config/wt.toml, committed with the repository
(wt config create --project writes a commented starter). Hook names are fixed:
| Moment | Blocks until done | Runs detached |
|---|---|---|
| Any switch | pre-switch | post-switch |
| A worktree is created | pre-start | post-start |
| Worktrunk makes a commit | pre-commit | post-commit |
wt merge | pre-merge | post-merge |
| A worktree is removed | pre-remove | post-remove |
A failing pre- hook cancels the operation. Use pre-start for anything the program launched
with -x needs at once (dependencies, env files) and post-start for slow or long-lived jobs
(dev server, cache copy). Each hook can be a string, a table of named commands that run
together, or [[pre-merge]] blocks that run in sequence.
[pre-start]
deps = "npm ci"
[post-start]
caches = "wt step copy-ignored"
dev = "npm run dev -- --port {{ branch | hash_port }}"
[pre-merge]
test = "npm test"
lint = "npm run lint"
[list]
url = "http://localhost:{{ branch | hash_port }}"
{{ branch | hash_port }} maps a branch name to a stable port between 10000 and 19999, so
every worktree gets its own dev server and wt list shows its URL.wt step copy-ignored copies gitignored files (node_modules/, .env, build output) from
the main worktree into the new one, using copy-on-write on APFS, btrfs and XFS. A
.worktreeinclude file with gitignore-style patterns narrows it; --dry-run previews it.
Python virtual environments hold absolute paths, so recreate them instead of copying.{{ worktree_path }}, {{ primary_worktree_path }}, {{ default_branch }},
{{ target }}. Values are shell-escaped already: do not quote them.Project hooks are repository code, so Worktrunk asks before the first run and whenever a command's text changes. Without a terminal the prompt cannot appear and the command fails:
wt hook show # read what would run
wt config approvals add # the user approves; saved in ~/.config/worktrunk/approvals.toml
wt switch --create fix/csv-export --yes # one-off bypass, nothing saved
An agent shows the hook commands to the user and gets a yes before using --yes. --no-hooks
skips them for one command.
| Command | Result |
|---|---|
wt switch --create feat/audit-log | New branch from the default branch, new worktree, hooks run |
wt switch -c hotfix/tax --base release/2.4 | Same, branching from another base |
wt switch feat/audit-log | Go to the branch's worktree, creating one if it has none |
wt switch pr:412 | Check out a GitHub pull request's branch (needs an authenticated gh) |
wt switch - / wt switch ^ | Previous worktree / default-branch worktree |
wt switch -c feat/x -x claude -- 'task text' | Create, then run a program inside it |
-x names the program; every argument after -- is passed to it, so a prompt with spaces stays
one argument. To keep several agents alive, start each in its own terminal multiplexer session:
tmux new-session -d -s audit-log \
"wt switch --create feat/audit-log -x claude -- 'Record who changed each invoice and when'"
A non-interactive shell has no shell function, so wt switch creates the worktree but cannot
change directory. Ask for the path instead and address the worktree explicitly:
dir=$(wt switch --create fix/webhook-retry --no-cd --format=json | jq -r .path)
wt -C "$dir" list # JSON keys: action, branch, path, created_branch, base_branch
wt list prints one row per worktree: @ marks the current one, ^ the main one. Status uses
+ staged, ! modified, ? untracked, ↑ ahead of the default branch, ↓ behind,
↕ diverged, ✗ would conflict, _ same commit as it with a clean tree, – same commit with
uncommitted changes, ⊂ already contained in it.
--full adds CI status and needs gh or glab; --branches includes branches without a
worktree.
wt list --format=json | jq -r '.items[] | select(.default_branch.ahead > 0) | .branch'
wt list --format=json | jq -r '.items[] | select(.display.state == "would_conflict") | .branch'
wt step for-each -- git status --short
Local merge, from inside the feature worktree or with -C: wt merge targets the default
branch, wt merge develop another. In order, it commits what is uncommitted, squashes the
branch to one commit, rebases onto the target, runs pre-merge, fast-forwards the target, then
removes the worktree and branch. Flags switch stages off: --no-squash, --no-rebase,
--no-remove, --no-ff (make a merge commit), --stage tracked (leave untracked files out).
Know these before running it:
git status --short first, or pass
--stage tracked.[commit.generation] of the user config. With
none configured the message is a plain Changes to round.js (Squash commits from BRANCH
when several commits are squashed); if that is not acceptable,
commit by hand first and run wt merge --no-commit, which requires a clean working tree.git rebase --abort, then repeat.Pull-request route: commit, git push -u origin BRANCH, open the request, and after it is merged
run wt remove in that worktree.
wt remove (current worktree) or wt remove feat/audit-log fix/tax deletes the directory, and
the branch too when its content is already in the default branch. It refuses a worktree with
uncommitted changes and keeps an unmerged branch; -f and -D override those two checks and
destroy work, so use them only on the user's explicit word. Removal runs in the background
unless --foreground is given. wt step prune --dry-run previews removal of merged worktrees.
Marta Olsen wants three independent changes to ~/code/ledger-api built at once and merged
locally. The .config/wt.toml from step 4 is committed and approved (wt config approvals add).
cd ~/code/ledger-api
tmux new-session -d -s csv "wt switch -c fix/csv-export -x claude -- 'Quote fields containing commas in the CSV export; add a test'"
tmux new-session -d -s round "wt switch -c fix/invoice-rounding -x claude -- 'Round invoice totals half-even to 2 decimals; add tests'"
tmux new-session -d -s audit "wt switch -c feat/audit-log -x claude -- 'Record who changed each invoice and when'"
wt list
Branch Status main↕ Path URL
@ main ^ . http://localhost:12107
+ fix/csv-export ↑ ↑2 ../ledger-api.fix-csv-export http://localhost:13130
+ fix/invoice-rounding ! ↑ ↑1 ../ledger-api.fix-invoice-rounding http://localhost:15280
+ feat/audit-log ?– ../ledger-api.feat-audit-log http://localhost:16726
fix/csv-export is clean and ahead, so it goes first. She reads git -C ../ledger-api.fix-csv-export diff main... and merges:
wt -C ../ledger-api.fix-csv-export merge
npm test and npm run lint run as pre-merge; on success main advances by one squashed
commit and the worktree disappears. The next branch's own wt merge rebases it onto the new
main, which is where overlapping edits surface.
Asked to fix a retry bug while the user keeps editing main, an agent in a subprocess shell:
wt hook show # shows pre-start "npm ci": user agrees
dir=$(wt switch -c fix/webhook-retry --no-cd --yes --format=json | jq -r .path)
# ... edit files under "$dir", run tests with: npm --prefix "$dir" test
git -C "$dir" add src/webhooks/retry.ts test/retry.test.ts
git -C "$dir" commit -m "fix: back off webhook retries exponentially"
wt -C "$dir" merge --no-commit --yes --format=json
{ "branch": "fix/webhook-retry", "committed": false, "rebased": false, "removed": true,
"squashed": false, "target": "main" }
The agent reports the commit now on main, the removed worktree and branch, and that nothing
was pushed.
git switch inside it; wt switch moves between directories.hash_port and database names with {{ branch | sanitize_db }}.post- hooks do not print to the terminal. wt hook post-start --foreground
re-runs one visibly, and -v on any command prints the resolved template variables.~/.config/worktrunk/config.toml run in every repository with no approval
prompt. Keep them small and never copy one from an untrusted source.--config-set 'merge.squash=false') or by
environment (WORKTRUNK_MERGE__SQUASH=false); nested keys are joined with two underscores.post-start outlives its worktree; stop it in a pre-remove hook.