Skip to content

Instantly share code, notes, and snippets.

@OutThisLife
Last active July 30, 2026 04:55
Show Gist options
  • Select an option

  • Save OutThisLife/69e87de53bb37ff85387cb120632f255 to your computer and use it in GitHub Desktop.

Select an option

Save OutThisLife/69e87de53bb37ff85387cb120632f255 to your computer and use it in GitHub Desktop.
BB Workflows
# gh: auto-pin default repo when a clone has multiple remotes.
# Without this, `gh pr checkout N` fails with "No default remote repository".
# Preference: origin → upstream → first github.com remote. Silent + once per repo.
_gh_ensure_default() {
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 0
git config --get-regexp '^remote\..*\.gh-resolved$' >/dev/null 2>&1 && return 0
local remote
for remote in origin upstream; do
if git remote get-url "$remote" >/dev/null 2>&1; then
command gh repo set-default "$remote" >/dev/null 2>&1 && return 0
fi
done
# Fall back to first github.com remote (ssh or https).
for remote in ${(f)"$(git remote 2>/dev/null)"}; do
case "$(git remote get-url "$remote" 2>/dev/null)" in
*github.com*)
command gh repo set-default "$remote" >/dev/null 2>&1 && return 0
;;
esac
done
}
gh() {
_gh_ensure_default
command gh "$@"
}
# Keep tab-completion working through the function wrapper.
(( $+functions[_gh] )) && compdef _gh gh

General

Cross-platform concepts. For OS-specific install + scheduling, see mac.md or windows.md — each is self-contained.


Dev root ($DEV_ROOT) + dotfiles (chezmoi)

Every machine keeps its repos in one folder, but the name differs per box (~/www on the old Mac, ~/Developer on newer ones, C:\dev on Windows). To avoid hardcoding that path, everything below derives from a single env var:

export DEV_ROOT="$HOME/Developer"   # or ~/www — wherever your repos live

Set it once and every script here (gtr-clean, the LaunchAgent/cron job, hgui/htui, per-repo AWS profiles) follows automatically. In this setup it's exported by a chezmoi-managed ~/.zsh.d/05-devroot.zsh, and passed explicitly into the LaunchAgent/cron env (they don't source your shell). Scripts fall back to ~/www if it's unset.

One private repo provisions the whole machine. chezmoi keeps the shell config, starship, the cleanup script + scheduler, the repo manifest (name + origin + branch), and the real .env files in a single private repo (it holds secrets, so keep it private). On a fresh machine:

sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply <you>/dotfiles

installs the CLI deps (brew, gh, glab, git-gtr, …), prompts for DEV_ROOT (default ~/Developer), clones every repo into it, drops the .env files in place, and schedules the hourly worktree prune — per OS. Worktrees are never synced; they're regenerated on demand with gtr.


gh multi-remote default (37-gh.zsh)

gh resolves the host repo from remotes, not from "I'm in this directory." If a clone has more than one GitHub remote (origin, upstream, fork, …) and nothing is pinned, commands like gh pr checkout 123 die with:

No default remote repository has been set

Even though you're standing in the right repo. One-shot fix:

gh repo set-default origin   # or: owner/repo

For every repo automatically, drop this in ~/.zsh.d/37-gh.zsh (chezmoi- managed here as dot_zsh.d/37-gh.zsh). It wraps gh so the first call in a repo with no gh-resolved pin silently prefers originupstream → first github.com remote. Full file: 37-gh.zsh.

Manual override anytime: gh repo set-default <remote|owner/repo> / --view / --unset.


Auto-prune git worktrees (GitHub + GitLab)

Git, GitHub, and GitLab do not remove local worktree folders when a PR/MR is merged or closed. git-gtr fixes that — it reads PR/MR state via gh (GitHub) or glab (GitLab), auto-detected per repo from origin, and removes the matching worktrees + branches.

  • Removes: worktrees whose branch has a merged or closed PR/MR.
  • Keeps: open PRs, dirty worktrees (unless --force), branches with no PR/MR.
  • Self-hosted GitLab: git gtr config set gtr.provider gitlab

Core command

cd /path/to/repo
git fetch --prune origin
git gtr clean --merged --closed --dry-run   # preview
git gtr clean --merged --closed --yes       # do it

Clean ALL repos under a dev folder

for d in "${DEV_ROOT:-$HOME/www}"/*/; do
  git -C "$d" rev-parse --is-inside-work-tree >/dev/null 2>&1 || continue
  # main checkouts only (skip linked worktrees)
  [ "$(git -C "$d" rev-parse --git-dir)" = "$(git -C "$d" rev-parse --git-common-dir)" ] || continue
  echo "== $d"
  # git-gtr won't prune unless the <repo>-worktrees container exists; make it,
  # clean, then remove it again if still empty so it doesn't litter $DEV_ROOT.
  mkdir -p "${d%/}-worktrees"
  git -C "$d" fetch --prune origin 2>/dev/null || true
  git -C "$d" gtr clean --merged --closed --yes
  rmdir "${d%/}-worktrees" 2>/dev/null || true
done

Shared cleanup script

~/bin/gtr-clean-worktrees.sh — used by the mac LaunchAgent and the Windows Task Scheduler job. Identical logic on both; only PATH differs.

#!/usr/bin/env bash
# Prune local git worktrees whose GitHub PRs/MRs are merged or closed,
# and reap orphan husk directories git leaves behind.
# Uses git-gtr (https://github.com/coderabbitai/git-worktree-runner).
set -euo pipefail

export PATH="/opt/homebrew/bin:/usr/local/bin:/home/linuxbrew/.linuxbrew/bin:/usr/bin:/bin:$PATH"

# Dev root: passed by the LaunchAgent/cron env, exported by the shell, or fallback.
DEV_ROOT="${DEV_ROOT:-$HOME/www}"

log() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"; }

_is_main_worktree() {
  local repo="${1%/}"
  [[ -d "$repo" ]] || return 1
  git -C "$repo" rev-parse --is-inside-work-tree &>/dev/null || return 1
  [[ "$(git -C "$repo" rev-parse --git-dir)" == "$(git -C "$repo" rev-parse --git-common-dir)" ]]
}

# Reap orphan husk dirs: directories under $DEV_ROOT that look like worktree
# siblings of <repo> (match <repo>-*) but are NOT registered in that repo's
# `git worktree list`. git-gtr unregisters merged worktrees but leaves the
# directory behind when it contains untracked/gitignored content (e.g. a Vite
# build cache). Those husks accumulate forever without this sweep.
#
# SAFETY — refuse to touch anything that still looks like a real checkout:
#   - must not match any registered worktree path for this repo
#   - must NOT be a live git work tree (independent repo like
#     brooklyn-skills-personal next to brooklyn-skills would otherwise match
#     the prefix glob and get deleted)
#   - must not be the main repo or $DEV_ROOT itself
#   - require a trailing hyphen in the glob (already: "${name}-"*) so
#     hermes-agent never matches hermes-agentfoo without the separator
_reap_orphans() {
  local repo="${1%/}"
  local name; name="$(basename "$repo")"
  local dir; dir="$(dirname "$repo")"
  # Collect registered worktree paths for this repo.
  local -a registered=()
  local wt
  while IFS= read -r wt; do
    [[ -n "$wt" ]] && registered+=("$wt")
  done < <(git -C "$repo" worktree list --porcelain 2>/dev/null \
    | awk '/^worktree /{print $2}')
  local sibling
  for sibling in "$dir/${name}-"*; do
    [[ -d "$sibling" ]] || continue
    # Never the main repo or DEV_ROOT itself (path equality).
    [[ "$sibling" == "$repo" || "$sibling" == "$DEV_ROOT" ]] && continue
    # Skip if still a registered worktree of THIS repo.
    local found=0
    for wt in "${registered[@]}"; do
      [[ "$wt" == "$sibling" ]] && { found=1; break; }
    done
    ((found)) && continue
    # CRITICAL: if this dir is still a live git checkout (its own repo OR a
    # worktree of some other repo), leave it alone. Only true husks — dirs
    # with no .git, left behind after git-gtr unregistered them — get removed.
    if git -C "$sibling" rev-parse --is-inside-work-tree &>/dev/null; then
      log "skip live checkout (not a husk): $sibling"
      continue
    fi
    log "reaping orphan husk: $sibling"
    rm -rf -- "$sibling"
  done
}

# Reap empty sibling <repo>-worktrees containers that older git-gtr releases
# create. Cosmetic, but keeps $DEV_ROOT tidy.
_reap_empty_worktree_containers() {
  local repo="${1%/}"
  local c; c="$(dirname "$repo")/$(basename "$repo")-worktrees"
  [[ -d "$c" ]] || return 0
  # Only remove if truly empty (rmdir fails otherwise — safe).
  rmdir "$c" 2>/dev/null || true
}

_clean_repo() {
  local repo="${1%/}"
  log "repo: $repo"
  # git-gtr early-returns before --merged/--closed if its default
  # <repo>-worktrees dir is missing (even when sibling worktrees exist).
  mkdir -p "$(dirname "$repo")/$(basename "$repo")-worktrees"
  git -C "$repo" fetch --prune origin 2>/dev/null \
    || git -C "$repo" fetch --prune 2>/dev/null \
    || log "warn: fetch failed for $repo"
  local rc=0
  git -C "$repo" gtr clean --merged --closed --yes || rc=$?
  # Drop the container if it's now empty so $DEV_ROOT stays tidy.
  _reap_empty_worktree_containers "$repo"
  # Reap orphan husk dirs git left behind (unregistered but not deleted).
  _reap_orphans "$repo"
  return "$rc"
}

main() {
  local repos=()

  if [[ "${GTR_CLEAN_ALL:-0}" == "1" ]]; then
    local d
    for d in "$DEV_ROOT"/*/; do
      _is_main_worktree "$d" || continue
      repos+=("${d%/}")
    done
  else
    repos=("${GTR_CLEAN_REPO:-$DEV_ROOT/hermes-agent}")
  fi

  if ((${#repos[@]} == 0)); then
    log "no repos to clean"
    exit 0
  fi

  local repo
  for repo in "${repos[@]}"; do
    _clean_repo "$repo" || log "warn: clean failed for $repo"
  done
}

main "$@"
Env var Default Meaning
GTR_CLEAN_ALL 0 1 → every main checkout under $DEV_ROOT/* (all repos, GitHub + GitLab)
GTR_CLEAN_REPO $DEV_ROOT/hermes-agent Single repo when GTR_CLEAN_ALL=0
DEV_ROOT ~/www Root folder holding all your repos (see top section)

Worktrees: share deps, isolate state

A git worktree only carries tracked files. The expensive stuff — node_modules, a Python .venv, build caches — is gitignored, so a freshly-created worktree has none of it and can't run until you reinstall. Doing that per worktree wastes disk and minutes.

Split every un-tracked resource into two buckets:

Resource Content-identical across branches? Strategy
node_modules when the lockfile matches the source checkout yes symlink from one canonical checkout
node_modules when the lockfile diverges no local install, stamped so it only reinstalls when the lock changes
Python .venv effectively yes run the source checkout's interpreter directly
Docker stacks, dev-server ports, DB volumes no — runtime state isolate per worktree (next section)

Share what's byte-identical and expensive to build; isolate what holds live state. Deleting a symlinked/idle node_modules whose lock still matches source is safe — re-link it next run.

The minimal symlink helper, generic to any lockfile-based project:

# Link node_modules from a canonical checkout when lockfiles match byte-for-byte;
# otherwise the branch bumped a dep and needs its own install.
wt_link_deps() {
  local worktree="${1:-$PWD}" source="$2"
  if cmp -s "$worktree/package-lock.json" "$source/package-lock.json"; then
    [[ -e "$worktree/node_modules" ]] || ln -s "$source/node_modules" "$worktree/node_modules"
  else
    ( cd "$worktree" && npm ci )
  fi
}

A fully worked example — lock-stamping, partial-install repair, --dev from-source builds, Electron backend cleanup on exit — lives in its own gist: Hermes worktree launchers (hermes / htui / hgui), complete with helpers.


Worktrees + Docker / dev servers (same idea, opposite goal)

Deps you share (symlink one copy). Running state you isolate — two worktrees that both docker compose up or bind the same port fight over the same containers, volumes, networks, and sockets. Same worktree magic, inverted: instead of pointing many worktrees at one resource, give each its own namespace.

Resource Collision if shared Fix (per worktree)
Compose containers / volumes / networks compose up in branch B stops/reuses branch A's stack unique COMPOSE_PROJECT_NAME derived from the worktree dir
DB / cache volumes branches clobber each other's data falls out of COMPOSE_PROJECT_NAME (volumes are prefixed by project)
Dev-server / HMR ports second server can't bind (some apps pin a fixed port → one at a time) offset the port by a per-worktree number, or accept one-at-a-time
Host bind-mounts (.:/app) none — each worktree is already a distinct path nothing; the worktree path is the isolation
Gitignored config (.env, certs, seed data) drift between copies symlink from the source checkout (this one you do share)

Drop this in a worktree's .envrc (direnv) or source it before running compose. It namespaces the stack and shares secrets from the source checkout instead of copying them:

# Per-worktree Docker isolation. Namespace the compose stack by worktree dir so
# `docker compose up` never touches another branch's containers/volumes/networks.
export COMPOSE_PROJECT_NAME="$(basename "$PWD" | tr -C 'a-z0-9' '-' | tr 'A-Z' 'a-z')"

# Share gitignored config from the source checkout instead of copying it into
# every worktree — same symlink trick used for deps. Point SOURCE at that checkout.
SOURCE="${DEV_ROOT:-$HOME/dev}/<repo>"
for f in .env .env.local; do
  [[ -e "$f" || ! -e "$SOURCE/$f" ]] || ln -s "$SOURCE/$f" "$f"
done

# Stable per-worktree port offset (0–99) so parallel dev servers/HMR don't clash.
# Feed $DEV_PORT into your compose file / dev script.
_wt_offset="$(( $(cksum <<<"$PWD" | cut -d' ' -f1) % 100 ))"
export DEV_PORT="$(( 3000 + _wt_offset ))"

Then reference the vars in docker-compose.yml (container_name: ${COMPOSE_PROJECT_NAME}-web, ports: ["${DEV_PORT}:3000"]) or your dev script. git gtr prunes the worktree; docker compose -p "$COMPOSE_PROJECT_NAME" down -v tears down its stack. To share one long-lived dev DB across all worktrees instead of per-branch data, point them at a single external volume rather than a project-scoped one.


Cursor disk (state.vscdb bloat)

Cursor stores all chat/agent history in one SQLite file. It grows unbounded and there's no auto-cleanup. Do not delete the live file — it breaks chat loading ("Loading Chat…" forever).

OS Path
macOS ~/Library/Application Support/Cursor/User/globalStorage/state.vscdb
Windows %APPDATA%\Cursor\User\globalStorage\state.vscdb

Fix via Command Palette (both platforms):

  1. Developer: Delete Old Chats… — choose a retention window; deletes older chats and compacts the file.
  2. Developer: GC Agent KV Blobs — clears orphaned blobs and runs VACUUM (slow on a multi-GB DB).

VACUUM alone won't shrink it if the data is live — the space is real saved history, so you must prune old chats to reclaim it.

#!/usr/bin/env bash
# Prune local git worktrees whose GitHub PRs/MRs are merged or closed,
# and reap orphan husk directories git leaves behind.
# Uses git-gtr (https://github.com/coderabbitai/git-worktree-runner).
set -euo pipefail
export PATH="/opt/homebrew/bin:/usr/local/bin:/home/linuxbrew/.linuxbrew/bin:/usr/bin:/bin:$PATH"
# Dev root: passed by the LaunchAgent/cron env, exported by the shell, or fallback.
DEV_ROOT="${DEV_ROOT:-$HOME/www}"
log() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"; }
_is_main_worktree() {
local repo="${1%/}"
[[ -d "$repo" ]] || return 1
git -C "$repo" rev-parse --is-inside-work-tree &>/dev/null || return 1
[[ "$(git -C "$repo" rev-parse --git-dir)" == "$(git -C "$repo" rev-parse --git-common-dir)" ]]
}
# Reap orphan husk dirs: directories under $DEV_ROOT that look like worktree
# siblings of <repo> (match <repo>-*) but are NOT registered in that repo's
# `git worktree list`. git-gtr unregisters merged worktrees but leaves the
# directory behind when it contains untracked/gitignored content (e.g. a Vite
# build cache). Those husks accumulate forever without this sweep.
#
# SAFETY — refuse to touch anything that still looks like a real checkout:
# - must not match any registered worktree path for this repo
# - must NOT be a live git work tree (independent repo like
# brooklyn-skills-personal next to brooklyn-skills would otherwise match
# the prefix glob and get deleted)
# - must not be the main repo or $DEV_ROOT itself
# - require a trailing hyphen in the glob (already: "${name}-"*) so
# hermes-agent never matches hermes-agentfoo without the separator
_reap_orphans() {
local repo="${1%/}"
local name; name="$(basename "$repo")"
local dir; dir="$(dirname "$repo")"
# Collect registered worktree paths for this repo.
local -a registered=()
local wt
while IFS= read -r wt; do
[[ -n "$wt" ]] && registered+=("$wt")
done < <(git -C "$repo" worktree list --porcelain 2>/dev/null \
| awk '/^worktree /{print $2}')
local sibling
for sibling in "$dir/${name}-"*; do
[[ -d "$sibling" ]] || continue
# Never the main repo or DEV_ROOT itself (path equality).
[[ "$sibling" == "$repo" || "$sibling" == "$DEV_ROOT" ]] && continue
# Skip if still a registered worktree of THIS repo.
local found=0
for wt in "${registered[@]}"; do
[[ "$wt" == "$sibling" ]] && { found=1; break; }
done
((found)) && continue
# CRITICAL: if this dir is still a live git checkout (its own repo OR a
# worktree of some other repo), leave it alone. Only true husks — dirs
# with no .git, left behind after git-gtr unregistered them — get removed.
if git -C "$sibling" rev-parse --is-inside-work-tree &>/dev/null; then
log "skip live checkout (not a husk): $sibling"
continue
fi
log "reaping orphan husk: $sibling"
rm -rf -- "$sibling"
done
}
# Reap empty sibling <repo>-worktrees containers that older git-gtr releases
# create. Cosmetic, but keeps $DEV_ROOT tidy.
_reap_empty_worktree_containers() {
local repo="${1%/}"
local c; c="$(dirname "$repo")/$(basename "$repo")-worktrees"
[[ -d "$c" ]] || return 0
# Only remove if truly empty (rmdir fails otherwise — safe).
rmdir "$c" 2>/dev/null || true
}
_clean_repo() {
local repo="${1%/}"
log "repo: $repo"
# git-gtr early-returns before --merged/--closed if its default
# <repo>-worktrees dir is missing (even when sibling worktrees exist).
mkdir -p "$(dirname "$repo")/$(basename "$repo")-worktrees"
git -C "$repo" fetch --prune origin 2>/dev/null \
|| git -C "$repo" fetch --prune 2>/dev/null \
|| log "warn: fetch failed for $repo"
local rc=0
git -C "$repo" gtr clean --merged --closed --yes || rc=$?
# Drop the container if it's now empty so $DEV_ROOT stays tidy.
_reap_empty_worktree_containers "$repo"
# Reap orphan husk dirs git left behind (unregistered but not deleted).
_reap_orphans "$repo"
return "$rc"
}
main() {
local repos=()
if [[ "${GTR_CLEAN_ALL:-0}" == "1" ]]; then
local d
for d in "$DEV_ROOT"/*/; do
_is_main_worktree "$d" || continue
repos+=("${d%/}")
done
else
repos=("${GTR_CLEAN_REPO:-$DEV_ROOT/hermes-agent}")
fi
if ((${#repos[@]} == 0)); then
log "no repos to clean"
exit 0
fi
local repo
for repo in "${repos[@]}"; do
_clean_repo "$repo" || log "warn: clean failed for $repo"
done
}
main "$@"

macOS

Full macOS setup for auto-pruning git worktrees when their GitHub/GitLab PRs are merged or closed, plus disk tips. Self-contained.


1. Install git-gtr

brew tap coderabbitai/tap
brew trust coderabbitai/tap
brew install git-gtr

Also needs authenticated gh (GitHub) and/or glab (GitLab) — provider is auto-detected per repo from origin.


2. Run it manually

Single repo:

cd $DEV_ROOT/hermes-agent
git fetch --prune origin
git gtr clean --merged --closed --dry-run   # preview
git gtr clean --merged --closed --yes       # do it

Every repo under $DEV_ROOT:

for d in $DEV_ROOT/*/; do
  git -C "$d" rev-parse --is-inside-work-tree >/dev/null 2>&1 || continue
  [ "$(git -C "$d" rev-parse --git-dir)" = "$(git -C "$d" rev-parse --git-common-dir)" ] || continue
  echo "== $d"
  git -C "$d" fetch --prune origin 2>/dev/null || true
  git -C "$d" gtr clean --merged --closed --yes
done

Removes worktrees with merged/closed PRs; keeps open PRs and dirty trees.


3. zsh helper

Add to ~/.zshrc:

#!/usr/bin/env bash
# Prune local git worktrees whose GitHub PRs/MRs are merged or closed,
# and reap orphan husk directories git leaves behind.
# Uses git-gtr (https://github.com/coderabbitai/git-worktree-runner).
set -euo pipefail

export PATH="/opt/homebrew/bin:/usr/local/bin:/home/linuxbrew/.linuxbrew/bin:/usr/bin:/bin:$PATH"

# Dev root: passed by the LaunchAgent/cron env, exported by the shell, or fallback.
DEV_ROOT="${DEV_ROOT:-$HOME/www}"

log() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"; }

_is_main_worktree() {
  local repo="${1%/}"
  [[ -d "$repo" ]] || return 1
  git -C "$repo" rev-parse --is-inside-work-tree &>/dev/null || return 1
  [[ "$(git -C "$repo" rev-parse --git-dir)" == "$(git -C "$repo" rev-parse --git-common-dir)" ]]
}

# Reap orphan husk dirs: directories under $DEV_ROOT that look like worktree
# siblings of <repo> (match <repo>-*) but are NOT registered in that repo's
# `git worktree list`. git-gtr unregisters merged worktrees but leaves the
# directory behind when it contains untracked/gitignored content (e.g. a Vite
# build cache). Those husks accumulate forever without this sweep.
#
# SAFETY — refuse to touch anything that still looks like a real checkout:
#   - must not match any registered worktree path for this repo
#   - must NOT be a live git work tree (independent repo like
#     brooklyn-skills-personal next to brooklyn-skills would otherwise match
#     the prefix glob and get deleted)
#   - must not be the main repo or $DEV_ROOT itself
#   - require a trailing hyphen in the glob (already: "${name}-"*) so
#     hermes-agent never matches hermes-agentfoo without the separator
_reap_orphans() {
  local repo="${1%/}"
  local name; name="$(basename "$repo")"
  local dir; dir="$(dirname "$repo")"
  # Collect registered worktree paths for this repo.
  local -a registered=()
  local wt
  while IFS= read -r wt; do
    [[ -n "$wt" ]] && registered+=("$wt")
  done < <(git -C "$repo" worktree list --porcelain 2>/dev/null \
    | awk '/^worktree /{print $2}')
  local sibling
  for sibling in "$dir/${name}-"*; do
    [[ -d "$sibling" ]] || continue
    # Never the main repo or DEV_ROOT itself (path equality).
    [[ "$sibling" == "$repo" || "$sibling" == "$DEV_ROOT" ]] && continue
    # Skip if still a registered worktree of THIS repo.
    local found=0
    for wt in "${registered[@]}"; do
      [[ "$wt" == "$sibling" ]] && { found=1; break; }
    done
    ((found)) && continue
    # CRITICAL: if this dir is still a live git checkout (its own repo OR a
    # worktree of some other repo), leave it alone. Only true husks — dirs
    # with no .git, left behind after git-gtr unregistered them — get removed.
    if git -C "$sibling" rev-parse --is-inside-work-tree &>/dev/null; then
      log "skip live checkout (not a husk): $sibling"
      continue
    fi
    log "reaping orphan husk: $sibling"
    rm -rf -- "$sibling"
  done
}

# Reap empty sibling <repo>-worktrees containers that older git-gtr releases
# create. Cosmetic, but keeps $DEV_ROOT tidy.
_reap_empty_worktree_containers() {
  local repo="${1%/}"
  local c; c="$(dirname "$repo")/$(basename "$repo")-worktrees"
  [[ -d "$c" ]] || return 0
  # Only remove if truly empty (rmdir fails otherwise — safe).
  rmdir "$c" 2>/dev/null || true
}

_clean_repo() {
  local repo="${1%/}"
  log "repo: $repo"
  # git-gtr early-returns before --merged/--closed if its default
  # <repo>-worktrees dir is missing (even when sibling worktrees exist).
  mkdir -p "$(dirname "$repo")/$(basename "$repo")-worktrees"
  git -C "$repo" fetch --prune origin 2>/dev/null \
    || git -C "$repo" fetch --prune 2>/dev/null \
    || log "warn: fetch failed for $repo"
  local rc=0
  git -C "$repo" gtr clean --merged --closed --yes || rc=$?
  # Drop the container if it's now empty so $DEV_ROOT stays tidy.
  _reap_empty_worktree_containers "$repo"
  # Reap orphan husk dirs git left behind (unregistered but not deleted).
  _reap_orphans "$repo"
  return "$rc"
}

main() {
  local repos=()

  if [[ "${GTR_CLEAN_ALL:-0}" == "1" ]]; then
    local d
    for d in "$DEV_ROOT"/*/; do
      _is_main_worktree "$d" || continue
      repos+=("${d%/}")
    done
  else
    repos=("${GTR_CLEAN_REPO:-$DEV_ROOT/hermes-agent}")
  fi

  if ((${#repos[@]} == 0)); then
    log "no repos to clean"
    exit 0
  fi

  local repo
  for repo in "${repos[@]}"; do
    _clean_repo "$repo" || log "warn: clean failed for $repo"
  done
}

main "$@"
gtr-clean              # just hermes-agent
gtr-clean -n           # dry-run
gtr-clean --all        # every repo under $DEV_ROOT
gtr-clean /path/to/repo

4. Hourly LaunchAgent (automatic)

Script ~/bin/gtr-clean-worktrees.sh (chmod +x it):

#!/usr/bin/env bash
# Prune local git worktrees whose GitHub PRs/MRs are merged or closed,
# and reap orphan husk directories git leaves behind.
# Uses git-gtr (https://github.com/coderabbitai/git-worktree-runner).
set -euo pipefail

export PATH="/opt/homebrew/bin:/usr/local/bin:/home/linuxbrew/.linuxbrew/bin:/usr/bin:/bin:$PATH"

# Dev root: passed by the LaunchAgent/cron env, exported by the shell, or fallback.
DEV_ROOT="${DEV_ROOT:-$HOME/www}"

log() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"; }

_is_main_worktree() {
  local repo="${1%/}"
  [[ -d "$repo" ]] || return 1
  git -C "$repo" rev-parse --is-inside-work-tree &>/dev/null || return 1
  [[ "$(git -C "$repo" rev-parse --git-dir)" == "$(git -C "$repo" rev-parse --git-common-dir)" ]]
}

# Reap orphan husk dirs: directories under $DEV_ROOT that look like worktree
# siblings of <repo> (match <repo>-*) but are NOT registered in that repo's
# `git worktree list`. git-gtr unregisters merged worktrees but leaves the
# directory behind when it contains untracked/gitignored content (e.g. a Vite
# build cache). Those husks accumulate forever without this sweep.
#
# SAFETY — refuse to touch anything that still looks like a real checkout:
#   - must not match any registered worktree path for this repo
#   - must NOT be a live git work tree (independent repo like
#     brooklyn-skills-personal next to brooklyn-skills would otherwise match
#     the prefix glob and get deleted)
#   - must not be the main repo or $DEV_ROOT itself
#   - require a trailing hyphen in the glob (already: "${name}-"*) so
#     hermes-agent never matches hermes-agentfoo without the separator
_reap_orphans() {
  local repo="${1%/}"
  local name; name="$(basename "$repo")"
  local dir; dir="$(dirname "$repo")"
  # Collect registered worktree paths for this repo.
  local -a registered=()
  local wt
  while IFS= read -r wt; do
    [[ -n "$wt" ]] && registered+=("$wt")
  done < <(git -C "$repo" worktree list --porcelain 2>/dev/null \
    | awk '/^worktree /{print $2}')
  local sibling
  for sibling in "$dir/${name}-"*; do
    [[ -d "$sibling" ]] || continue
    # Never the main repo or DEV_ROOT itself (path equality).
    [[ "$sibling" == "$repo" || "$sibling" == "$DEV_ROOT" ]] && continue
    # Skip if still a registered worktree of THIS repo.
    local found=0
    for wt in "${registered[@]}"; do
      [[ "$wt" == "$sibling" ]] && { found=1; break; }
    done
    ((found)) && continue
    # CRITICAL: if this dir is still a live git checkout (its own repo OR a
    # worktree of some other repo), leave it alone. Only true husks — dirs
    # with no .git, left behind after git-gtr unregistered them — get removed.
    if git -C "$sibling" rev-parse --is-inside-work-tree &>/dev/null; then
      log "skip live checkout (not a husk): $sibling"
      continue
    fi
    log "reaping orphan husk: $sibling"
    rm -rf -- "$sibling"
  done
}

# Reap empty sibling <repo>-worktrees containers that older git-gtr releases
# create. Cosmetic, but keeps $DEV_ROOT tidy.
_reap_empty_worktree_containers() {
  local repo="${1%/}"
  local c; c="$(dirname "$repo")/$(basename "$repo")-worktrees"
  [[ -d "$c" ]] || return 0
  # Only remove if truly empty (rmdir fails otherwise — safe).
  rmdir "$c" 2>/dev/null || true
}

_clean_repo() {
  local repo="${1%/}"
  log "repo: $repo"
  # git-gtr early-returns before --merged/--closed if its default
  # <repo>-worktrees dir is missing (even when sibling worktrees exist).
  mkdir -p "$(dirname "$repo")/$(basename "$repo")-worktrees"
  git -C "$repo" fetch --prune origin 2>/dev/null \
    || git -C "$repo" fetch --prune 2>/dev/null \
    || log "warn: fetch failed for $repo"
  local rc=0
  git -C "$repo" gtr clean --merged --closed --yes || rc=$?
  # Drop the container if it's now empty so $DEV_ROOT stays tidy.
  _reap_empty_worktree_containers "$repo"
  # Reap orphan husk dirs git left behind (unregistered but not deleted).
  _reap_orphans "$repo"
  return "$rc"
}

main() {
  local repos=()

  if [[ "${GTR_CLEAN_ALL:-0}" == "1" ]]; then
    local d
    for d in "$DEV_ROOT"/*/; do
      _is_main_worktree "$d" || continue
      repos+=("${d%/}")
    done
  else
    repos=("${GTR_CLEAN_REPO:-$DEV_ROOT/hermes-agent}")
  fi

  if ((${#repos[@]} == 0)); then
    log "no repos to clean"
    exit 0
  fi

  local repo
  for repo in "${repos[@]}"; do
    _clean_repo "$repo" || log "warn: clean failed for $repo"
  done
}

main "$@"

Plist ~/Library/LaunchAgents/com.brooklyn.gtr-clean.plist (GTR_CLEAN_ALL=1 → all repos):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>com.brooklyn.gtr-clean</string>
    <key>ProgramArguments</key>
    <array>
      <string>/Users/brooklyn/bin/gtr-clean-worktrees.sh</string>
    </array>
    <key>EnvironmentVariables</key>
    <dict>
      <key>PATH</key>
      <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
      <key>GTR_CLEAN_ALL</key>
      <string>1</string>
      <key>DEV_ROOT</key>
      <string>/Users/you/Developer</string>
    </dict>
    <key>StartInterval</key>
    <integer>3600</integer>
    <key>StandardOutPath</key>
    <string>/Users/brooklyn/Library/Logs/gtr-clean.log</string>
    <key>StandardErrorPath</key>
    <string>/Users/brooklyn/Library/Logs/gtr-clean.log</string>
    <key>RunAtLoad</key>
    <true/>
  </dict>
</plist>

Manage:

launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/com.brooklyn.gtr-clean.plist   # enable
launchctl bootout   "gui/$(id -u)" ~/Library/LaunchAgents/com.brooklyn.gtr-clean.plist   # disable
tail -f ~/Library/Logs/gtr-clean.log

Hermes-only instead of all repos: drop GTR_CLEAN_ALL and set GTR_CLEAN_REPO to the repo path.


Disk cleanup

Check the real data volume (sealed / is misleading):

df -h /System/Volumes/Data | tail -1
du -sh path        # actual bytes; Docker.raw's ls size is virtual, use du

Safe wins: ~/Movies/FocuSee Project, ~/Library/Caches/*, idle worktree node_modules, Chrome caches under ~/Library/Application Support/Google/Chrome/ (*/GPUCache, */Service Worker/CacheStorage). Never touch Chrome History/Cookies/Login Data or the live Cursor state.vscdb.

BB Workflows

Personal dev + power-user setup notes.

File Contents
general.md git-gtr + cleanup script, worktrees, gh multi-remote default — cross-platform
37-gh.zsh Auto-pin gh default remote when a clone has multiple remotes
mac.md macOS — install, zsh helper, hourly LaunchAgent, disk tips
windows.md Windows 11 — power tools, install, hourly Task Scheduler

Windows 11

Power-user mouse/window utilities + PowerToys, plus git worktree auto-prune. Self-contained.

Stack

Tool Role
AltDrag Alt + drag anywhere on a window to move/resize (Linux muscle memory). AltSnap is the maintained fork — same habit, can bind drag to Win+mouse.
PowerToys Command Palette as launcher; Peek, Keyboard Manager, Advanced Paste, FancyZones if snap layouts aren't enough. Don't enable every module.
Pixie (Nattyware) Tiny color picker — hex/RGB/HSV + cursor coords. Portable pixie.exe.
Feewhee (Nattyware) Titlebar / frame + mouse wheel → window opacity.
window-switcher Alt+` cycles windows of the same app; optional Alt+Tab for apps. Standalone Rust exe.

Philosophy: mouse-native window control > more launchers. Command Palette already covers launch — don't also run Flow/Raycast.

PowerToys modules that earn their keep

  • Command Palette
  • Peek (try before installing QuickLook)
  • Keyboard Manager
  • Advanced Paste
  • FancyZones — only if you outgrow Win11 snap

Same energy, if a gap shows up

  • Everything — instant filename search
  • EarTrumpet — per-app volume from the tray
  • Nilesoft Shell — tame Win11's right-click menu
  • ShareX — region / GIF / OCR when Snipping Tool isn't enough
  • Espanso — text expansion for strings you type daily

Skip: second launcher, tiling WM unless you want that lifestyle, "debloat" scripts that gut Defender.

Don't stack overlaps

AltDrag or AltSnap · Command Palette or Flow · one opacity tool (Feewhee) · Peek or QuickLook.


Auto-prune git worktrees (GitHub + GitLab)

Runs in Git Bash. Removes worktrees whose branch has a merged/closed PR/MR; keeps open PRs and dirty trees.

1. Install git-gtr

git clone https://github.com/coderabbitai/git-worktree-runner.git
cd git-worktree-runner && ./install.sh

Needs authenticated gh and/or glab — provider auto-detected per repo from origin. Self-hosted GitLab: git gtr config set gtr.provider gitlab.

2. Run it manually

Single repo:

cd $DEV_ROOT/hermes-agent
git fetch --prune origin
git gtr clean --merged --closed --dry-run   # preview
git gtr clean --merged --closed --yes       # do it

Every repo under your dev folder:

for d in $DEV_ROOT/*/; do
  git -C "$d" rev-parse --is-inside-work-tree >/dev/null 2>&1 || continue
  [ "$(git -C "$d" rev-parse --git-dir)" = "$(git -C "$d" rev-parse --git-common-dir)" ] || continue
  echo "== $d"
  git -C "$d" fetch --prune origin 2>/dev/null || true
  git -C "$d" gtr clean --merged --closed --yes
done

3. Cleanup script

~/bin/gtr-clean-worktrees.sh:

#!/usr/bin/env bash
set -euo pipefail
# PATH: point at your Git/gh/glab install
export PATH="/mingw64/bin:/usr/bin:$PATH"
DEV_ROOT="${DEV_ROOT:-$HOME/www}"   # where your repos live (see general.md)

_is_main_worktree() {
  local repo="${1%/}"
  git -C "$repo" rev-parse --is-inside-work-tree &>/dev/null || return 1
  [[ "$(git -C "$repo" rev-parse --git-dir)" == "$(git -C "$repo" rev-parse --git-common-dir)" ]]
}

_clean_repo() {
  local repo="${1%/}"
  # git-gtr needs the <repo>-worktrees container to exist before it prunes;
  # create it, then remove it afterward if still empty (keeps $DEV_ROOT tidy).
  mkdir -p "$(dirname "$repo")/$(basename "$repo")-worktrees"
  git -C "$repo" fetch --prune origin 2>/dev/null || true
  local rc=0
  git -C "$repo" gtr clean --merged --closed --yes || rc=$?
  rmdir "$(dirname "$repo")/$(basename "$repo")-worktrees" 2>/dev/null || true
  return "$rc"
}

if [[ "${GTR_CLEAN_ALL:-0}" == "1" ]]; then
  for d in "$DEV_ROOT"/*/; do
    _is_main_worktree "$d" || continue
    _clean_repo "${d%/}"
  done
else
  _clean_repo "${GTR_CLEAN_REPO:-$DEV_ROOT/hermes-agent}"
fi

4. Hourly Task Scheduler (automatic)

Create a Basic Task → trigger Daily, repeat every 1 hour:

  • Program: C:\Program Files\Git\bin\bash.exe
  • Arguments: -lc '/c/Users/you/bin/gtr-clean-worktrees.sh'
  • Environment: set GTR_CLEAN_ALL=1 for all repos (or GTR_CLEAN_REPO for one), and DEV_ROOT to your repos folder (e.g. C:\dev / /c/dev). Ensure gh/glab are on PATH.

Redirect output to a log if you want history:

-lc '/c/Users/you/bin/gtr-clean-worktrees.sh >> /c/Users/you/gtr-clean.log 2>&1'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment