git-harvest
v0.2.3
Published
Clean up branches and worktrees
Maintainers
Readme
git-harvest
English | 日本語
Clean up branches and worktrees.
Try it out (--dry-run)
See what would be deleted without deleting anything:
npx -y git-harvest@latest --dry-runRun directly without installing (Recommended)
Always runs the latest version — no separate update step needed.
# bun
bunx git-harvest@latest
# pnpm
pnpx git-harvest@latest
# npm
npx -y git-harvest@latest(Optional) Set up aliases
# bun
echo "alias ghv='bunx git-harvest@latest'" >> ~/.zshrc
echo "alias 'ghv!'='bunx git-harvest@latest --all'" >> ~/.zshrc
# pnpm
echo "alias ghv='pnpx git-harvest@latest'" >> ~/.zshrc
echo "alias 'ghv!'='pnpx git-harvest@latest --all'" >> ~/.zshrc
# npm
echo "alias ghv='npx -y git-harvest@latest'" >> ~/.zshrc
echo "alias 'ghv!'='npx -y git-harvest@latest --all'" >> ~/.zshrcgit harvest
# Git subcommand — run as `git harvest` (no install)
git config --global alias.harvest '!pnpm dlx git-harvest@latest'
# or: git config --global alias.harvest '!bunx git-harvest@latest'
# or: git config --global alias.harvest '!npx -y git-harvest@latest'Recommended workflow
By combining with Git hooks' post-merge command, you can automatically harvest after every merge or pull.
With lefthook
There are many Git hook tools such as husky, pre-commit, and simple-git-hooks, but Lefthook is recommended because it is language-agnostic and easy to integrate into monorepos. Additionally, by using lefthook-local.yaml, you can run hooks only for yourself without affecting other team members.
# lefthook-local.yaml
post-merge:
commands:
git-harvest:
run: npx -y git-harvest@latest
# or: bunx git-harvest@latest
# or: pnpx git-harvest@latestShell (macOS/Linux)
curl -fsSL https://raw.githubusercontent.com/nozomiishii/git-harvest/main/install.sh | bashRestart your terminal or run source ~/.zshrc to start using git-harvest.
Homebrew
brew install nozomiishii/tap/git-harvest(Optional) Set up aliases
Set up aliases for quicker access. You can use both or just the one you prefer:
ghv / ghv!
# Shell alias
echo "alias ghv='git-harvest'" >> ~/.zshrc
echo "alias 'ghv!'='git-harvest --all'" >> ~/.zshrcgit harvest
# Git subcommand — run as `git harvest`
git config --global alias.harvest '!git-harvest'Uninstall
curl -fsSL https://raw.githubusercontent.com/nozomiishii/git-harvest/main/uninstall.sh | bashUsage
git-harvestOptions
git-harvest --help # Show help
git-harvest --version # Show version
git-harvest --dry-run # Show what would be deleted without actually deleting
git-harvest --all # Delete all branches and worktrees except the default branch
git-harvest logo # Show the git-harvest logoWhat it does
Status markers:
| Marker | Meaning |
|---|---|
| ✓ | Removed |
| → | Will be removed (dry-run) |
| · | Kept (followed by reason) |
Worktree decision flow
flowchart TD
Start([evaluate worktree]) --> Main{main<br/>worktree?}
Main -->|Yes| KeepMain[keep<br/>not displayed]
Main -->|No| Locked{git worktree<br/>lock?}
Locked -->|Yes| KeepLocked["· locked"]
Locked -->|No| Running{running<br/>Claude session?}
Running -->|Yes| KeepRunning["· session running"]
Running -->|No| ManagedPath{under<br/>.claude/worktrees/?}
ManagedPath -->|Yes| DeleteManaged["✓ delete<br/>(forces through uncommitted / unmerged)"]
ManagedPath -->|No| Merged{merged?}
Merged -->|Yes| Uncommitted{uncommitted<br/>changes?}
Uncommitted -->|Yes| KeepUncommitted["· uncommitted changes"]
Uncommitted -->|No| DeleteMerged["✓ delete"]
Merged -->|No| NoUnique{no unique<br/>commits?}
NoUnique -->|Yes| KeepNoUnique["· no unique commits"]
NoUnique -->|No| KeepNotMerged["· not merged"]
classDef keep fill:#f5f5f5,stroke:#9e9e9e,color:#424242
classDef delete fill:#eeffc4,stroke:#C0FF39,color:#000
class KeepMain,KeepLocked,KeepRunning,KeepUncommitted,KeepNoUnique,KeepNotMerged keep
class DeleteManaged,DeleteMerged delete| Order | Condition | Display | Default | --all |
|---|---|---|---|---|
| 1 | Locked with git worktree lock | · locked | Keep | Delete (forced through with -f -f, shown as (was locked)) |
| 2 | Running Claude session (~/.claude/sessions/<pid>.json matches cwd and pid is alive) | · session running | Keep | Delete |
| 3 | Path is under .claude/worktrees/ and no running session | ✓ / → | Delete (forces through uncommitted / unmerged commits) | Delete |
| 4 | Merged + uncommitted changes | · uncommitted changes | Keep | Delete |
| 5 | Merged + clean | ✓ / → | Delete | Delete |
| 6 | No unique commits | · no unique commits | Keep | Delete |
| 7 | Not merged | · not merged | Keep | Delete |
| - | Main working tree | (not shown) | Keep | Keep |
Row 1 (lock) is the top-priority guard. git worktree lock is an explicit "don't touch this" signal, so the default mode keeps it regardless of running session or .claude/worktrees/ membership. Only --all breaks through it with git worktree remove --force --force, leaving a ✓ <path> (was locked) trace.
Row 3 is path-regime: worktrees under .claude/worktrees/ are treated as Claude-managed workspaces and aggressively deleted when no active session backs them (i.e. the session was archived or the local CLI exited). Worktrees outside this path fall through to rows 4+ — the original conservative logic — to avoid touching anything Claude didn't create.
Deletion behavior under .claude/worktrees/: the worktree is removed with --force even when it has uncommitted changes or unmerged commits. The following are preserved, however:
- Conversation history: stays on the Claude Code side, so
claude --resume <session-id>can pick up where you left off. - Unmerged commits: the branch ref is retained (
cleanup_branchesprotects unmerged branches), sogit checkout <branch>recovers them.
The only thing genuinely lost is uncommitted changes, so commit before closing a Claude session. Conversely, keeping the session open is a way to protect WIP that you can't commit yet.
About the "Disconnected" indicator on iPhone
A Remote Control session shown as "Disconnected" on iPhone / the claude app is not a paused-and-resumable state. It means the session has fully ended — the official docs make this explicit:
Local process must keep running: Remote Control runs as a local process. If you close the terminal, quit VS Code, or otherwise stop the
claudeprocess, the session ends.Extended network outage: if your machine is awake but unable to reach the network for more than roughly 10 minutes, the session times out and the process exits.
So a Disconnected session means the local process has already exited and the session is over. What remains on iPhone is server-side bookkeeping — messages sent there don't reach anything.
git-harvest mirrors this reality by only checking for an active local process (a matching entry in ~/.claude/sessions/<pid>.json with a live pid). It does not distinguish Connected / Disconnected / Archived on the iPhone side. Disconnected worktrees are therefore subject to the path-regime delete.
The conversation history (~/.claude/projects/<encoded-cwd>/<session-id>.jsonl) is kept separately, so claude --resume <session-id> can start a new session from where you left off. The worktree dir itself needs to be recreated separately (via git worktree add or EnterWorktree).
Branch decision flow
flowchart TD
Start([evaluate branch]) --> Default{default<br/>branch?}
Default -->|Yes| KeepDefault[keep<br/>not displayed]
Default -->|No| Deletable{merged<br/>or<br/>no unique commits?}
Deletable -->|No| KeepNotMerged["· not merged"]
Deletable -->|Yes| CheckedOut{checked out in<br/>another worktree?}
CheckedOut -->|Yes| KeepCheckedOut["· currently checked out"]
CheckedOut -->|No| Delete["✓ delete"]
classDef keep fill:#f5f5f5,stroke:#9e9e9e,color:#424242
classDef delete fill:#eeffc4,stroke:#C0FF39,color:#000
class KeepDefault,KeepNotMerged,KeepCheckedOut keep
class Delete delete| State | Display | Default | --all |
|---|---|---|---|
| Merged | ✓ / → | Delete | Delete |
| Merged + checked out | · currently checked out | Keep | Error |
| Not merged | · not merged | Keep | Delete |
| No unique commits | ✓ / → | Delete | Delete |
| Default branch | (not shown) | Keep | Keep |
--allexits with an error if a non-default branch is currently checked out.--dry-run --allshows all resources as→without errors.
Claude Code integration details
git-harvest reads these paths from Claude Code:
| Path | Used for |
|---|---|
| ~/.claude/sessions/<pid>.json | Detecting a running Claude session (cwd matches worktree path AND pid is alive) |
Archiving or deleting a session from Claude Code Agent View or the claude app remote control removes the corresponding ~/.claude/sessions/<pid>.json. git-harvest interprets the missing session file as "the user no longer needs this".
--all bypasses every guard and force-removes worktrees. Only the worktree directories are removed; session metadata is left untouched.
Without Claude Code installed, worktrees under .claude/worktrees/ are still subject to the path-regime delete. If you happen to create worktrees under that path manually without using Claude, they will be deleted — but most users without Claude won't adopt that path convention, so the impact is limited.
Override paths for testing or non-standard installs:
| Env var | Default |
|---|---|
| GIT_HARVEST_CLAUDE_SESSIONS_DIR | ~/.claude/sessions |
