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 🙏

© 2025 – Pkg Stats / Ryan Hefner

lumberjack-cli

v0.1.1

Published

A minimal CLI tool for cleaning up stale git branches and worktrees

Downloads

537

Readme

🪓 Lumberjack

A minimal CLI tool for cleaning up stale git branches and worktrees.

npm install -g lumberjack-cli
chop

Why?

If you use git worktrees or work on a team with squash-merge workflows, you've probably got a graveyard of stale local branches and worktrees. Lumberjack helps you clean them up safely.

Quick Start

# Preview what would be deleted (always safe)
chop --gone --dry-run

# Clean up branches where the remote has been deleted
chop --branch --gone

# Clean up worktrees where the remote branch was deleted
chop --tree --gone

# Clean up both branches and worktrees
chop --gone

Usage

chop [options]

At least one detection flag is required. When run without --branch or --tree, both branches and worktrees are processed.


Target Flags

Specify what to clean up. If neither is provided, both are targeted.

| Flag | Short | Description | |------|-------|-------------| | --branch | -b | Target local branches. | | --tree | -t | Target git worktrees. |

chop --branch --gone         # Only branches
chop --tree --gone           # Only worktrees
chop --branch --tree --gone  # Both (explicit)
chop --gone                  # Both (implicit)

Detection Flags

Specify the conditions for cleanup. Multiple flags use OR logic - items matching ANY condition will be flagged.

| Flag | Description | Branches | Worktrees | |------|-------------|:--------:|:---------:| | --gone | Upstream remote has been deleted. Best for squash-merge workflows where branches are auto-deleted. | ✅ | ✅ | | --merged[=<branch>] | Has been merged into the specified branch, configured mergeBase, or current branch. | ✅ | ✅ | | --squashed[=<branch>] | Changes are present in the target branch (tree comparison). Works for squash-merge even when remote still exists. | ✅ | ✅ | | --stale[=<days>] | No commits in the specified number of days. Default: 30 (or config value). | ✅ | ✅ | | --all | Shorthand for --gone --merged --squashed --stale. | ✅ | ✅ |

Examples

# Branches where remote is gone
chop --branch --gone

# Worktrees where remote branch was deleted
chop --tree --gone

# Branches with no commits in 60 days
chop --branch --stale=60

# Branches merged into main (explicit)
chop --branch --merged=main

# Branches whose changes are in main (works for squash-merge)
chop --branch --squashed=main

# Branches that are gone OR stale (OR logic)
chop --branch --gone --stale=30

# Both branches and worktrees, gone OR merged
chop --gone --merged

# Kitchen sink - branches and worktrees matching any condition
chop --all

Global Options

| Flag | Short | Description | |------|-------|-------------| | --dry-run | -d | Preview what would be deleted without actually deleting anything. | | --force | -f | Skip confirmation prompts. Use with caution. | | --protect <patterns> | -p | Comma-separated branch patterns to never delete (in addition to defaults). | | --no-protect | | Disable default protected branches. Not recommended. | | --no-fetch | | Skip the git fetch --prune step. Useful if you've just fetched. | | --keep-branch | | When removing worktrees, don't delete the associated branch. | | --json | | Output results as JSON (useful for scripting). | | --verbose | -v | Show detailed information about why each item was flagged. | | --help | -h | Show help. | | --version | -V | Show version. |


Configuration

Lumberjack can be configured via a .lumberjackrc file in your home directory or project root, or via a lumberjack key in package.json.

Config File

// ~/.lumberjackrc or ./.lumberjackrc
{
  "protect": ["main", "master", "develop", "staging", "production"],
  "stale": 30,
  "fetch": true,
  "mergeBase": "main",
  "defaultCommand": "--branch --gone"
}

Package.json

{
  "lumberjack": {
    "protect": ["main", "master", "develop"],
    "stale": 30
  }
}

Config Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | protect | string[] | ["main", "master", "develop"] | Branch patterns to never delete. Supports glob patterns. | | stale | number | 30 | Default number of days for --stale flag. | | fetch | boolean | true | Run git fetch --prune before detection. Set to false to always skip. | | mergeBase | string | null | Default branch for --merged check. If not set, uses current branch. | | defaultCommand | string | null | If set, chop without arguments runs this command. |

Config Precedence

  1. CLI flags (highest)
  2. Project .lumberjackrc
  3. Home directory ~/.lumberjackrc
  4. package.json lumberjack key
  5. Defaults (lowest)

Dry Run Output

$ chop --branch --gone --dry-run

DRY RUN - no changes will be made

Would delete 3 branches:

  feat/auth-flow
  └─ remote gone (upstream: origin/feat/auth-flow)

  feat/user-settings  
  └─ remote gone (upstream: origin/feat/user-settings)

  hotfix/typo
  └─ remote gone (upstream: origin/hotfix/typo)

Run without --dry-run to delete.

JSON Output

For scripting and automation:

$ chop --branch --gone --json --dry-run
{
  "dryRun": true,
  "branches": [
    {
      "name": "feat/auth-flow",
      "reason": "gone",
      "details": {
        "upstream": "origin/feat/auth-flow",
        "lastCommit": "2025-01-15T10:30:00Z",
        "lastCommitAge": 12
      }
    },
    {
      "name": "feat/user-settings",
      "reason": "gone",
      "details": {
        "upstream": "origin/feat/user-settings",
        "lastCommit": "2025-01-24T14:20:00Z",
        "lastCommitAge": 3
      }
    }
  ],
  "worktrees": [],
  "summary": {
    "branchesFound": 2,
    "branchesDeleted": 0,
    "worktreesFound": 0,
    "worktreesDeleted": 0,
    "protected": 0,
    "skipped": 0
  }
}

Protected Branches

By default, Lumberjack will never delete these branches:

  • main
  • master
  • develop

You can add to this list via config or CLI:

# Add staging and production to protected list
chop --branch --gone --protect staging,production

# Protect branches matching a pattern
chop --branch --gone --protect "release/*"

To see which branches are protected:

chop --verbose

Common Workflows

Squash-Merge Cleanup

If your team uses squash-merge and auto-deletes branches on GitHub/GitLab:

# Best option - relies on remote branch being deleted
chop --branch --gone

If your workflow doesn't auto-delete remote branches:

# Detects if changes are in main regardless of merge strategy
chop --branch --squashed=main

Weekly Maintenance

# Clean up everything that's clearly safe to remove
chop --all --dry-run

# If it looks good
chop --all

Before Starting a New Feature

# Quick cleanup of stale worktrees
chop --tree --gone --stale

CI/Automation

# JSON output for logging
chop --branch --gone --force --json >> cleanup.log

Configure a Default

If you always run the same cleanup, set it as default:

// .lumberjackrc
{
  "defaultCommand": "--branch --gone"
}

Now chop with no arguments runs chop --branch --gone.


How Detection Works

--gone Detection

  1. Runs git fetch --prune (unless --no-fetch)
  2. Runs git branch -vv to list branches with tracking info
  3. Identifies branches where upstream shows as [origin/branch: gone]

This is the most reliable method for squash-merge workflows.

--merged Detection

  1. Determines the base branch:
    • If --merged=<branch> is provided, uses that branch
    • Else if mergeBase is set in config, uses that
    • Else uses the current branch
  2. Runs git branch --merged <base>
  3. Returns all branches whose commits are reachable from the base branch

Note: This doesn't work for squash-merged branches since the commits are rewritten. Use --gone or --squashed for squash-merge workflows.

# Check against explicit branch
chop --branch --merged=main

# Check against configured mergeBase (or current if not set)
chop --branch --merged

--squashed Detection

  1. Determines the base branch:
    • If --squashed=<branch> is provided, uses that branch
    • Else if mergeBase is set in config, uses that
    • Else uses the current branch
  2. For each branch, simulates a merge using git merge-tree --write-tree
  3. Compares the resulting tree hash with the base branch's current tree
  4. If the trees are identical, the branch adds no new changes - it's already in base

This approach correctly detects squash-merged branches even when:

  • The branch had multiple commits that were squashed into one
  • The base branch has moved forward since the squash merge

Note: Requires Git 2.38+ for the merge-tree --write-tree feature.

# Check if branch changes are already in main
chop --branch --squashed=main

# Check against configured mergeBase (or current if not set)
chop --branch --squashed

--stale Detection

  1. Determines the threshold:
    • If --stale=<days> is provided, uses that value
    • Else if stale is set in config, uses that
    • Else uses 30 days
  2. For each branch, gets the date of the last commit
  3. Flags branches with no commits within the threshold
# Check for branches stale for 60+ days
chop --branch --stale=60

# Check against configured stale threshold (or 30 if not set)
chop --branch --stale

Comparison with Alternatives

| Tool | Branches | Worktrees | Squash Detection | |------|----------|-----------|------------------| | Lumberjack | ✅ | ✅ | ✅ | | git branch -d | ✅ | ❌ | ❌ | | git worktree prune | ❌ | ✅ (partial) | ❌ | | git-sweep | ✅ | ❌ | ❌ | | git-trim | ✅ | ❌ | ❌ |


Requirements

  • Node.js 18+
  • Git 2.20+ (Git 2.38+ required for --squashed detection)

License

MIT


Contributing

Issues and PRs welcome at github.com/jameshole/lumberjack.