{"id":51264254,"url":"https://github.com/retif/stalewood","last_synced_at":"2026-06-29T14:30:44.132Z","repository":{"id":359544679,"uuid":"1246546121","full_name":"retif/stalewood","owner":"retif","description":"Find and reap merged git worktrees — detects, reports and prunes stale git worktrees","archived":false,"fork":false,"pushed_at":"2026-05-22T13:51:25.000Z","size":158,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-22T16:49:16.557Z","etag":null,"topics":["cli","developer-tools","git","git-worktree","golang","worktree"],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/retif.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-22T09:50:58.000Z","updated_at":"2026-05-22T13:51:29.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/retif/stalewood","commit_stats":null,"previous_names":["retif/stalewood"],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/retif/stalewood","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/retif%2Fstalewood","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/retif%2Fstalewood/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/retif%2Fstalewood/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/retif%2Fstalewood/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/retif","download_url":"https://codeload.github.com/retif/stalewood/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/retif%2Fstalewood/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34931514,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-29T02:00:05.398Z","response_time":58,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","developer-tools","git","git-worktree","golang","worktree"],"created_at":"2026-06-29T14:30:43.600Z","updated_at":"2026-06-29T14:30:44.127Z","avatar_url":"https://github.com/retif.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# stalewood\n\n\u003e Find and reap merged git worktrees.\n\n\u003c!-- markdownlint-disable MD013 --\u003e\n[![Go Reference](https://pkg.go.dev/badge/github.com/retif/stalewood.svg)](https://pkg.go.dev/github.com/retif/stalewood)\n[![CI](https://github.com/retif/stalewood/actions/workflows/ci.yml/badge.svg)](https://github.com/retif/stalewood/actions/workflows/ci.yml)\n[![CodeQL](https://github.com/retif/stalewood/actions/workflows/codeql.yml/badge.svg)](https://github.com/retif/stalewood/actions/workflows/codeql.yml)\n[![govulncheck](https://github.com/retif/stalewood/actions/workflows/govulncheck.yml/badge.svg)](https://github.com/retif/stalewood/actions/workflows/govulncheck.yml)\n[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/retif/stalewood/badge)](https://scorecard.dev/viewer/?uri=github.com/retif/stalewood)\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n\u003c!-- markdownlint-enable MD013 --\u003e\n\n`stalewood` scans a directory tree for git worktrees and tells you which ones\nare safe to delete — those whose work is already integrated into another\nbranch — and can reap them for you. A `--lint` mode drops into a git hook to\nkeep stale worktrees from piling up.\n\nIt finds worktrees three ways: directories under `.claude/worktrees/`,\n`git worktree list` of every repo it encounters, and abandoned worktrees\n(orphan directories and stale entries) — so nothing slips through.\n\n## Install\n\n```sh\n# Go toolchain\ngo install github.com/retif/stalewood@latest\n\n# Homebrew\nbrew install retif/tap/stalewood\n\n# npm  (or run without installing: npx stalewood)\nnpm install -g stalewood\n\n# Nix\nnix run github:retif/stalewood -- --help\n```\n\nPrebuilt binaries for Linux, macOS and Windows — plus Linux `.deb`,\n`.rpm` and `.snap` packages — are on the\n[releases page](https://github.com/retif/stalewood/releases).\n\n## Build from source\n\n```sh\ngit clone https://github.com/retif/stalewood\ncd stalewood \u0026\u0026 go build -o stalewood .\n```\n\nThe binary is self-contained and dependency-free. With Nix, `nix develop`\ngives a dev shell and `nix build` builds the tool; common tasks are in the\n`justfile` (`just build`, `just test`, `just check`).\n\n## Usage\n\n```sh\nstalewood [flags] [path]\n```\n\n`path` defaults to the current directory.\n\n| Flag         | Effect                                                            |\n|--------------|-------------------------------------------------------------------|\n| `--size`     | measure each worktree's disk usage                                |\n| `--base REF` | test every worktree against `REF` instead of its own base         |\n| `--lint SEL` | lint mode: exit 1 if a worktree matches `SEL` (repeatable)         |\n| `--json`     | emit JSON instead of the tree                                     |\n| `--prune`    | remove worktrees whose work is merged                             |\n| `--force`    | with `--prune`, also remove merged worktrees that are dirty/locked |\n| `--dry-run`  | with `--prune`, show what would be removed without removing it    |\n| `--verbose`  | log per-worktree detail to stderr                                 |\n| `--quiet`    | suppress progress output                                          |\n| `--print`    | print the whole report at once (disable the pager)               |\n| `--no-pager` | alias for `--print`                                               |\n| `--version`  | print version and exit                                            |\n| `--json-schema` | print the JSON Schema for `--json` output                  |\n| `-h, --help` | show help                                                         |\n\nExit codes: `0` success, `1` runtime failure, `2` usage error.\n\n### Examples\n\n```sh\nstalewood --size ~/projects             # report, with disk usage\nstalewood --base origin/main ~/repo     # force a specific base\nstalewood --prune --dry-run ~/projects  # preview what --prune would remove\nstalewood --prune ~/projects            # remove merged worktrees\nstalewood --json ~/projects             # machine-readable output (grouped by repo)\n```\n\n## The report\n\nThe report is a tree grouped by repo. Each `●` node is a repo (with its full\npath); each `├─`/`└─` node is a worktree showing a glyph, name, verdict and\ntags; the `├──` leaves give the worktree's full path, branch and base.\n\n```text\n● gitea   /home/you/projects/gitea\n  ├─ ✗ gitea-toasts  unmerged [untracked files]\n  │  ├── path    /home/you/projects/gitea-toasts\n  │  ├── branch  sse-toasts\n  │  └── base    origin/main\n  └─ ✓ gitea-issue-fixes  merged -\u003e origin/main [claude]\n     ├── path    /home/you/projects/gitea/.claude/worktrees/gitea-issue-fixes\n     ├── branch  fix/issue-19-sse-state\n     └── base    fix/user-project-move-multiproject-detach (sha)\n```\n\nA summary and a legend follow; the legend describes only the glyphs and tags\nthat actually appear in that run.\n\n| Marker          | Meaning                                                       |\n|-----------------|---------------------------------------------------------------|\n| `✓` / `✗`       | merged / unmerged                                             |\n| `⚠`             | abandoned (orphan dir or stale git entry)                     |\n| `!`             | error — the worktree could not be analyzed                    |\n| `-\u003e REF`        | merged, but into `REF` — a branch other than its own base     |\n| `[claude]`      | created by Claude Code (under `.claude/worktrees/`)           |\n| `[modified files]`    | tracked files have uncommitted changes                        |\n| `[untracked files]`   | the worktree has untracked files                              |\n| `[locked]`      | a git worktree lock is held                                   |\n| `[lock-stale]`  | locked, but the process that took the lock is gone            |\n| `[git-prunable]`| git's own `worktree list` flags the entry prunable            |\n\n## Discovery\n\nWorktrees are found from three sources, unioned and de-duplicated by path:\n\n1. **`.claude/worktrees/*`** — Claude Code worktree directories, found by\n   walking the tree. `node_modules` and `.git` are skipped; a child with no\n   `.git` entry (e.g. a committed test fixture) is not a worktree and is\n   ignored.\n2. **`git worktree list`** — every git repo found under the path is asked for\n   its linked worktrees, so worktrees living *outside* `.claude/worktrees/`\n   (e.g. ones you made by hand) are included too. The main checkout is not.\n3. **Abandoned worktrees** — found by cross-referencing the two:\n   - **orphan dir** (`abandoned-orphan`) — a worktree directory on disk that\n     no repo's `git worktree list` knows about (its `.git` file points to a\n     deleted git dir);\n   - **stale entry** (`abandoned-stale`) — a `git worktree list` entry whose\n     directory is gone.\n\n   Abandoned worktrees carry no merge analysis; they show a `fix` leaf with\n   the suggested cleanup.\n\n## Merge classification\n\nA live worktree counts as **merged** if either:\n\n- its HEAD is an ancestor of its **base** branch\n  (`git merge-base --is-ancestor`); or\n- its HEAD is contained in **any branch other than its own**\n  (`git for-each-ref --contains`) — catches work integrated into a branch\n  other than the base.\n\n### Base detection\n\nBy default each worktree is tested against the branch it was forked from. The\nbase is recovered in this order; the `base` leaf suffix shows which step won:\n\n| Source        | Suffix       | How                                              |\n|---------------|--------------|--------------------------------------------------|\n| `--base REF`  | `(flag)`     | explicit override, applied to every worktree     |\n| reflog ref    | *(none)*     | the branch's `Created from \u003cref\u003e` reflog entry   |\n| reflog SHA    | `(sha)`      | that reflog entry's commit, named via `name-rev` |\n| upstream      | `(upstream)` | the branch's configured upstream branch          |\n| auto          | `(auto)`     | the repo's main branch (remote `HEAD` preferred) |\n\nThe reflog-SHA step recovers a base even when the reflog ref is the unhelpful\nliteral `HEAD` or names a since-removed remote.\n\n### Caveats\n\n- **Squash / rebase merges.** Both checks are reachability-based; a branch that\n  was squash-merged or rebased onto its target shows as `unmerged`. Verify by\n  hand before force-pruning.\n- **Sibling worktrees.** If two worktrees share commits, each may report the\n  other's branch as containing its work. Check `merged -\u003e REF` before pruning.\n\n## Pruning\n\n`--prune` runs `git worktree remove` on every **merged** worktree — anywhere,\nnot just under `.claude/worktrees/`. Running with `--prune --dry-run` (or\nwith no flags at all) reports exactly what `--prune` would remove without\ntouching anything. Unmerged worktrees are kept; a merged worktree that is\ndirty or locked is skipped unless `--force` is given — a `[lock-stale]` skip\nsays so, since forcing it is safe. **Abandoned worktrees are never removed by\n`--prune`.** Exit status is non-zero if any removal failed.\n\n## Lint mode\n\n`--lint` turns stalewood into a checker for a single repo — built for git\nhooks. It scans only the git repo containing `[path]` (no directory walk, so it\nis fast enough for `pre-push`) and **exits 1 if any worktree matches**.\n\nEach `--lint` value is a comma-separated **AND-group** of predicates; repeat\n`--lint` to **OR** the groups; prefix a predicate with `!` to negate it.\n\n```sh\nstalewood --lint abandoned                    # fail if any abandoned worktree\nstalewood --lint abandoned --lint lock-stale  # abandoned OR lock-stale\nstalewood --lint removable,manual             # merged AND not a Claude worktree\nstalewood --lint merged,untracked             # merged AND has untracked files\n```\n\nPredicates: `merged` `unmerged` `live` `abandoned` `orphan` `stale` `dirty`\n`modified` `untracked` `locked` `lock-stale` `claude` `manual` `detached`\n`error` `git-prunable` `removable` `any`.\n\nMatching worktrees are printed; exit status is `1` on a match, `0` when clean\n(and silent), `2` on a bad selector. Use it in a global `pre-push` hook\n(`git config --global core.hooksPath \u003cdir\u003e`):\n\n```sh\n#!/bin/sh\n# \u003chooks\u003e/pre-push - block pushes while stale worktrees linger\nexec stalewood --lint abandoned --lint lock-stale\n```\n\n## Terminal behaviour\n\nstalewood adapts to where its output goes:\n\n- **Colour \u0026 weight** — glyphs and verdicts are bold and colour-coded by\n  severity, repo nodes bold-cyan, connectors dim. On an interactive terminal\n  only; disabled when piped or when `NO_COLOR` is set.\n- **Progress** — a transient progress line is shown on an interactive stderr\n  during a scan. `--quiet` silences it; `--verbose` replaces it with durable\n  per-worktree log lines on stderr.\n- **Paging** — human output is paged through `$PAGER` (default `less -FIRX`)\n  on an interactive terminal; `--no-pager` disables it. JSON is never paged.\n\nPiped or redirected, output is plain, unpaged and uncoloured. Every git\nsubprocess runs under a timeout so a wedged repo cannot stall the scan.\n\n## Verifying a release\n\nEvery release archive carries a signed SLSA build-provenance attestation —\nproof it was built from this repository's CI. Verify a downloaded archive\nwith the GitHub CLI:\n\n```sh\ngh attestation verify stalewood_\u003cversion\u003e_\u003cos\u003e_\u003carch\u003e.tar.gz \\\n  --repo retif/stalewood\n```\n\n`checksums.txt` is signed with keyless cosign:\n\n```sh\ncosign verify-blob checksums.txt \\\n  --bundle checksums.txt.bundle \\\n  --certificate-identity-regexp '^https://github.com/retif/stalewood' \\\n  --certificate-oidc-issuer https://token.actions.githubusercontent.com\n```\n\nAn SBOM (`*.sbom.json`, from syft) and a SLSA provenance file\n(`*.intoto.jsonl`, verifiable with `slsa-verifier`) are attached to every\nrelease.\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for the pull-request workflow and\n[CLAUDE.md](CLAUDE.md) for the design and CLI conventions. CLI behaviour\nfollows [clig.dev](https://clig.dev) where reasonable.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fretif%2Fstalewood","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fretif%2Fstalewood","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fretif%2Fstalewood/lists"}