copse
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
Contents
- Install
- Quick start
- How it works
- Commands
- Setup for new worktrees
- Configuration
- Agents and scripts
- Troubleshooting
- Development
- License
Install
uv tool install copse # or: pipx install copse
uv tool install -e . # from a clone
Upgrade 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.
Quick start
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 done
Run copse new 200 on a terminal with no -r to pick repos interactively.
How it works
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.
Commands
| 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. |
copse new
| 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/main
copse ls
copse 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.
copse rm
| 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. |
Setup for new worktrees
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"
Configuration
| 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. |
Agents and scripts
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/copse
Troubleshooting
copse 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.
Development
uv sync
uv run pytest
copse 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.
License
MIT, see LICENSE. NOTICE credits the projects some code was adapted from.
Metadata
Release files for copse 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| copse-0.5.0.tar.gz | 24.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| copse-0.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 42.2 kB
Release files / copse-0.5.0.tar.gz
| Download URL | copse-0.5.0.tar.gz |
|---|---|
| Size | 24.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0ae8ca3d8ac22e159d63c8bd493768dad36564c631ef7d580f8693b103fbd722
|
|
BLAKE2b-256 checksum How to use checksums |
0f9d48c258a651d5d78ad96b6b23b30651d9c3604cfbeef8e70a6b34614f9b9d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logRelease files / copse-0.5.0-py3-none-any.whl
| Download URL | copse-0.5.0-py3-none-any.whl |
|---|---|
| Size | 17.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b2bfc677094ad9cac91e7c7a4f7508bf70181d7bdc746f8049bad763cd60e73f
|
|
BLAKE2b-256 checksum How to use checksums |
af574b44e2a252580af3a70e0812d3d810d60029af88a60ff4b0f32a639eb961
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency log