Skip to main content

copse

CI PyPI Python versions License

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

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 .git directory, skipping dot-folders, node_modules and 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), or libs-auth-service if another repo has the same folder name.
  • Per-repo branch and base. Use -r repo[:branch[:base]]. Branch defaults to the task name. repo is the id from copse repos, or the folder name if that is unique.
  • Branch choice. For each repo, the first rule that matches wins:
    1. A local branch with that name exists. It is reused.
    2. <remote>/<branch> exists. A local branch is created that tracks it.
    3. Otherwise a new branch is cut from the base (see below).
  • Base. With no base given, copse uses what <remote>/HEAD points at, or the repo's current branch if there is none. A base written as origin/x always uses that remote ref, even with --from local. Otherwise --from remote (the default) uses origin/<base> when it exists, and --from local uses <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 bare git push publishes it. Until the first push, copse ls shows no ahead/behind. A repo without the remote gets no upstream.
  • Reruns. Rerunning new marks worktrees that already exist as exists.
  • Include files. copse new copies git-ignored files such as .env into each new worktree (see Setup for new worktrees).
  • Removal. copse rm removes worktrees. With --delete-branch it deletes a branch only if copse created it for that task. Those branches carry a branch.<name>.copseTask git config entry. Branches you attached are kept and reported as branch_kept in JSON. Deletion uses git 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 new exits 2 in a script. Pass -r for every repo. Without a TTY it will not prompt.
  • A repo is missing from copse repos. It must have a .git directory within 2 levels of the current directory. Dot-folders, node_modules and existing worktrees are skipped.
  • Two repos share a folder name. Use the id from copse repos (for example libs/auth-service).
  • git push has 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 --force for 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)

Source distribution for copse 0.5.0
File Size Uploaded
copse-0.5.0.tar.gz 24.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for copse 0.5.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page