From 1220b7ac80d4204c2641a201911b5f0ca825787d Mon Sep 17 00:00:00 2001 From: Laurie Reynolds Date: Fri, 10 Jul 2026 09:31:54 -0700 Subject: [PATCH] Add branch-guard snippet, global-packages doc, and remote-notify friction note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/branch-guard: local pre-commit + pre-push hooks that refuse commits/pushes to main — the real guard when a remote ruleset lets the repo owner (and thus an AI session on their credentials) bypass it. README documents Husky and plain core.hooksPath install plus the deny-list pairing that makes it a hard lock for an assistant. - docs/global-packages.md: global npm CLIs used as occasional sanity checks (ccusage, devlove, devrage), with a note on why /usage can't aggregate token usage across git worktrees and ccusage can. - docs/ai-collaboration.md: friction entry for questions buried in a summary not firing a notification over remote-control/loop, with a Stop-hook and AskUserQuestion handle. - README.md + docs/INDEX.md: register the new snippet and doc. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01SKEr4FwR8c3qz2WRDqgLHu --- README.md | 2 + docs/INDEX.md | 2 + docs/ai-collaboration.md | 41 ++++++++++++ docs/global-packages.md | 57 +++++++++++++++++ scripts/branch-guard/README.md | 108 ++++++++++++++++++++++++++++++++ scripts/branch-guard/pre-commit | 10 +++ scripts/branch-guard/pre-push | 13 ++++ 7 files changed, 233 insertions(+) create mode 100644 docs/global-packages.md create mode 100644 scripts/branch-guard/README.md create mode 100755 scripts/branch-guard/pre-commit create mode 100755 scripts/branch-guard/pre-push diff --git a/README.md b/README.md index 9097e01..84a7409 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ scripts/ cli-boilerplate/ Starter CLI in TS + Python: arg parsing, --help, leveled logging, exit codes http-fetch/ Zero-dep fetch wrapper: per-attempt timeout + retry/backoff + typed errors (TS + Python) secret-guard/ Pre-commit hook that blocks committing .env files + common secret patterns + branch-guard/ Local pre-commit + pre-push hooks that refuse commits/pushes to main kill-port/ Find and kill the process listening on a TCP port (macOS, lsof) a11y-check/ Standalone axe-core accessibility smoke check against a local URL playwright/ Minimal Playwright test (google.com) + axe-core a11y scan + config @@ -37,6 +38,7 @@ docs/ toolchain.md Inventory of the tools used in the next-template repo references.md External resources: agent skills, package hygiene, TS reference ai-collaboration.md Personal log of Claude Code friction points + fixes + global-packages.md Global npm CLIs installed occasionally as a sanity check (ccusage, devlove, devrage) backlog.md Candidate snippet ideas (the first batch has shipped) ``` diff --git a/docs/INDEX.md b/docs/INDEX.md index ee2b521..0e32484 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -22,6 +22,7 @@ Legend — **Deps:** `none` = language built-ins / stdlib only. | [cli-boilerplate](../scripts/cli-boilerplate) | Starter CLI: arg parsing, `--help`, leveled logging, exit codes | TS + Py | none (`tsx` dev-only) | project | Standing up a new Node or Python command-line script. | | [http-fetch](../scripts/http-fetch) | fetch wrapper: per-attempt timeout + retry/backoff + typed errors | TS (Node 18+) + Py | none | project | A script/backend needs resilient HTTP without `axios`/`requests`. Pairs with [ai-cache](../scripts/ai-cache). | | [secret-guard](../scripts/secret-guard) | Pre-commit hook blocking `.env` + common secret patterns | POSIX sh | git + grep | `.git/hooks` or `core.hooksPath` | Stop secrets leaking into a commit. Pre-commit only — not a history scanner. Companion to [keychain](../scripts/keychain). | +| [branch-guard](../scripts/branch-guard) | Local pre-commit + pre-push hooks that refuse commits/pushes to `main` | POSIX sh | git (Husky optional) | `.husky/` or `.githooks/` + `core.hooksPath` | You (or an AI session using your credentials) can bypass the remote ruleset, so only a local hook actually fires. Pairs with [secret-guard](../scripts/secret-guard). | | [kill-port](../scripts/kill-port) | Find and kill the process on a TCP port | POSIX sh | `lsof` + `kill` (macOS) | project / `bin` | A dev server is stuck on a port. Linux: use `fuser`/`ss`. | | [a11y-check](../scripts/a11y-check) | One-command axe-core accessibility smoke check of a URL | sh | `npx @axe-core/cli` + Chrome/driver | project | Quick a11y pass on a small site with no test setup. *Graduate to* [playwright](../scripts/playwright)/vitest-axe when you outgrow it. | | [playwright](../scripts/playwright) | Minimal Playwright e2e + axe-core a11y example (points at google.com) | TS | `@playwright/test`, `@axe-core/playwright` (ships `package.json`) | project | Starting browser/e2e tests. Shows strict + severity-gated a11y assertions. | @@ -51,5 +52,6 @@ Legend — **Deps:** `none` = language built-ins / stdlib only. - [toolchain.md](./toolchain.md) — tool inventory from the next-template repo. - [references.md](./references.md) — external resources (agent skills, package hygiene, TS). - [ai-collaboration.md](./ai-collaboration.md) — personal log of Claude Code friction points + fixes. +- [global-packages.md](./global-packages.md) — global npm CLIs installed occasionally as a sanity check (ccusage, devlove, devrage). - [backlog.md](./backlog.md) — candidate snippet ideas not yet built. - [INDEX.md](./INDEX.md) — this file. diff --git a/docs/ai-collaboration.md b/docs/ai-collaboration.md index d4fe40c..e1c1949 100644 --- a/docs/ai-collaboration.md +++ b/docs/ai-collaboration.md @@ -131,6 +131,47 @@ reading. Two guards, neither complete: The bias itself is a human-factors problem, not a settings one; the aim is to shrink the volume of prompts so the remaining ones get real attention. +### A question buried in a summary doesn't ping you (remote-control + `/loop`) + +When you're away and leaning on remote notifications, a session that ends an +iteration by *summarizing what it did and slipping a question into the prose* +doesn't fire a notification. Pings are triggered by **action-required** events — a +permission prompt or a structured choice (`AskUserQuestion`) — **not** by a question +written as ordinary text. Seen a few times while running `/loop` over remote-control: +the run effectively paused on an embedded question, but nothing pinged, so it just +sat there. + +Handles, neither complete: + +- **Ping on every turn-end, not just formal prompts.** A `Stop` hook fires whenever + the main agent yields the turn — including when it stops on a prose question — so + you're notified regardless of how the question is phrased: + + ```json + { + "hooks": { + "Stop": [ + { "hooks": [{ "type": "command", + "command": "osascript -e 'display notification \"Claude stopped — may be waiting\" with title \"Claude Code\"'" }] } + ] + } + } + ``` + + Blunt: it also pings on a clean "done," trading precision for never missing a + question. Complements the `permission_prompt|agent_completed` `Notification` hook + above rather than replacing it. + +- **Make Claude ask structurally, not in prose.** Instruct it (global `CLAUDE.md`) to + surface anything that needs an answer as an `AskUserQuestion` prompt instead of a + line in a wrap-up — structured questions are action-required and do ping. The + existing "use AskUserQuestion for 3+ alternatives" rule doesn't cover the single + yes/no buried in a summary; widen it to *any* real question when working remote. + +**Still open:** the root cause is that a prose question isn't classified as "action +required," so the model's own phrasing decides whether you get pinged. Neither handle +makes an in-summary question reliably notify on its own. + ### VSCode "open file" diff can drop changes When a file is open in VSCode, Claude sometimes surfaces the change as an editor diff; diff --git a/docs/global-packages.md b/docs/global-packages.md new file mode 100644 index 0000000..c2447c2 --- /dev/null +++ b/docs/global-packages.md @@ -0,0 +1,57 @@ +# Global packages + +Global npm CLIs worth having on a dev machine — installed once, run occasionally +as a **sanity check** rather than wired into any project. Not snippets to copy; +machine setup, like the *personal env* configs. + +```bash +npm i -g ccusage devlove devrage +``` + +(`ccusage` also runs fine one-off without a global install: `npx ccusage@latest`.) + +## ccusage + +> `npm i -g ccusage` · + +Analyzes coding-agent CLI **token usage and cost from local data** — it reads +Claude Code's own session transcripts under `~/.claude/projects/**/*.jsonl` and +sums tokens/cost, with daily/monthly/session breakdowns. Because it walks *every* +project directory on disk, it naturally spans **all git worktrees** in one total. + +### Why it matters with worktrees (and how it differs from `/usage`) + +Running work across git worktrees fragments Claude Code sessions: + +- The `wt` helper's `-c` flag ([`scripts/worktrees`](../scripts/worktrees)) opens a + new iTerm2 tab and launches a **fresh `claude` process** in the worktree + (`cd && claude`). It does not continue the session you ran `wt` from — + each worktree gets its own independent session. +- Claude Code keys session storage by working directory + (`~/.claude/projects//`). Different worktree = different path = + a separate project directory and separate session logs. + +Crucially, **`/usage` won't roll these up.** Its figures are computed from local +session history *on this machine* for the session/directory you run it in (the +docs note they exclude other devices and claude.ai), so `/usage` in one worktree +can't see the sessions logged under another worktree's directory. What you saw is +the documented behavior, not a `wt` quirk. + +`ccusage` sidesteps the fragmentation entirely: it aggregates across all those +per-worktree project directories, so it's the tool that gives a single complete +picture of usage when you've been working in parallel worktrees. + +## devlove + +> `npm i -g devlove` · + +Novelty CLI: counts how many times you've **been kind** to your coding agents +(scans your agent transcripts for polite/appreciative messages). Pure fun / vibe +check, not a metric to act on. + +## devrage + +> `npm i -g devrage` + +The counterpart to `devlove`: counts how many times you've **sworn at** your +coding agents. Also novelty — a lighthearted read on how a rough session went. diff --git a/scripts/branch-guard/README.md b/scripts/branch-guard/README.md new file mode 100644 index 0000000..a3740b6 --- /dev/null +++ b/scripts/branch-guard/README.md @@ -0,0 +1,108 @@ +# branch-guard + +> **Purpose:** Local git hooks that refuse to commit or push directly to `main`. · **Lang:** POSIX sh · **Deps:** git (Husky optional) +> **Copy to:** `.husky/` (Husky) or `.githooks/` + `core.hooksPath`. · **Use when:** you (or an AI session using your credentials) can bypass branch protection on the remote, so the only guard that actually fires is local. · **Related:** [secret-guard](../secret-guard) + +Two hooks — a **pre-commit** and a **pre-push** — that abort when you're on +`main` (commit) or pushing to `refs/heads/main` (push). Pure POSIX `sh` + `git`, +**zero runtime dependency**; Husky is just a convenient installer. + +## Why local, and why this is the real protection + +GitHub branch-protection rules / rulesets run **server-side**, but a ruleset that +lets the **repo owner bypass** it can't stop a push made with the owner's +credentials — which is exactly the case when an AI coding session (Claude Code, +etc.) pushes on your behalf. The server waves it through because it's "you." + +The **local** pre-push hook is the last checkpoint that sees the push *before it +leaves the machine*, so it's the layer that can actually catch an accidental +`git push` while `main` is checked out, or a stray `git push origin HEAD:main`. +That's the protection this snippet provides. + +## What each hook does + +- **`pre-commit`** — reads the current branch (`git rev-parse --abbrev-ref HEAD`) + and exits non-zero if it's `main`. Stops a commit from landing on `main` in the + first place. +- **`pre-push`** — reads git's stdin (` + ` per line) and exits non-zero if any `remote_ref` is + `refs/heads/main`. Catches `git push` (branch tracking main), `git push origin + main`, and `git push origin HEAD:main` alike. + +Both print a `✋` message and the intentional-override command, then exit `1`. + +## Install + +### (a) Husky (Node repos — how these were originally added) + +```bash +npm install --save-dev husky +npx husky init # creates .husky/ and wires the prepare script +cp scripts/branch-guard/pre-commit scripts/branch-guard/pre-push .husky/ +chmod +x .husky/pre-commit .husky/pre-push +``` + +Husky v9+ runs these bare hook files as-is (no `husky.sh` sourcing line needed). +The `prepare` script `husky init` adds means the hooks activate on `npm install` +for every clone — this is the team-shareable path. + +### (b) Plain `core.hooksPath` (no Node / no Husky) + +```bash +mkdir -p .githooks +cp scripts/branch-guard/pre-commit scripts/branch-guard/pre-push .githooks/ +chmod +x .githooks/pre-commit .githooks/pre-push +git config core.hooksPath .githooks # per-clone; each dev runs this once +``` + +Commit `.githooks/`. `core.hooksPath` is local git config, so wire the one-line +`git config` into your bootstrap/`make setup`. + +### Already have a pre-commit hook? (e.g. secret-guard) + +A repo has one file per hook, so **chain** them. With Husky, add the guard line +to the existing `.husky/pre-commit`; with plain hooks, call one from the other: + +```sh +# in your existing pre-commit, before your own logic: +"$(dirname "$0")/branch-guard-pre-commit" || exit 1 +``` + +This pairs naturally with [`secret-guard`](../secret-guard) (also a pre-commit): +one keeps secrets out of a commit, the other keeps commits off `main`. + +## Bypassing intentionally + +```bash +git commit --no-verify # or: git commit -n +git push --no-verify +``` + +> **Pair with a deny rule to make it a hard lock for an AI session.** On its own +> this hook is a speed-bump: `--no-verify` walks right past it. To close that for +> an assistant working with your credentials, **deny** the bypass commands in your +> Claude Code settings so the assistant can't reach for them: +> +> ```jsonc +> // .claude/settings.json → permissions.deny +> "Bash(git commit --no-verify:*)", +> "Bash(git commit -n:*)", +> "Bash(git push --no-verify:*)" +> ``` +> +> Now the hook stops the *accidental* push (plain `git push` on main) and the deny +> rule stops the *deliberate* override — while you, working in your own terminal +> outside Claude Code's permission layer, keep `--no-verify` for the legitimate +> owner bypass. Note the deny patterns are prefix matches: a flag in a different +> position (`git push origin main --no-verify`) won't match, so keep the branch as +> the guard's job and treat the deny list as belt-to-its-suspenders, not the sole +> lock. + +## Tweaks on copy-in + +- **Different default branch.** Change `main` to `master`/`develop` in both files + (the `[ "$branch" = "main" ]` test and the `refs/heads/main` case). +- **Guard several branches.** Widen the `case` in `pre-push`, e.g. + `refs/heads/main|refs/heads/release/*)`. +- **Message/tone.** The `echo` lines are yours to reword; keep them on stderr + (`>&2`) so they show even when git is quiet. diff --git a/scripts/branch-guard/pre-commit b/scripts/branch-guard/pre-commit new file mode 100755 index 0000000..678d853 --- /dev/null +++ b/scripts/branch-guard/pre-commit @@ -0,0 +1,10 @@ +#!/usr/bin/env sh +# Refuse to commit directly on main. The remote ruleset allows a bypass for the repo owner +# (and pushes use those credentials), so this local guard is the real protection against an +# accidental commit landing on main. Override intentionally with: git commit --no-verify +branch="$(git rev-parse --abbrev-ref HEAD)" +if [ "$branch" = "main" ]; then + echo "✋ Refusing to commit directly on main — create a branch first (git checkout -b …)." >&2 + echo " Override intentionally with: git commit --no-verify" >&2 + exit 1 +fi diff --git a/scripts/branch-guard/pre-push b/scripts/branch-guard/pre-push new file mode 100755 index 0000000..92d0e05 --- /dev/null +++ b/scripts/branch-guard/pre-push @@ -0,0 +1,13 @@ +#!/usr/bin/env sh +# Refuse to push to main. The remote ruleset allows a bypass for the repo owner, so this +# local guard is the real protection against an accidental push to main. +# Override intentionally with: git push --no-verify +while read -r _local_ref _local_sha remote_ref _remote_sha; do + case "$remote_ref" in + refs/heads/main) + echo "✋ Refusing to push to main — open a PR instead." >&2 + echo " Override intentionally with: git push --no-verify" >&2 + exit 1 + ;; + esac +done