npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

stalewood

v0.1.6

Published

Find and reap merged git worktrees

Readme

stalewood

Find and reap merged git worktrees.

Go Reference CI CodeQL govulncheck OpenSSF Scorecard License: MIT

stalewood scans a directory tree for git worktrees and tells you which ones are safe to delete — those whose work is already integrated into another branch — and can reap them for you. A --lint mode drops into a git hook to keep stale worktrees from piling up.

It finds worktrees three ways: directories under .claude/worktrees/, git worktree list of every repo it encounters, and abandoned worktrees (orphan directories and stale entries) — so nothing slips through.

Install

# Go toolchain
go install github.com/retif/stalewood@latest

# Homebrew
brew install retif/tap/stalewood

# npm  (or run without installing: npx stalewood)
npm install -g stalewood

# Nix
nix run github:retif/stalewood -- --help

Prebuilt binaries for Linux, macOS and Windows — plus Linux .deb, .rpm and .snap packages — are on the releases page.

Build from source

git clone https://github.com/retif/stalewood
cd stalewood && go build -o stalewood .

The binary is self-contained and dependency-free. With Nix, nix develop gives a dev shell and nix build builds the tool; common tasks are in the justfile (just build, just test, just check).

Usage

stalewood [flags] [path]

path defaults to the current directory.

| Flag | Effect | |--------------|-------------------------------------------------------------------| | --size | measure each worktree's disk usage | | --base REF | test every worktree against REF instead of its own base | | --lint SEL | lint mode: exit 1 if a worktree matches SEL (repeatable) | | --json | emit JSON instead of the tree | | --prune | remove worktrees whose work is merged | | --force | with --prune, also remove merged worktrees that are dirty/locked | | --dry-run | with --prune, show what would be removed without removing it | | --verbose | log per-worktree detail to stderr | | --quiet | suppress progress output | | --print | print the whole report at once (disable the pager) | | --no-pager | alias for --print | | --version | print version and exit | | --json-schema | print the JSON Schema for --json output | | -h, --help | show help |

Exit codes: 0 success, 1 runtime failure, 2 usage error.

Examples

stalewood --size ~/projects             # report, with disk usage
stalewood --base origin/main ~/repo     # force a specific base
stalewood --prune --dry-run ~/projects  # preview what --prune would remove
stalewood --prune ~/projects            # remove merged worktrees
stalewood --json ~/projects             # machine-readable output (grouped by repo)

The report

The report is a tree grouped by repo. Each node is a repo (with its full path); each ├─/└─ node is a worktree showing a glyph, name, verdict and tags; the ├── leaves give the worktree's full path, branch and base.

● gitea   /home/you/projects/gitea
  ├─ ✗ gitea-toasts  unmerged [untracked files]
  │  ├── path    /home/you/projects/gitea-toasts
  │  ├── branch  sse-toasts
  │  └── base    origin/main
  └─ ✓ gitea-issue-fixes  merged -> origin/main [claude]
     ├── path    /home/you/projects/gitea/.claude/worktrees/gitea-issue-fixes
     ├── branch  fix/issue-19-sse-state
     └── base    fix/user-project-move-multiproject-detach (sha)

A summary and a legend follow; the legend describes only the glyphs and tags that actually appear in that run.

| Marker | Meaning | |-----------------|---------------------------------------------------------------| | / | merged / unmerged | | | abandoned (orphan dir or stale git entry) | | ! | error — the worktree could not be analyzed | | -> REF | merged, but into REF — a branch other than its own base | | [claude] | created by Claude Code (under .claude/worktrees/) | | [modified files] | tracked files have uncommitted changes | | [untracked files] | the worktree has untracked files | | [locked] | a git worktree lock is held | | [lock-stale] | locked, but the process that took the lock is gone | | [git-prunable]| git's own worktree list flags the entry prunable |

Discovery

Worktrees are found from three sources, unioned and de-duplicated by path:

  1. .claude/worktrees/* — Claude Code worktree directories, found by walking the tree. node_modules and .git are skipped; a child with no .git entry (e.g. a committed test fixture) is not a worktree and is ignored.

  2. git worktree list — every git repo found under the path is asked for its linked worktrees, so worktrees living outside .claude/worktrees/ (e.g. ones you made by hand) are included too. The main checkout is not.

  3. Abandoned worktrees — found by cross-referencing the two:

    • orphan dir (abandoned-orphan) — a worktree directory on disk that no repo's git worktree list knows about (its .git file points to a deleted git dir);
    • stale entry (abandoned-stale) — a git worktree list entry whose directory is gone.

    Abandoned worktrees carry no merge analysis; they show a fix leaf with the suggested cleanup.

Merge classification

A live worktree counts as merged if either:

  • its HEAD is an ancestor of its base branch (git merge-base --is-ancestor); or
  • its HEAD is contained in any branch other than its own (git for-each-ref --contains) — catches work integrated into a branch other than the base.

Base detection

By default each worktree is tested against the branch it was forked from. The base is recovered in this order; the base leaf suffix shows which step won:

| Source | Suffix | How | |---------------|--------------|--------------------------------------------------| | --base REF | (flag) | explicit override, applied to every worktree | | reflog ref | (none) | the branch's Created from <ref> reflog entry | | reflog SHA | (sha) | that reflog entry's commit, named via name-rev | | upstream | (upstream) | the branch's configured upstream branch | | auto | (auto) | the repo's main branch (remote HEAD preferred) |

The reflog-SHA step recovers a base even when the reflog ref is the unhelpful literal HEAD or names a since-removed remote.

Caveats

  • Squash / rebase merges. Both checks are reachability-based; a branch that was squash-merged or rebased onto its target shows as unmerged. Verify by hand before force-pruning.
  • Sibling worktrees. If two worktrees share commits, each may report the other's branch as containing its work. Check merged -> REF before pruning.

Pruning

--prune runs git worktree remove on every merged worktree — anywhere, not just under .claude/worktrees/. Running with --prune --dry-run (or with no flags at all) reports exactly what --prune would remove without touching anything. Unmerged worktrees are kept; a merged worktree that is dirty or locked is skipped unless --force is given — a [lock-stale] skip says so, since forcing it is safe. Abandoned worktrees are never removed by --prune. Exit status is non-zero if any removal failed.

Lint mode

--lint turns stalewood into a checker for a single repo — built for git hooks. It scans only the git repo containing [path] (no directory walk, so it is fast enough for pre-push) and exits 1 if any worktree matches.

Each --lint value is a comma-separated AND-group of predicates; repeat --lint to OR the groups; prefix a predicate with ! to negate it.

stalewood --lint abandoned                    # fail if any abandoned worktree
stalewood --lint abandoned --lint lock-stale  # abandoned OR lock-stale
stalewood --lint removable,manual             # merged AND not a Claude worktree
stalewood --lint merged,untracked             # merged AND has untracked files

Predicates: merged unmerged live abandoned orphan stale dirty modified untracked locked lock-stale claude manual detached error git-prunable removable any.

Matching worktrees are printed; exit status is 1 on a match, 0 when clean (and silent), 2 on a bad selector. Use it in a global pre-push hook (git config --global core.hooksPath <dir>):

#!/bin/sh
# <hooks>/pre-push - block pushes while stale worktrees linger
exec stalewood --lint abandoned --lint lock-stale

Terminal behaviour

stalewood adapts to where its output goes:

  • Colour & weight — glyphs and verdicts are bold and colour-coded by severity, repo nodes bold-cyan, connectors dim. On an interactive terminal only; disabled when piped or when NO_COLOR is set.
  • Progress — a transient progress line is shown on an interactive stderr during a scan. --quiet silences it; --verbose replaces it with durable per-worktree log lines on stderr.
  • Paging — human output is paged through $PAGER (default less -FIRX) on an interactive terminal; --no-pager disables it. JSON is never paged.

Piped or redirected, output is plain, unpaged and uncoloured. Every git subprocess runs under a timeout so a wedged repo cannot stall the scan.

Verifying a release

Every release archive carries a signed SLSA build-provenance attestation — proof it was built from this repository's CI. Verify a downloaded archive with the GitHub CLI:

gh attestation verify stalewood_<version>_<os>_<arch>.tar.gz \
  --repo retif/stalewood

checksums.txt is signed with keyless cosign:

cosign verify-blob checksums.txt \
  --bundle checksums.txt.bundle \
  --certificate-identity-regexp '^https://github.com/retif/stalewood' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

An SBOM (*.sbom.json, from syft) and a SLSA provenance file (*.intoto.jsonl, verifiable with slsa-verifier) are attached to every release.

Contributing

See CONTRIBUTING.md for the pull-request workflow and CLAUDE.md for the design and CLI conventions. CLI behaviour follows clig.dev where reasonable.