A lightweight CLI for managing git worktrees, with automatic PR tracking and first-class support for AI-agent worktrees.
Simplifies creating isolated branches as git worktrees, seeing their live PR state, and housekeeping the ones that have landed — across every repository you work in.
Earlier versions kept a ledger: repo-cli only knew about worktrees it created itself. 2.0 replaces that with a lens — it discovers every worktree across your registered clone roots, whoever made it, and shows them in one view:
- Discovery over registration —
repo listsees worktrees created by repo-cli, by Codex, by Claude, or by hand (git worktree add). Each row is tagged with its provenance (repo-cli·codex·claude·manual). - Automatic PR linking — each branch's live GitHub PR is resolved automatically (batched, cached, offline-tolerant). No more
repo pr linkfor the common case. - Multiple roots per repo — a repo can have several clone roots (a bare clone plus a normal checkout, say); repo-cli tracks worktrees and branches across all of them.
- Agent-aware, agent-safe — agent worktrees are surfaced but never mutated: repo-cli only touches git worktrees and local branches, never an agent's own application state, and agent rows are opt-in for any destructive action.
repo list2.0 — Source · PR · Age · Path columns, provenance tags,--source/--prsfilters,--json.repo root— register additional clone roots for a repo so discovery spans all of them.repo env— run Codex-compatible environment files (setup/run/cleanup) from inside a worktree.repo clean— retire merged-PR worktrees, prune stale records, and delete orphanedclaude/*branches, all reviewed..worktreeincludeseeding — new worktrees inherit a clone's gitignored.env-type files (repo create, or backfill withrepo env seed).repo doctorv2 — git-version floor, root health, drift, PR-cache age, andghauth status.
- Worktree Management — create and delete worktrees without manual git commands
- Discovery & Provenance — one view of every worktree across roots, tagged by who made it
- Automatic PR Tracking — live GitHub PR state per branch, cached and offline-tolerant
- Agent Environments — run Codex-style env files; seed
.worktreeincludefiles into new worktrees - Housekeeping —
repo cleansafely retires landed worktrees and orphaned agent branches - Repo Aliasing — use short aliases instead of full URLs
- Rich Table Display — worktrees, branches, source, PR status, and age at a glance
- Shell Auto-complete — tab completion for repos, branches, and commands
- Python 3.11+
- Git 2.31+ (for
git rev-parse --path-format=absoluteand per-worktreeextensions.worktreeConfig) ghCLI (optional, for PR features) — authenticated viagh auth login
# Clone the repository
gh repo clone sadpandajoe/repo-cli
cd repo-cli
# Create virtual environment and install
uv venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
uv pip install -e .repo --help
repo --version # Check installed versionYou should see the CLI help with available commands.
Check for updates and upgrade automatically:
# Check if newer version is available
repo upgrade-check
# Upgrade to the latest version
repo upgrade
# Skip safety checks (use with caution)
repo upgrade --forceThe upgrade command will:
- Auto-detect your installation directory
- Check for uncommitted changes and warn you
- Pull latest changes from your current branch
- Reinstall dependencies (using uv or pip)
- Provide clear progress indicators
If you prefer manual control:
# If installed with uv
cd /path/to/repo-cli
git pull origin main
uv sync --dev
# If installed with pip
cd /path/to/repo-cli
git pull origin main
source .venv/bin/activate # On Windows: .venv\Scripts\activate
pip install -e ".[dev]"Verify the new version:
repo --versionIf you encounter any issues, run diagnostics:
repo doctor# Initialize configuration
repo init
# Register a repository (clones it as a bare repo under base_dir)
repo register myrepo git@github.com:user/repo.git
# Create a worktree for a branch
repo create myrepo feature-123
# List every worktree across all roots, with source, PR state, and age
repo list
# Navigate to a worktree (prints path with cd hint)
repo activate myrepo feature-123
# Delete a worktree (refuses agent-owned and root checkouts)
repo delete myrepo feature-123
# Also retire the branch, locally and on the remote
repo delete myrepo feature-123 --delete-remote--delete-branch and --delete-remote each read the tip they are about to delete before prompting and then delete that exact commit — locally via git update-ref -d <ref> <oid>, on the remote via --force-with-lease at the same OID. The two are checked independently, because another clone can push to the remote without your local branch moving at all. Either check failing leaves that branch alone and exits non-zero, so a partial cleanup is never reported as success. A branch that is already gone — someone used GitHub's "Delete branch" button first — is the end state you asked for, so it is reported and exits zero; that is distinct from a tip that could not be read, which leaves the branch possibly still there and does fail.
# All worktrees across every registered root, tagged by provenance
repo list
# Filter by who created them, or by PR state
repo list --source claude # repo-cli | codex | claude | manual | root
repo list --prs open # open | merged | closed | draft | override | none
repo list --refresh # bypass the 5-minute PR cache
repo list --json # stable machine-readable rowsA PR is only shown against a worktree whose clone root can be proven to be the alias's github.com repository. GitHub is asked "which PR has this head ref on owner/repo" — nothing in that answer is about the clone the worktree lives in, and a fork or an enterprise clone at the same owner/repo carries the same branch names. So a root whose origin names a different repo, sits on another host, resolves through an ssh-config alias, or could not be read at all shows — and a line saying why, rather than the upstream's PR. A manual repo pr link override still applies: that is your intent, not an inference from a branch name.
A repo can have more than one clone on disk (e.g. a bare clone plus a normal checkout an agent created). Register the extra ones so discovery spans them all:
repo root add myrepo ~/opt/code/myrepo # register another clone root
repo root list myrepo # show a repo's roots + health
repo root remove myrepo ~/opt/code/myrepo # stop scanning a rootroot add accepts any path inside the clone — a subdirectory, or one of its linked worktrees — and registers the clone root it belongs to, because discovery enumerates per clone root. If the clone's origin names a different repo than the alias (a fork), it asks before registering, since PRs are matched by branch name and a fork clone can mis-attribute them; with no terminal to ask on, it declines. Registering it anyway is supported — its worktrees are listed and addressable like any other — but they carry no auto-detected PR, and repo clean never proposes deleting their worktrees or branches (pruning stale registrations is unaffected — that reads no PR state).
root remove refuses to remove a repo's last root — a rootless repo is invisible to the lens, and nothing reseeds it. Register a replacement first, or drop the alias with repo unregister. Removing repo-cli's own bare clone warns that repo create still writes worktrees there and will re-register it.
Run the setup/run/cleanup env files an agent (or you) placed in the repo, from inside a worktree:
repo env list # environments defined for this repo
repo env up # run the setup steps
repo env run <action> # run a named action
repo env down # run cleanup
repo env status # show the active environment selection
repo env seed # backfill .worktreeinclude files into this worktreeSelection persists per-worktree via git config --worktree; repo-cli never writes codex.* state and never prints env-file contents.
Scripts receive the worktree they run in plus a source tree: the checkout that owns the worktree when that root is a normal clone, otherwise the repo's first non-bare root (repo-cli's bare roots hold no files), otherwise the worktree itself. Roots can sit on different commits, so a worktree always reads its own clone's env files and helpers. repo env seed refuses to copy anything when it cannot list the source clone's worktrees — without that list it cannot tell a file inside another worktree from one in the source.
repo clean gathers, into one reviewed list, worktrees whose PR has merged, every non-root detached worktree, stale worktree records git would prune, and orphaned claude/* branches (no worktree anywhere, no open PR):
repo clean # review candidates interactively (agent rows opt-in)
repo clean --yes # accept the pre-selected rows without prompting
repo clean --include-closed # also offer worktrees whose PR was closed (not merged)
repo clean --dry-run # show the table and remove nothing
repo clean --force # allow removing a dirty worktree / a branch with unlanded commitsSafety: root checkouts are never candidates, agent rows default unselected, dirty worktrees are skipped without --force, as are branches whose commits sit on no remote ref and whose PR did not merge, and --dry-run mutates nothing. Detached worktrees have no branch or PR evidence that their work is finished, so they are review-only by default: they are always unselected, --yes never removes them, and interactive cleanup requires an explicit row selection. Dirty detached worktrees additionally require --force. Git-locked detached worktrees remain visible but blocked; unlock them outside repo-cli before trying again. Use repo clean --dry-run to inspect the full candidate table safely. Stale worktree records are offered per root, because git worktree prune clears the whole root at once — the row lists every record it would remove.
Nothing is deleted on a guess. A worktree or branch is only offered when this run established its PR state — literally this run: clean always refetches, never reading the 5-minute cache other commands share, because a PR can be reopened or a merge reverted inside that window and "true four minutes ago" is not evidence for deleting now. (--dry-run refetches too; a preview that reports what a cached answer would have done is worse than a slow one.) A result served from a stale cache, a PR page that was cut short, a repo whose lookup failed, a branch with a manual pr_overrides pin, and a repo whose worktree scan came back incomplete are all reported and left alone. So is every root that cannot be proven to be the alias's github.com repository — a fork, an enterprise clone, an ssh-config alias, or an origin that could not be read — because PRs are matched by branch name and an unproven root's branches cannot be attributed to those PRs. Everything skipped is printed, so "nothing to clean" never hides "couldn't tell". A merged PR is itself proof the work landed — squash and rebase merges rewrite commit SHAs, so a normally-merged branch looks unpushed to git, and only branches with no merge to cite need --force. That proof covers the commit the PR merged and nothing pushed after it: a branch that moved on since it landed still needs --force, and the deletion compares against the tip shown in the table, so commits that arrive while the prompt is open are never destroyed by a decision taken before they existed.
PR state is linked automatically, so you rarely touch these:
repo pr open myrepo feature-123 # open the branch's PR in a browser
repo pr link 4567 myrepo feature-123 # deprecated: pin a PR override for a misdetectionrepo pr open applies the same identity proof as repo list: from a worktree whose clone root cannot be shown to be the alias's github.com repository it refuses to guess, and points you at repo pr link — opening a URL is an assertion about this clone, and a shared branch name is not evidence for it.
repo --version # 2.0.0
repo doctor # git version, root health, drift, PR-cache age, gh auth
repo upgrade-check # check for a newer version
repo upgrade # pull + reinstall (see Upgrading below)
# Unregister a repository alias (errors if worktrees exist; --force overrides)
repo unregister myrepo
repo unregister myrepo --force --remove-data --yesFor quick navigation, use the --print flag with shell command substitution:
# Jump directly to a worktree
cd $(repo activate myrepo feature-123 --print)
# Or create an alias in your .bashrc/.zshrc
alias repoa='cd $(repo activate "$@" --print)'
repoa myrepo feature-123Branch names follow Git's official rules and support hierarchical grouping with slashes:
Allowed:
- Alphanumeric characters, dots, hyphens, underscores
- Forward slashes for hierarchical grouping (e.g.,
feature/JIRA-123,bugfix/auth) @symbol (except the sequence@{)
Examples of valid branch names:
repo create myrepo main
repo create myrepo feature/JIRA-123
repo create myrepo bugfix/foo@bar
repo create myrepo user/joe/feature
repo create myrepo release/v1.2.3Prohibited:
- Cannot start or end with
/ - Cannot contain consecutive slashes
// - Cannot contain
..or@{ - Cannot end with
. - Slash-separated components cannot start with
.or end with.lock - Cannot contain spaces,
~,^,:,?,*,[,\
Repository aliases have stricter rules to prevent path traversal attacks:
Allowed:
- Alphanumeric characters, dots, hyphens, underscores only
- No slashes (path traversal protection)
- Cannot contain
::(internal delimiter)
Examples:
repo register myrepo git@github.com:user/repo.git # Valid
repo register api-core git@github.com:company/api-core.git # Valid
repo register my.repo git@github.com:user/my.repo.git # ValidInvalid examples:
repo register my/repo ... # Error: slashes not allowed
repo register ../prod ... # Error: path traversal blocked
repo register repo::name ... # Error: :: is internal delimiterUse repo unregister to remove a repo alias from your config. It is the inverse of repo register.
Unregistering also drops any PR pinned under that alias. Overrides are keyed alias::branch, so leaving them would let an old pin apply to whatever repository the alias is reused for next — and a pin overrides auto-detection everywhere. Force-registering an alias to a different URL drops them for the same reason; re-registering the same URL keeps them.
# Unregister an alias with no active worktrees (interactive confirmation)
repo unregister myrepo
# Skip the confirmation prompt
repo unregister myrepo --yesBy default, unregister refuses to remove an alias that still has worktrees — including worktrees the lens discovers under the repo's roots but has no config entry for (an agent's, say), which are listed with their provenance. It also refuses when one of the repo's roots could not be scanned, because an unreadable root cannot be shown to be empty. Delete the worktrees first with repo delete, or pass --force to also remove their config entries:
# Drop config entries for any remaining worktrees too
repo unregister myrepo --force --yes--force only updates the config. The bare repo and worktree directories on disk are left intact unless you also pass --remove-data:
# Also rmtree the on-disk bare repo and worktree directories
repo unregister myrepo --force --remove-data --yes--remove-data only deletes paths it positively recognizes as repo-cli-managed:
- nested layout (v0.2.0+):
<base_dir>/<alias>/.bare - legacy flat layout:
<base_dir>/<alias>.gitand<base_dir>/<alias>-<branch>
A directory at <base_dir>/<alias> without a .bare child is treated as unrelated user data and left untouched, with a warning.
Immediately before deleting anything, --remove-data re-enumerates every clone it is about to remove — straight from git, not from the config, since the alias and its roots are unregistered by that point. A worktree that appeared while the confirmation was open, or a clone that cannot be re-enumerated, aborts the deletion: the alias stays unregistered but the data is left on disk for you to inspect. --force skips this check, as it does the others.
In an interactive shell, repo unregister will also offer to delete on-disk data if you didn't pass --remove-data — answer y at the follow-up prompt. With --yes (or in a non-interactive context), data is left on disk if --remove-data wasn't explicitly passed; run repo doctor to surface orphaned directories or rm -rf them manually.
repo doctor scans <base_dir> for repo directories whose alias isn't registered (e.g. left behind after repo unregister without --remove-data, or moved manually) and lists them so you can adopt them with repo register or remove them with rm -rf.
2.0 migrates your ~/.repo-cli/config.yaml automatically and transparently on first run — migrations are presence-based, idempotent, and write a timestamped backup before any change. You don't run anything by hand. Two 2.0 migrations apply:
- M1 — seeds
repos.*.rootsfrom your existing bare layout so discovery has roots to scan. - M2 — re-keys worktree annotations by their resolved path and moves any legacy
pr:field into a branch-scopedpr_overridessection. The config version stamps to2.0.0.
Everything keeps working during the transition; a dead legacy pr: key is harmless and is cleaned up by M2. If anything looks off after upgrading, repo doctor reports config health, root health, and drift.
# Clone the repository
git clone https://github.com/sadpandajoe/repo-cli.git
cd repo-cli
# Option 1: Using uv (recommended)
uv sync --dev
# Option 2: Using pip
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
pip install -e ".[dev]"Note: Complete the setup steps above before running tests. The package must be installed in development mode for tests to import the repo_cli module.
# With uv (recommended - handles installation automatically)
uv run pytest tests/ -v
# With pip (requires editable install: pip install -e ".[dev]")
pytest tests/ -v
# With coverage
uv run pytest tests/ -v --cov=repo_cli --cov-report=term-missing# Install pre-commit hooks (recommended)
pre-commit install
# Run linting
uv run ruff check src/ tests/
# Run formatting
uv run ruff format src/ tests/
# Run pre-commit on all files
pre-commit run --all-filesThe project uses pre-commit to automatically check code quality before commits:
- ruff - Linting with auto-fix
- ruff-format - Code formatting
- trailing-whitespace - Remove trailing whitespace
- end-of-file-fixer - Ensure files end with newline
- check-yaml - Validate YAML files
- check-added-large-files - Prevent large files
After installing (pre-commit install), these checks run automatically on every commit.
Nested bare-repo layout (v0.2.0+):
- Base directory:
~/code/ - Bare repos:
~/code/{repo}/.bare/ - Worktrees:
~/code/{repo}/{branch}/(branch names percent-encoded) - Additional roots: any other clone you register with
repo root add - Config:
~/.repo-cli/config.yaml - PR cache:
~/.repo-cli/pr-cache.json(disposable; regenerated if corrupt)
Discovery lists worktrees across all registered roots. PR state is fetched via the gh CLI's GitHub GraphQL API (one call per repo, chunked, concurrent, 5-minute cache with offline stale-serve); no tokens are stored by repo-cli.
MIT