Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
```

Expand Down
2 changes: 2 additions & 0 deletions docs/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down Expand Up @@ -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.
41 changes: 41 additions & 0 deletions docs/ai-collaboration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
57 changes: 57 additions & 0 deletions docs/global-packages.md
Original file line number Diff line number Diff line change
@@ -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` · <https://github.com/ccusage/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 <worktree> && 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/<sanitized-cwd>/`). 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` · <https://github.com/SirTenzin/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.
108 changes: 108 additions & 0 deletions scripts/branch-guard/README.md
Original file line number Diff line number Diff line change
@@ -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 (`<local ref> <local sha> <remote ref>
<remote sha>` 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.
10 changes: 10 additions & 0 deletions scripts/branch-guard/pre-commit
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions scripts/branch-guard/pre-push
Original file line number Diff line number Diff line change
@@ -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
Loading