This repository collects small tools and automation scripts I have created for Linux, macOS, and Raspberry Pi systems.
Some of these scripts are also referenced from my playbook, so I keep them here in one place for versioning, reuse, and maintenance.
Most of the top-level files are standalone utilities. The remaining structured subproject is file-tools/.
RSS feed generators previously lived here under rss-feed-generators/. They moved on 2026-07-29 to the rss-feeds-tools repository (~/Projects/private/rss-feeds-tools), which is now the single home for every feed-generation script.
ticket previously lived here. It moved on 2026-08-16 to the private ~/private-tools repository and was renamed work-ticket (with jira-ticket as an alias), because it names a specific employer's repositories, branch prefix, and workspace paths — details that do not belong in a public repository. Both directories are on PATH, so only the command's name changed. Do not add it back here.
tools/
├── check-git-branch-source/ # Go source for check-git-branch (compiled binary)
├── check-git-repos-source/ # Go source for check-git-repos (compiled binary)
├── file-tools/ # File utility scripts
├── menu-app-source/ # Go source for menu-app (compiled binary)
├── menu-app-template.yaml # Starter .menu-app.yaml template
├── pause-source/ # Go source for pause (compiled binary)
├── tests/ # Test suites for top-level scripts
├── README.md
├── RUNBOOK.md
├── TODO.md # Pending work for the tools in this repo
└── <scripts> # Standalone executables (one file each; see below)
Pending work for these tools is tracked in TODO.md.
The compiled Go binaries (check-git-repos, check-git-branch, pause, menu-app) are distributed via package managers and GitHub Releases.
brew tap kevinpinscoe/homebrew-tap
brew install --cask check-git-repos
brew install --cask check-git-branch
brew install --cask pause
brew install --cask menu-appThese shipped as formulas until the cask migration. A formula and a cask of the same name cannot coexist, so remove any older install first:
brew uninstall check-git-repos check-git-branch pause menu-appcurl -sL https://kevinpinscoe.github.io/apt/gpg.key \
| sudo gpg --dearmor -o /etc/apt/keyrings/kevinpinscoe.gpg
echo "deb [signed-by=/etc/apt/keyrings/kevinpinscoe.gpg] \
https://kevinpinscoe.github.io/apt stable main" \
| sudo tee /etc/apt/sources.list.d/kevinpinscoe.list
sudo apt update
sudo apt install check-git-repos check-git-branch pause menu-appsudo curl -fsSL https://kevinpinscoe.github.io/rpm/kevinpinscoe.repo \
-o /etc/yum.repos.d/kevinpinscoe.repo
sudo dnf install check-git-repos check-git-branch pause menu-appPre-built binaries for Linux (amd64/arm64) and macOS (arm64) are available on the Releases page. Each release includes a checksums.txt for verification.
git clone https://github.com/kevinpinscoe/tools.git
cd tools
# Build individual binaries
go build -o ~/.local/bin/check-git-repos ./check-git-repos-source # or: make -C check-git-repos-source install-local
go build -o ~/.local/bin/check-git-branch ./check-git-branch-source
go build -o ~/.local/bin/pause ./pause-source
go build -o ~/.local/bin/menu-app ./menu-app-sourceThe Python and shell scripts require no build step — copy them to any directory on your PATH.
To install just menu-app without the other compiled tools:
brew tap kevinpinscoe/homebrew-tap
brew install --cask menu-appIf you installed menu-app before the cask migration, run brew uninstall menu-app first — the formula and the cask cannot coexist.
curl -sL https://kevinpinscoe.github.io/apt/gpg.key \
| sudo gpg --dearmor -o /etc/apt/keyrings/kevinpinscoe.gpg
echo "deb [signed-by=/etc/apt/keyrings/kevinpinscoe.gpg] \
https://kevinpinscoe.github.io/apt stable main" \
| sudo tee /etc/apt/sources.list.d/kevinpinscoe.list
sudo apt update
sudo apt install menu-appsudo curl -fsSL https://kevinpinscoe.github.io/rpm/kevinpinscoe.repo \
-o /etc/yum.repos.d/kevinpinscoe.repo
sudo dnf install menu-appSee menu-app-source/README.md for binary download instructions and build-from-source steps.
ai-checkpointadds, updates or removes one lane section of a repository'sCHECKPOINT.mdas a locked, validated, atomic transaction — every other lane is kept byte for byte, an empty or truncated result is refused, and removing the last lane deletes the file — and keeps a verified copy of every version outside the repository in~/.local/state/ai-checkpoints/<repository-id>/(the Mac's, frommac-container), retained 90 days.history,show,diffand an explicit, previewedrestorerecover a damaged checkpoint; nothing is ever restored automatically. A previewed, Kevin-approvedmigraterewrites legacy## Lane <id> <text>headings, which otherwise block every lane in the file, to## Lane <id>, with--id LINE=NEWIDto resolve a would-be duplicate id (AI-106). Required for every checkpoint mutation by~/ai/directives/project-planning-with-ai.md(AI-99). Python standard library andgitonly. SeeRUNBOOK.md.backupbacks up a file to$HOME/.backups/<abs-path>/preserving its directory structure. Supports-lto list all backups with timestamps,-r <path>to remove a single backup, and-cto clear all backups. Works on macOS and Linux.check-git-branchis a compiled Go binary that scans git repos under$HOME(and any extra paths in$CHECK_GIT_BRANCH) and reports any that are not on their default branch or have non-default local branches left over from previous work; silent when everything is clean.check-git-reposis a compiled Go binary that scans all git repositories under$HOME(and any extra paths in$CHECK_GIT_REPOS) and reports any that are ahead, behind, diverged, have uncommitted changes, are holding stale.git/*.lockfiles, have stashes (STASH/STALE,--stash-stale-after), or have a leftover untrackedCHECKPOINT.md(reported asCHECKPOINTrather thanUNTRACKED); supports--batch-modefor scripting,--remove-locksto clear stale locks, and--lock-stale-afterto tune how old a lock must be before it counts as stale (default 5m).--worktreereportsWTfor a repo with a linked git worktree present (e.g. anai-wt/<ISSUE>AI worktree), with--stale-days(default 3) additionally reportingSTALEfor one older than that.--checkpointreports only the repos holding aCHECKPOINT.mdand prints nothing else — no summary, no count, and no output at all when none are found — skipping every other check so it finishes in seconds rather than minutes. From source,make installbuilds to~/binandmake install-localto~/.local/bin.claude-log-viewis a curses TUI for browsingmyclaudelogs; navigates_<REL>cwd-directories under<LOG_ROOT>/and views logs through an ANSI-stripping pipeline.claude-settings-baseline.shbrings~/.claude/settings.jsonup to the Claude Code baseline:permissions.defaultMode=auto, and every skill in~/.claude/skillsset touser-invocable-onlyinskillOverrides, so no skill is loaded into Claude's context until it is run with/nameor via skills-tui. Fill-only (never changes a key already set) and idempotent, so it is safe to re-run on every shell start; mac-container does exactly that. Requiresjq>= 1.6; runs under macOS's bash 3.2.create-ticket-in-youtrack/create-ticket-in-youtrack.pyinteractively creates a YouTrack issue in eitherWork - InboxorKevin - Inbox, setting theIssue domaincustom field from the same "Is this work" answer that picks the project (Employer workfor work,Personalotherwise) and always assigning the issue to Kevin Inscoe; reads the server URL from$YOUTRACK_SERVERand theapp/youtrack/workAPI token from OpenBao (via parzival when the wrapper'syoutrack-workAppRole is provisioned,baodirectly otherwise). Prints the human-readable issue ID and a clickable issue URL on success.ddir/ddir.pycompares two directories recursively, reporting missing files and running side-by-side diffs on files that differ.difftoolprints or saves the Git diff of one tracked file, so a change a coding agent reports ("I changedRUNBOOK.mdin this worktree") can be reviewed or uploaded for AI review without before-and-after copies. The default compares the working file againstHEAD(staged and unstaged changes, net);--staged,--unstaged,--from A --to Band--merge-base A Bchoose other endpoints,--context Nsets the context (default 20) and-o FILEsaves the patch, refusing to overwrite without--force. The diff goes to stdout and the summary to stderr. It only reads: the repository, the index and the reviewed file are never changed. Python standard library andgitonly. SeeRUNBOOK.md.decode-tldr-tracking-links.shdecodes TLDR newsletter tracking links so the underlying destination URL is easier to inspect.eksis a Python urwid TUI that reads~/.environment/eks-clusters.dat, logs in via AWS SSO, writes the selected profile to~/.environment/.env_set.sh, and runsaws eks update-kubeconfigto set the default kubectl context.find-in-ai.shsearches Markdown notes in the current tree withrg.find-obsidian-vaultslists the live Obsidian vaults under$HOME(or-r DIR), one path per line. Vaults inside a linked git worktree are hidden: the non-work project workflow creates a worktree per issue, and a worktree of a vault repository carries its own.obsidian, so every open project branch would otherwise invent a phantom vault — a dozen at a time on this workstation. Pass-ato include them. The container, cache and vendored-dependency trees are pruned rather than descended, which accounts for most of the run time and all of thePermission deniednoise on Fedora.-0separates results with NUL forxargs -0; any script consuming this output should use it, because two vault names contain a space or an apostrophe. Portable to macOS — no GNU-onlyfindpredicates. This is a locator, not an auditor. The authoritative catalogue tool isscripts/discover-vaults.pyin~/Projects/private/obsidian-hacks, which additionally reconciles against Obsidian's registration file, audits repository layout against rule 1 ofobsidian-best-practices.md, and reports baseline plugin conformance. Prefer that one where it is available; reach for this one on a host where that repository is not checked out, or when a plain list of paths is all you need.fix-file-name.shrenames a file by replacing runs of spaces and non-alphanumeric characters with a single hyphen and lowercasing the result.titleconverts text to a markdown-filename-friendly slug: all non-alphanumeric runs collapse to a single hyphen, the result is lowercased, and leading/trailing hyphens are stripped. Warns and exits if the input contains a dot (usefix-file-name.shfor actual filenames).free-portprints a single free TCP port in a given range (default 20000–40000). Tries 200 random candidates first, then falls back to a sequential scan; skips kernel ephemeral ports, reserved ports from/proc/sys/net/ipv4/ip_local_reserved_ports, and any ports in$FREE_PORT_BLOCKED_PORTS(comma-separated, defaults to5432). Requiresss(Linux; part ofiproute2).human-only-search-guard(invoked asrgorugrepvia symlinks of the same name) promptsAre you human? Only a human is authorized to run this command.before running the real binary; a non-interactive caller (an AI agent's Bash tool, a sandboxed exec, a pipe) has no TTY to answer with and is denied outright. Exists to stop unscoped recursive searches an AI session might otherwise run against the FLDW's spinning/homedisk — see~/ai/directives/unscoped-recursive-searches.md.gitcfis a Python TUI that lists every untracked, modified or deleted file in the current git repo, lets you multi-select via an urwid checkbox picker, then commits (one batch commit if a memo is given, one per file otherwise) and pushesHEADtoorigin. Every commit message is prefixed withCommitted by gitcf tool:and a trailing space — both the auto-generatedAdded/Modified/Deleted <name>messages and any memo you type; pass--no-prefixto suppress it for that run. Deletions are listed so that a plain-mvrename can be committed whole — the untracked new name and the not-yet-staged deletion of the old name are both offered. A rename/copy git has already detected (R/Ccodes) is shown and committed asRenamed <old> → <new>/Copied <old> → <new>instead of the genericModified <name>. Unresolved merge conflicts are withheld from the picker and named on stderr, sincegit addon one would stage the conflict markers. A selected change that is already recorded in the index — a staged deletion above all, and equally a staged modification, addition or rename — is committed without being re-git added, because a staged deletion's path is absent from both the working tree and the index, so asking git to add it fails outright. Files that apre-commithook has already swept into an earlier commit in the same run are skipped rather than retried — see thegitcftroubleshooting section inRUNBOOK.md.CHECKPOINT.mdis withheld from the picker outright — it must never be committed — and its presence at the repo root prints a one-line warning before the picker opens. gitcf has no branch awareness of its own, so it always prints which branch is about to receive the commits/push before opening the picker, plus a reminder to log commits on the matching YouTrack issue when that branch looks like a project branch (KDA-10,KEVIN-91, etc.). It also fetches and compares the branch against its upstream before the picker opens, and warns — advisory only, never blocking — when the branch has fallen behind or diverged, since committing onto a stale branch is what makes the eventual push fail; pass--no-fetchto skip that check, and note that an unreachable origin is reported but never fatal. A rejected push is explained rather than raised: gitcf prints the commits the run made, the ahead/behind counts, and thegit pull --rebaseremedy, then exits non-zero without a traceback. A repo listed in~/.config/gitcf/ignore.txtis refused outright — nogit status, no picker — for a repo another system owns writes to (e.g.~/Projects/private/pim-records, Radicale's own auto-committed history).json-statsreports statistics about a JSON file — modified date/time, file size, line count, number of top-level JSON documents, counts of each JSON data type, whether the JSON is nested, maximum nesting depth, and the size and path of the largest object/array — as a human-readable report by default, or raw JSON with-j. Handles NDJSON-style files holding several concatenated top-level JSON documents by aggregating stats across all of them. Requiresjq.jsonfmtsafely formats JSON and JSONC files in place.jsonfmt-fedorais a variant with Fedora-specific installer hints (dnfinstead of Homebrew) for use on RPM-based systems.k3sis a Python urwid TUI that reads~/.environment/k3s-clusters.datand sets the default kubectl context to the selected k3s cluster viakubectl config use-context.mainbranchswitches back to the default branch and cleans up the current feature branch or git worktree; prompts for confirmation, warns about uncommitted/stashed changes, and refuses to proceed if a tmux session for the branch is still active. Usework-ticket --cleanfirst when working in a worktree. Companion towork-ticket, which lives in the private~/private-toolsrepository.mdflists the Markdown files (*.md,*.markdown) in the current directory only (no recursion) and presents anfzfchooser with a liveglow-rendered preview pane, then opens the selected file in themdfriedterminal Markdown viewer. Requiresfzf,glow, andmdfried.menu-appis a compiled Go binary (Bubble Tea TUI) that reads a.menu-app.yamlfile from the current repository's git root and presents its entries as a menu of scripts to run; each script runs from the git root and control returns to the menu afterward. Exits withnot a git initialized directorywhen run outside a repo, and offers to create a starter config frommenu-app-template.yamlwhen none exists. Source inmenu-app-source/.agent-credential-guardis called bymyclaude,mycodexandmyclaude-screenbefore they start anything: it refuses (exit 77) to start an AI agent from a shell that carries an elevated OpenBao session — the flagbao-authsets, or on a work host an unexpectedBAO_TOKEN/VAULT_TOKEN— and names the variable, never its value. Mistake prevention only: a bareclaudeorcodexis not coveredmyclaudelaunchesclaudeinside a namedabducosession withscriptdisk logging, and writes a cleaned.txtsibling next to the raw.logwhen the session exits (or viamyclaude --clean <log-file>after a detach). Prompts for a short session name on each launch (≤ 15 chars; spaces become hyphens, non-alphanumeric chars are stripped, result is lowercased — e.g."Today's Journal"→todays-journal). Logs land at<LOG_ROOT>/_<REL>/<timestamp>.log, where_<REL>encodes the cwdmyclaudewas launched from (e.g._.environment,_tools,_Projects-foo,_home).scriptbegins capturing immediately;date && exec clauderuns inside that logging shell. ExportsCLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1so Claude Code renders into the terminal's native scrollback for cleaner logs. Also defaultsPARZIVAL_IDENTITY=ai(override by exporting it beforehand; an explicit--asflag still wins) so credential fetches in a recorded session run under a restricted parzival identity — the session is logged to disk, so anything printed is captured permanently. Detach with Ctrl+; reattach withabduco -a <session-name>. Requiresabducoandutil-linux-script(sudo dnf install util-linux-script), plus Claude Code >= 2.1.132.myclaude-screenis the legacyscreen-based version ofmyclaude(preserved for platforms whereabducois unavailable). Seemyclaudefor the current version.mycodexlaunchescodexinside a namedabducosession withscriptdisk logging — identical in structure tomyclaudebut runscodexinstead ofclaude. Logs land at<LOG_ROOT>/CODEX/_<REL>/<timestamp>.log. Same session management, detach key (Ctrl+), cleanup pipeline, andPARZIVAL_IDENTITY=aidefault asmyclaude.newest-fileprints the most recently modified file found recursively under the current directory; thin wrapper aroundfile-tools/list_recursively_newest_file.py.pauseis a compiled Go binary that sleeps for a specified number of seconds, displaying a live countdown status line on stderr; drop it into scripts whereversleep Nwould leave the user wondering how long remains.pullguarantees that no commit existing on a remote is missing from this machine: it fetches every remote of every git repo under$HOME(plus any paths in$CHECK_GIT_REPOS) in parallel, then fast-forwards every local branch that is behind its upstream — not just the checked-out one. Failed fetches, diverged branches, and fast-forwards git refuses are each reported individually and make the run exit non-zero, so nothing is ever skipped silently. Supports--dry-run,--autostash,--no-fetch,-j N,-q, and explicitPATH...arguments. Shares the ignore list at~/.config/check-git-repos-source/ignore.txtwithcheck-git-repos. On a real terminal a braille spinner (matchingcheck-git-repos) shows discovery/fetch progress instead of sitting silent — this matters on hosts where$HOME/$CHECK_GIT_REPOScrosses a slow bind mount.pull-requestsscans all git repos under a root directory (default:~/Projects) for open GitHub PRs authored by$GITHUB_USER; supports whitelist/blacklist filtering via~/.config/pull-request/.radar.shdisplays live animated NOAA weather radar in the terminal via mpv. Downloads GIF radar loops fromradar.weather.gov, plays them on a loop, and auto-refreshes every 2 minutes. Single-key controls switch between regional views (CONUS, Northeast, Southeast, Great Lakes, Pacific NW) and city stations (Melbourne, Jacksonville, Atlanta, NYC, Chicago, Dallas, Denver, Seattle, Los Angeles, Miami, Morristown TN). The Morristown TN (KMRX) station overlays a bordered "you are here" marker at a calibrated pixel position via an mpvlavfi/drawboxfilter. Falls back to macOS Preview if mpv is unavailable. Requireswgetandmpv.restorerestores a file previously saved bybackupfrom$HOME/.backups/<abs-path>/back to the current directory. Companion tobackup.set-ghostty-tab-namesets the name of the terminal tab the calling process is running in, so an AI agent working a ticket can label its tab with the ticket key (FLDW-12,KTA-1) and re-label it with that key plus a lowercasec(FLDW-12c) once the ticket is closed — the tab then shows both which ticket the session last worked and that it is finished. Because Ghostty here runstmux new-session -A -s main, the visible tab name is a tmux window name, and that is what the script sets; outside tmux it falls back to anOSC 2title escape. It always targets the caller's own pane ($TMUX_PANE, or process-ancestry lookup if that is unset) — a baretmux rename-windowrenames whichever window the client is viewing, which with several agents running silently relabels someone else's window.--reset— no longer part of closing a ticket, but still available for a tab with no finished ticket to remember — unsets the tab'sautomatic-renamerather than hardcoding a name, handing naming back to tmux, which names the tab after whatever is running in it —zshat an idle shell,claudeduring an AI session. Required by directive when a YouTrack or Jira ticket starts and again when it closes (when-creating-a-youtrack-ticket.md§§7 and 9,when-in-work-context.mdinstruction 8). Supports-t TARGETand--show. Thezerozsh function in~/.dotfilescalls--resetwhen returning an idle tab to rest; it replaced the retiredresetshim (KTA-51).trufflehog.shscans credential-relevant paths for secrets using TruffleHog; detects the current platform (Fedora, macOS, Raspberry Pi) and adjusts paths accordingly. Prints a summary grouped by detector. Compare output against~/.environment/.credentials-map.mdto identify undocumented credentials.walk_thru_readme_and_find_missing_files.pychecksREADME.mdfiles for local Markdown links that point to missing files.walk_thru_repo_looking_for_files_missing_from_README.pyfinds Markdown files in the repo that are not linked from a siblingREADME.md.wdprints the current working directory, replacing the$HOMEprefix with~when inside the home directory.what-did-i/what-did-i-accomplish-today.pyqueries git commits from GitHub (viagh) and Gitea (via REST API) for today (or yesterday withwhat-did-i yesterday, or a specific day withwhat-did-i YYYY-MM-DD) and writes a dated Markdown summary to$JOURNAL_PATH/ACCOMPLISHMENTS/YYYY-MM/git-work-for-YYYY-MM-DD.md(Linux:~/Journal/personal-journal; macOS:~/Journal/Professional); also prints to stdout.youtube-mdfetches a YouTube video's title viayt-dlp, slugifies it usingfix-file-name.shrules, then runsdefuddle parse --mdon the URL and saves the result to<slug>.mdin the current directory. Accepts the URL as an optional positional argument or prompts for it interactively.
file-tools/contains tools for locating, searching, or manipulating files.
skills-tui— TUI-based command-line skills chooser designed to be executed by Claude Code.gitme— quickly find local Git repositories.eng-tools— collection of handy online tools for engineers with a focus on good UX.metar-tool— obtains and parses weather observations (METARs) and forecasts.aws-linux-memory-tools— tools to determine whether an AWS Linux instance is undersized on memory.get-wx— simple Open-Meteo weather parser written in Go.unix-hacks— Unix hacks collected over the decades.line-reorder-gui— GUI tool for reordering lines via drag and drop.ashpodder— podcast client (a fork of bashpodder, named for Ash from Evil Dead).WXTools— tools for collecting, notifying, and reporting weather events.ddir— standalone repo for theddirdirectory-diff tool also present in this repo.