One folder per task, one git worktree per repo. Run it from any directory with repos 1-2 levels deep.
$ copse new 301
◇ Repos api, libs/auth-service-with-long-name
◇ Branch 301
◇ Start new branches from remote
╭─ Plan ───────────────────────────────────────────────╮
│ api 301 ← origin/main │
│ libs/auth-service-with-long-name 301 ← main │
╰──────────────────────────────────────────────────────╯
◆ Create 2 worktrees? Yes
✔ api 301 tasks/301/api
✔ libs/auth-service-with-long-name 301 tasks/301/auth-service-with-long-name
- Install
- Quick start
- How it works
- Commands
- Setup for new worktrees
- Configuration
- Agents and scripts
- Troubleshooting
- Development
- License
uv tool install copse # or: pipx install copse
uv tool install -e . # from a cloneUpgrade with copse update. It runs uv tool upgrade copse or pipx upgrade copse, and on Windows it prints the command instead. On a terminal, copse checks PyPI once a day and prints a one-line notice on stderr when a newer version exists. Set COPSE_NO_UPDATE_CHECK=1 to turn that off. The check never runs with --json or when output is piped.
cd ~/code # a folder that contains your repos
copse repos # see what it found
copse new 200 -r api -r web # tasks/200/api and tasks/200/web
copse ls 200 # branch, clean or dirty, ahead/behind, merged
copse rm 200 --delete-branch # clean up when the task is doneRun copse new 200 on a terminal with no -r to pick repos interactively.
Worktrees go in ./tasks/<task>/ next to your repos:
~/code/
├── api/ # repos you already have
├── web/
├── libs/
│ └── auth-service/
└── tasks/
└── 200/
├── api/ # worktree on branch 200
└── web/
- Repo discovery. copse scans 1-2 levels below the current directory for folders with a
.gitdirectory, skipping dot-folders,node_modulesand existing worktrees. A repo's id is the path below the current directory (libs/auth-service). Its worktree folder is the last path part (auth-service), orlibs-auth-serviceif another repo has the same folder name. - Per-repo branch and base. Use
-r repo[:branch[:base]]. Branch defaults to the task name.repois the id fromcopse repos, or the folder name if that is unique. - Branch choice. For each repo, the first rule that matches wins:
- A local branch with that name exists. It is reused.
<remote>/<branch>exists. A local branch is created that tracks it.- Otherwise a new branch is cut from the base (see below).
- Base. With no base given, copse uses what
<remote>/HEADpoints at, or the repo's current branch if there is none. A base written asorigin/xalways uses that remote ref, even with--from local. Otherwise--from remote(the default) usesorigin/<base>when it exists, and--from localuses<base>. A sha or tag also works. copse fetches first unless you pass--no-fetch. - Upstream. A new branch's upstream is
<remote>/<branch>(the same name, not the base), even before that branch exists on the remote, so a baregit pushpublishes it. Until the first push,copse lsshows no ahead/behind. A repo without the remote gets no upstream. - Reruns. Rerunning
newmarks worktrees that already exist asexists. - Include files.
copse newcopies git-ignored files such as.envinto each new worktree (see Setup for new worktrees). - Removal.
copse rmremoves worktrees. With--delete-branchit deletes a branch only if copse created it for that task. Those branches carry abranch.<name>.copseTaskgit config entry. Branches you attached are kept and reported asbranch_keptin JSON. Deletion usesgit branch -d, so a branch with unmerged commits survives unless you add--force, which also forces worktree removal and uses-D.
| Command | What it does |
|---|---|
copse repos |
List detected repos with their current branch. |
copse new TASK |
Create one worktree per repo under the task folder. |
copse ls [TASK] |
Show worktrees per task: branch, dirty, ahead/behind, merged. |
copse rm TASK |
Remove a task's worktrees, all or only the -r repos. |
copse update |
Upgrade copse with uv or pipx. |
| Flag | Default | Meaning |
|---|---|---|
-r, --repo SPEC |
interactive pick | Repo to include. Repeat for more. SPEC is repo[:branch[:base]]. |
-b, --branch |
task name | Branch for every repo without its own. |
--base |
<remote>/HEAD, else current branch |
Base for new branches. |
--from remote|local |
remote |
Cut new branches from <remote>/<base> or from local <base>. |
--remote |
origin |
Remote to fetch and track. |
--no-fetch |
off | Skip git fetch. |
--no-include |
off | Don't copy .worktreeinclude files. |
--no-setup |
off | Don't run the setup hook. |
--dry-run |
off | Plan only, create nothing. |
--root |
./tasks ($COPSE_ROOT) |
Where task folders live. |
--json |
off | Machine-readable output. |
copse new 200 -r api # branch 200, default base
copse new 200 -r api:feat/200:develop # own branch and base
copse new 200 --from local -r web:x:origin/main # local start, but this repo uses origin/maincopse ls [TASK] shows each worktree's branch, dirty state and ahead/behind counts. It also reports merged: true once the branch's commits are in the base it was cut from (or <remote>/HEAD for branches copse did not cut), including squash and fast-forward merges; false if not, or if the branch has no commits yet; null if there is no base to compare. Options: --root, --json.
| Flag | Meaning |
|---|---|
-r, --repo |
Repo id or folder name to remove. Repeat for more. Default is all of the task's repos. |
--delete-branch |
Also delete the task branch, if copse created it. |
--force |
Remove even with uncommitted changes. Also uses git branch -D. |
--root, --json |
As above. |
Both steps run in copse new, after each worktree is created, and both read from the main checkout.
Include files. List patterns in <repo>/.worktreeinclude, one per line, in gitignore syntax. copse copies matching files from the main checkout into the new worktree, but only files that git ignores, such as .env. It skips symlinks and never overwrites an existing file. JSON results list them under copied. Pass --no-include to skip.
# .worktreeinclude
.env
.env.local
Setup hook. Set a command with git config copse.setup "<cmd>", or add an executable <repo>/.copse/setup. copse runs it in each new worktree with these environment variables:
| Variable | Value |
|---|---|
COPSE_TASK |
Task name. |
COPSE_REPO |
Repo id. |
COPSE_MAIN |
Path of the repo's main checkout. |
On Windows, copse.setup runs in cmd.exe and a .copse/setup script can't run, so set a cmd command there.
If the hook fails, the result gets "setup": "failed" in JSON and copse new exits 1. Pass --no-setup to skip.
git config copse.setup "pnpm install"| Setting | Effect |
|---|---|
COPSE_ROOT |
Task root, same as --root. Default ./tasks. |
COPSE_NO_UPDATE_CHECK=1 |
Turn off the daily update notice. |
NO_COLOR |
Plain output. Color is only used on a TTY. |
git config copse.setup |
Setup hook command. |
<repo>/.worktreeinclude |
Files to copy into new worktrees. |
<repo>/.copse/setup |
Executable setup hook. |
Add --json to repos, new, ls or rm. new and rm print:
{
"task": "200",
"path": "/home/me/code/tasks/200",
"results": [
{"repo": "api", "path": ".../tasks/200/api", "branch": "200", "base": "main",
"status": "created", "error": null}
]
}status is created, exists, planned (dry run), removed or failed. Dry runs also include start, the ref the new branch would be cut from. new also reports copied and include_errors for new worktrees and, when a hook ran, setup.
| Code | Meaning |
|---|---|
| 0 | Everything worked. |
| 1 | At least one repo failed, or a setup hook failed. The others still ran. |
| 2 | Usage error, no -r without a TTY, or a cancelled prompt. Nothing was created. |
Without -r and without a TTY, copse exits 2 instead of prompting, and git never asks for credentials. Errors go to stderr as error: ....
copse new 200 -r api -r web:feat/200:develop --json | jq '.results[] | {repo,status,error}'To teach a coding agent to drive copse safely (always -r and --json, dry-run first, read exit codes, remove branches only when copse created them), install the skill:
npx skills add errhythm/copsecopse newexits 2 in a script. Pass-rfor every repo. Without a TTY it will not prompt.- A repo is missing from
copse repos. It must have a.gitdirectory within 2 levels of the current directory. Dot-folders,node_modulesand existing worktrees are skipped. - Two repos share a folder name. Use the id from
copse repos(for examplelibs/auth-service). git pushhas no upstream. Only repos that have the remote get one. Check--remote.- A branch survived
copse rm --delete-branch. copse did not create it, or it has unmerged commits. Add--forcefor the second case.
uv sync
uv run pytestcopse was called twt for about a day. A copse is a small group of trees, which is what a task is here: a handful of worktrees standing together in one folder, one per repo.
MIT, see LICENSE. NOTICE credits the projects some code was adapted from.