aboutsummaryrefslogtreecommitdiff
path: root/working
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-24 12:28:18 -0500
committerCraig Jennings <c@cjennings.net>2026-07-24 12:28:18 -0500
commitf0c1bc40708615d5b423922c08c1f27e6cf96259 (patch)
tree5fc8ebe7fde6381f4cd91b5de15fe4a2521889c8 /working
parent781fa0786e2096797e630dcb81ffbc62ed98460b (diff)
downloadrulesets-f0c1bc40708615d5b423922c08c1f27e6cf96259.tar.gz
rulesets-f0c1bc40708615d5b423922c08c1f27e6cf96259.zip
fix(hooks): the secret scan no longer passes when git fails
Every bundle built its scan input as `git diff --cached ... | grep ... || true`. The `|| true` has to stay, since grep exits 1 when it matches nothing and that is the ordinary case. But with no pipefail it also swallowed a failure of git itself, so an empty result made the scan search nothing, find nothing, and report clean with a real credential staged. A gate that passes without having looked. .emacs.d found it in elisp. It was in all five: bash, elisp, go, python and typescript, eleven sites once each bundle's staged-file list is counted. Two of those bundles are ones I wrote yesterday by copying bash, so I propagated it while closing an unrelated gap in the same file. Each site now reads the diff on its own and aborts if git fails, leaving the greps their `|| true`. Verified per bundle on three axes: refuses when the diff cannot be read, still blocks a real staged secret, still passes a clean commit. The cross-bundle test suite had its own version of the same disease. Its VARIANTS list read "elisp bash go" while python and typescript also shipped hooks, so every "in every variant" assertion had quietly skipped two bundles since the day they were added. VARIANTS is now discovered from the tree. Two fail-closed assertions join it, and .emacs.d's elisp suite is adopted here beside the canonical hook, because a test living in the consuming project cannot fail when the canonical regresses. Also removes the validate-el auto-test cap, Craig's call. Above 20 matching test files the runner skipped everything and exited 0 with no output. The premise was speed and it did not hold: a whole family runs in about a second. The cap was also hiding a real cross-test pollution bug that only surfaces when a family runs in one process.
Diffstat (limited to 'working')
-rw-r--r--working/hook-fail-open/cap-note.txt35
-rw-r--r--working/hook-fail-open/pre-commit78
-rw-r--r--working/hook-fail-open/pre-commit.diff38
-rw-r--r--working/hook-fail-open/rulesets-hook-note.txt31
-rw-r--r--working/hook-fail-open/test-pre-commit-hook.bats126
-rw-r--r--working/hook-fail-open/test-validate-el-hook.bats97
-rw-r--r--working/hook-fail-open/test-validate-el-hook.bats.superseded73
-rw-r--r--working/hook-fail-open/validate-el.diff20
-rw-r--r--working/hook-fail-open/validate-el.sh120
-rw-r--r--working/hook-fail-open/validate-el.sh.superseded142
10 files changed, 0 insertions, 760 deletions
diff --git a/working/hook-fail-open/cap-note.txt b/working/hook-fail-open/cap-note.txt
deleted file mode 100644
index ba92101..0000000
--- a/working/hook-fail-open/cap-note.txt
+++ /dev/null
@@ -1,35 +0,0 @@
-SUPERSEDES the validate-el.sh I sent an hour ago (inbox 2026-07-24-0902). That version added a loud skip notice above the cap. Craig chose to remove the cap instead, and the reason is worth passing on.
-
-WHY THE CAP WENT
-
-The comment said "skip if more matches than this (large test suites)" — i.e. speed. Measured on this machine, running a whole family as the hook would:
-
- ai-term 35 files 1.0s 208 tests
- music 45 files 1.7s 403 tests
- calendar-sync 63 files 0.9s 633 tests
-
-Under two seconds each. The cap was not buying anything.
-
-Worse, it was concealing a real failure. calendar-sync exits 1 when its 63 files run in one process: test-calendar-sync--sync-calendar-skips-when-in-flight marks a calendar "proton" as syncing to exercise the dispatcher's in-flight guard, and never resets it — it cleared the module's state hash on entry but not on exit. test-calendar-sync--sync-dispatch-normal-ics-fetcher, in a sibling file, dispatches a calendar also named "proton", hits the leftover guard, and the dispatch it asserts never happens. ERT's alphabetical order puts the polluter first.
-
-That bug was invisible from both directions: `make test` runs each file in its own Emacs, and the hook skipped the family for being over the cap. So the cap was not just useless, it was hiding the one thing that would have caught the defect.
-
-CHANGES TO validate-el.sh (attached)
-
-- Removed MAX_AUTO_TEST_FILES and the upper bound on the gate; it is now `if [ "$count" -ge 1 ]`.
-- Removed the notice_json helper and the over-cap elif from the earlier submission — with no cap they are unreachable, and leaving dead code in a hook whose whole problem was a silent path seemed wrong.
-
-The pre-commit fix from the earlier note is unaffected and still stands as sent.
-
-BEFORE YOU ADOPT THIS
-
-Removing the cap means the hook runs every stem-matched test file on each edit. Two things to weigh for other consuming projects:
-
-1. The timings above are this machine and this project. A project with genuinely slow tests (network, sleeps, big fixtures) could feel it. The :slow tag filter is already applied, which covers the usual offenders.
-2. It surfaces cross-test pollution that per-file runs hide. That is a feature, but it means a project adopting this may go red on first use in a way it was not before. That is what happened here, and finding it was the point.
-
-I checked the other big families in this project as stem groups before removing the cap: ai-term, music, calendar-sync, dirvish and the org-agenda-config / org-agenda-frame groups all pass. There is a separate pollution bug between test-org-agenda-frame.el and test-org-agenda-config-commands.el, but no single stem matches both, so the hook never produces that combination. Filed locally.
-
-TESTS: test-validate-el-hook.bats attached, rewritten for the no-cap behavior (6 tests). Two of them stage a deliberately failing test file, so a quiet pass proves the run actually happened rather than being skipped — that is the property the old cap violated.
-
-No reply needed.
diff --git a/working/hook-fail-open/pre-commit b/working/hook-fail-open/pre-commit
deleted file mode 100644
index 8fa489d..0000000
--- a/working/hook-fail-open/pre-commit
+++ /dev/null
@@ -1,78 +0,0 @@
-#!/usr/bin/env bash
-# Pre-commit hook: secret scan + paren validation on staged .el files.
-# Use `git commit --no-verify` to bypass for confirmed false positives.
-
-set -u
-
-REPO_ROOT="$(git rev-parse --show-toplevel)"
-cd "$REPO_ROOT" || exit 1
-
-# --- 1. Secret scan ---
-# Patterns for common credentials. Scans only added lines in the staged diff.
-#
-# Two passes because case-sensitivity differs. AWS keys are uppercase, sk- keys
-# lowercase, PEM headers fixed, so those match case-SENSITIVELY: under -i,
-# AKIA[0-9A-Z]{16} matches any mixed-case 20-char run, which random base64 in an
-# embedded image blob hits ~6% of the time per 100KB and blocks real commits.
-# Only the keyword=value patterns need -i.
-SECRET_PATTERNS_CS='(AKIA[0-9A-Z]{16}|sk-[a-zA-Z0-9_-]{20,}|-----BEGIN (RSA|DSA|EC|OPENSSH|PGP)( PRIVATE)?( KEY| KEY BLOCK)?-----)'
-SECRET_PATTERNS_CI='(api[_-]?key|api[_-]?secret|auth[_-]?token|secret[_-]?key|bearer[_-]?token|access[_-]?token|password)[[:space:]]*[:=][[:space:]]*["'"'"'][^"'"'"']{16,}["'"'"']'
-
-# Read the diff on its own so a git failure is distinguishable from "grep
-# matched nothing". Both end in a non-zero status, but only one of them means
-# there is nothing to scan; piping them together and swallowing the result with
-# `|| true` made a broken git look like a clean commit.
-if ! staged_diff="$(git diff --cached -U0 --diff-filter=AM)"; then
- echo "pre-commit: cannot read the staged diff — refusing to skip the secret scan" >&2
- exit 1
-fi
-
-# The greps keep their `|| true`: exiting 1 on no match is their normal result.
-added_lines="$(printf '%s\n' "$staged_diff" | grep '^+' | grep -v '^+++' || true)"
-
-cs_hits="$(printf '%s\n' "$added_lines" | grep -nE "$SECRET_PATTERNS_CS" || true)"
-ci_hits="$(printf '%s\n' "$added_lines" | grep -niE "$SECRET_PATTERNS_CI" || true)"
-# awk dedupes lines both passes matched, keeping first-seen order.
-secret_hits="$(printf '%s\n%s' "$cs_hits" "$ci_hits" \
- | grep -v '^[[:space:]]*$' | awk '!seen[$0]++' || true)"
-
-if [ -n "$secret_hits" ]; then
- echo "pre-commit: potential secret in staged changes:" >&2
- echo "$secret_hits" >&2
- echo "" >&2
- echo "Review the lines above. If this is a false positive (test fixture, documentation)," >&2
- echo "bypass with: git commit --no-verify" >&2
- exit 1
-fi
-
-# --- 2. Paren check on staged .el files ---
-# Same split as the secret scan above: a git failure must not read as "no .el
-# files staged", which would skip the paren check silently.
-if ! staged_names="$(git diff --cached --name-only --diff-filter=AM)"; then
- echo "pre-commit: cannot read the staged file list — refusing to skip the paren check" >&2
- exit 1
-fi
-
-staged_el="$(printf '%s\n' "$staged_names" | grep '\.el$' || true)"
-
-if [ -n "$staged_el" ]; then
- paren_fail=""
- while IFS= read -r f; do
- [ -z "$f" ] && continue
- [ -f "$f" ] || continue
- if ! out="$(emacs --batch --no-site-file --no-site-lisp "$f" \
- --eval '(check-parens)' 2>&1)"; then
- paren_fail="${paren_fail}${f}:
-${out}
-
-"
- fi
- done <<< "$staged_el"
-
- if [ -n "$paren_fail" ]; then
- printf 'pre-commit: paren check failed:\n\n%s' "$paren_fail" >&2
- exit 1
- fi
-fi
-
-exit 0
diff --git a/working/hook-fail-open/pre-commit.diff b/working/hook-fail-open/pre-commit.diff
deleted file mode 100644
index 26b3d7e..0000000
--- a/working/hook-fail-open/pre-commit.diff
+++ /dev/null
@@ -1,38 +0,0 @@
---- languages/elisp/githooks/pre-commit 2026-07-23 20:37:08.136094097 -0500
-+++ working/hook-fail-open/pre-commit 2026-07-24 09:02:54.812347251 -0500
-@@ -18,8 +18,17 @@
- SECRET_PATTERNS_CS='(AKIA[0-9A-Z]{16}|sk-[a-zA-Z0-9_-]{20,}|-----BEGIN (RSA|DSA|EC|OPENSSH|PGP)( PRIVATE)?( KEY| KEY BLOCK)?-----)'
- SECRET_PATTERNS_CI='(api[_-]?key|api[_-]?secret|auth[_-]?token|secret[_-]?key|bearer[_-]?token|access[_-]?token|password)[[:space:]]*[:=][[:space:]]*["'"'"'][^"'"'"']{16,}["'"'"']'
-
--added_lines="$(git diff --cached -U0 --diff-filter=AM \
-- | grep '^+' | grep -v '^+++' || true)"
-+# Read the diff on its own so a git failure is distinguishable from "grep
-+# matched nothing". Both end in a non-zero status, but only one of them means
-+# there is nothing to scan; piping them together and swallowing the result with
-+# `|| true` made a broken git look like a clean commit.
-+if ! staged_diff="$(git diff --cached -U0 --diff-filter=AM)"; then
-+ echo "pre-commit: cannot read the staged diff — refusing to skip the secret scan" >&2
-+ exit 1
-+fi
-+
-+# The greps keep their `|| true`: exiting 1 on no match is their normal result.
-+added_lines="$(printf '%s\n' "$staged_diff" | grep '^+' | grep -v '^+++' || true)"
-
- cs_hits="$(printf '%s\n' "$added_lines" | grep -nE "$SECRET_PATTERNS_CS" || true)"
- ci_hits="$(printf '%s\n' "$added_lines" | grep -niE "$SECRET_PATTERNS_CI" || true)"
-@@ -37,7 +46,14 @@
- fi
-
- # --- 2. Paren check on staged .el files ---
--staged_el="$(git diff --cached --name-only --diff-filter=AM | grep '\.el$' || true)"
-+# Same split as the secret scan above: a git failure must not read as "no .el
-+# files staged", which would skip the paren check silently.
-+if ! staged_names="$(git diff --cached --name-only --diff-filter=AM)"; then
-+ echo "pre-commit: cannot read the staged file list — refusing to skip the paren check" >&2
-+ exit 1
-+fi
-+
-+staged_el="$(printf '%s\n' "$staged_names" | grep '\.el$' || true)"
-
- if [ -n "$staged_el" ]; then
- paren_fail=""
diff --git a/working/hook-fail-open/rulesets-hook-note.txt b/working/hook-fail-open/rulesets-hook-note.txt
deleted file mode 100644
index 1c7a813..0000000
--- a/working/hook-fail-open/rulesets-hook-note.txt
+++ /dev/null
@@ -1,31 +0,0 @@
-Two hook fixes from .emacs.d, both the same defect class: a quality gate that reports success without having looked. Both files are rulesets-owned, so these local edits revert on the next sync — please take them into the canonicals.
-
-FILE 1: languages/elisp/githooks/pre-commit
-
-The secret scan built its input like this:
-
- added_lines="$(git diff --cached -U0 --diff-filter=AM | grep '^+' | grep -v '^+++' || true)"
-
-No pipefail, and || true swallows everything. Any git failure yields an empty added_lines, so the scan searches nothing, finds nothing, reports clean, and the commit proceeds with a real secret in it. Reproduced with a stub git that fails on the staged-diff call: the hook exited 0 with an AWS-shaped key staged.
-
-The || true cannot simply be removed — grep exits 1 when it matches nothing, which is the ordinary case for a commit with no added lines. The fix separates the two: read the diff on its own and abort on a git failure, then let the greps keep their || true.
-
-The staged-file list feeding the paren check (line 40 in the old file) had the identical hole, so a git failure there silently skipped paren validation. Fixed the same way.
-
-Note the local copy was already identical to the canonical before this change, so there is no other drift to reconcile.
-
-FILE 2: the elisp bundle's .claude/hooks/validate-el.sh
-
-The auto-test runner is gated on `[ "$count" -ge 1 ] && [ "$count" -le "$MAX_AUTO_TEST_FILES" ]` with no else branch. Above the cap the block is skipped, nothing is printed, and the hook exits 0 — indistinguishable from a passing run. Live for the three largest families in .emacs.d: calendar-sync 63 test files, music 45, ai-term 35. Every edit to those ran parens and byte-compile and zero tests, silently, and this has already let a wrong test assertion through more than once.
-
-The cap is not the problem and I did not change it. The silence is. Added a notice_json helper (reports via additionalContext + stderr, exits 0 rather than 2) and an elif that names the count, states plainly that it is NOT a pass, and gives the make test-file command. Live output on a calendar-sync edit:
-
- TESTS SKIPPED: 63 test files match calendar-sync.el — over the auto-run cap of 20, so none ran. This is NOT a pass. Run them explicitly, e.g. make test-file FILE=test-calendar-sync--add-days.el
-
-TESTS: both files now have bats coverage, attached as tests/test-pre-commit-hook.bats and tests/test-validate-el-hook.bats (8 + 4 tests, all green, shellcheck clean at warning level). They were written red-first: the fail-open tests drive a stubbed failing git, and the over-cap tests build a synthetic project with 21 test files. If rulesets wants these tests alongside the canonical hooks rather than in the consuming project, they should move — your call on where they belong, since the hooks are yours and the tests currently live in ours.
-
-One caveat on the pre-commit bats file: it deliberately assembles its credential fixtures at runtime (AKIA + tail) rather than embedding literals, because a literal key-shaped string trips the very hook under test on every commit touching that file. Worth preserving if you adopt it.
-
-CONTEXT: found during a skeptical review pass. The pre-commit one is the more interesting find — it surfaced *because* a reviewer was checking an unrelated one-line fix to the same file and noticed the fixed instance was the harmless one while the dangerous one three lines down was untouched. This is the fifth instance in .emacs.d of the family "a gate that enumerates or caps or swallows instead of discovering" — the earlier ones being a Makefile wildcard bug that ran zero integration tests for months, stale .elc masking unparseable source, and a hardcoded benchmark list.
-
-No reply needed unless you want the tests re-homed or the wording changed.
diff --git a/working/hook-fail-open/test-pre-commit-hook.bats b/working/hook-fail-open/test-pre-commit-hook.bats
deleted file mode 100644
index 413c71d..0000000
--- a/working/hook-fail-open/test-pre-commit-hook.bats
+++ /dev/null
@@ -1,126 +0,0 @@
-#!/usr/bin/env bats
-# Tests for githooks/pre-commit — the secret scan and paren check.
-#
-# The scan reads its input through a pipeline:
-#
-# added_lines="$(git diff --cached ... | grep '^+' | grep -v '^+++' || true)"
-#
-# `grep` exits 1 when it matches nothing, which is the ordinary case, so the
-# `|| true` has to stay. But with no `pipefail` it also swallows a failure of
-# `git diff` itself, and an empty `added_lines` makes the scan search nothing,
-# find nothing, and report clean. A gate that passes without looking is the
-# failure this file exists to pin: the fail-open test drives a broken `git diff`
-# and asserts the hook refuses rather than exiting 0.
-#
-# Each test builds a throwaway git repo in BATS_TEST_TMPDIR, so nothing touches
-# the real repository or its hooks.
-
-setup() {
- HOOK="${BATS_TEST_DIRNAME}/../githooks/pre-commit"
- REPO="${BATS_TEST_TMPDIR}/repo"
- mkdir -p "$REPO"
- cd "$REPO" || return 1
- git init -q .
- git config user.email t@example.com
- git config user.name Test
- # Split so the fixtures never appear as credential-shaped literals here.
- AWS_TAIL="IOSFODNN7EXAMPLE"
- WORD_TAIL="word"
-}
-
-# Put a stub `git` ahead of the real one that fails for the staged-diff call
-# and delegates everything else, so only the pipeline under test breaks.
-break_staged_diff() {
- mkdir -p "${BATS_TEST_TMPDIR}/bin"
- cat > "${BATS_TEST_TMPDIR}/bin/git" <<'STUB'
-#!/usr/bin/env bash
-if [ "${1:-}" = "diff" ] && [ "${2:-}" = "--cached" ] && [ "${3:-}" = "-U0" ]; then
- echo "simulated git failure" >&2
- exit 128
-fi
-exec /usr/bin/git "$@"
-STUB
- chmod +x "${BATS_TEST_TMPDIR}/bin/git"
- PATH="${BATS_TEST_TMPDIR}/bin:$PATH"
-}
-
-# ------------------------------- Normal cases -------------------------------
-
-@test "secret scan: blocks a staged AWS key" {
- # Assembled at runtime: a literal key-shaped string in this file would trip
- # the very hook under test on every commit that touches it, and this repo
- # mirrors to a public remote.
- printf 'aws = "%s"\n' "AKIA${AWS_TAIL}" > creds.txt
- git add creds.txt
- run "$HOOK"
- [ "$status" -eq 1 ]
- [[ "$output" == *"potential secret"* ]]
-}
-
-@test "secret scan: blocks a staged keyword=value password" {
- printf '%s = "%s"\n' "pass${WORD_TAIL}" "correcthorsebatterystaple" > conf.txt
- git add conf.txt
- run "$HOOK"
- [ "$status" -eq 1 ]
- [[ "$output" == *"potential secret"* ]]
-}
-
-@test "secret scan: allows an ordinary staged file" {
- printf 'just some prose\n' > notes.txt
- git add notes.txt
- run "$HOOK"
- [ "$status" -eq 0 ]
-}
-
-# ------------------------------ Boundary cases ------------------------------
-
-@test "secret scan: allows a commit with nothing staged" {
- run "$HOOK"
- [ "$status" -eq 0 ]
-}
-
-@test "paren check: blocks an unbalanced staged .el file" {
- printf '(defun broken ()\n (message "no close"\n' > bad.el
- git add bad.el
- run "$HOOK"
- [ "$status" -eq 1 ]
- [[ "$output" == *"paren check failed"* ]]
-}
-
-@test "paren check: allows a balanced staged .el file" {
- printf '(defun fine ()\n (message "ok"))\n' > good.el
- git add good.el
- run "$HOOK"
- [ "$status" -eq 0 ]
-}
-
-# -------------------------------- Error cases -------------------------------
-
-@test "secret scan: refuses to pass when the staged diff cannot be read" {
- # The scan must not report clean after searching nothing. Without a
- # pipefail-aware guard the broken diff yields an empty added_lines and the
- # hook exits 0, letting a real secret through unscanned.
- printf 'aws = "%s"\n' "AKIA${AWS_TAIL}" > creds.txt
- git add creds.txt
- break_staged_diff
- run "$HOOK"
- [ "$status" -ne 0 ]
-}
-
-@test "paren check: refuses to pass when the staged file list cannot be read" {
- printf '(defun broken ()\n (message "no close"\n' > bad.el
- git add bad.el
- mkdir -p "${BATS_TEST_TMPDIR}/bin2"
- cat > "${BATS_TEST_TMPDIR}/bin2/git" <<'STUB'
-#!/usr/bin/env bash
-if [ "${1:-}" = "diff" ] && [ "${2:-}" = "--cached" ] && [ "${3:-}" = "--name-only" ]; then
- echo "simulated git failure" >&2
- exit 128
-fi
-exec /usr/bin/git "$@"
-STUB
- chmod +x "${BATS_TEST_TMPDIR}/bin2/git"
- PATH="${BATS_TEST_TMPDIR}/bin2:$PATH"
- run "$HOOK"
- [ "$status" -ne 0 ]
-}
diff --git a/working/hook-fail-open/test-validate-el-hook.bats b/working/hook-fail-open/test-validate-el-hook.bats
deleted file mode 100644
index 43c3569..0000000
--- a/working/hook-fail-open/test-validate-el-hook.bats
+++ /dev/null
@@ -1,97 +0,0 @@
-#!/usr/bin/env bats
-# Tests for .claude/hooks/validate-el.sh — the auto-test runner.
-#
-# The runner used to skip entirely above MAX_AUTO_TEST_FILES=20, with no else
-# branch: nothing printed, exit 0, indistinguishable from a passing run. That
-# was live for the three largest families here (calendar-sync 63 test files,
-# music 45, ai-term 35), so every edit to those ran parens and byte-compile and
-# zero tests, silently.
-#
-# The cap was removed rather than made loud, because its premise did not hold.
-# Measured on this machine, running a whole family takes about a second:
-# ai-term 208 tests in 1.0s, music 403 in 1.7s, calendar-sync 633 in 0.9s. It
-# was also concealing a real cross-test pollution bug in calendar-sync that
-# only appears when that family runs in one process.
-#
-# These tests pin that no file count is skipped. Each builds a synthetic
-# project in BATS_TEST_TMPDIR and points CLAUDE_PROJECT_DIR at it, so nothing
-# runs against the real tree.
-
-setup() {
- HOOK="${BATS_TEST_DIRNAME}/../.claude/hooks/validate-el.sh"
- PROJ="${BATS_TEST_TMPDIR}/proj"
- mkdir -p "$PROJ/modules" "$PROJ/tests"
- export CLAUDE_PROJECT_DIR="$PROJ"
- printf '(provide (quote widget))\n' > "$PROJ/modules/widget.el"
-}
-
-# N green test files matching the widget stem.
-make_tests() {
- local n="$1" i
- for ((i = 1; i <= n; i++)); do
- printf '(require (quote ert))\n(ert-deftest test-widget-%d () (should t))\n' \
- "$i" > "$PROJ/tests/test-widget-${i}.el"
- done
-}
-
-# One failing test file, to prove the run is real rather than merely quiet.
-make_failing_test() {
- printf '(require (quote ert))\n(ert-deftest test-widget-bad () (should nil))\n' \
- > "$PROJ/tests/test-widget-bad.el"
-}
-
-hook_input() {
- printf '{"tool_input":{"file_path":"%s"}}' "$PROJ/modules/widget.el"
-}
-
-run_hook() {
- run bash -c "$(printf '%q' "$HOOK") <<< '$(hook_input)'"
-}
-
-# ------------------------------- Normal cases -------------------------------
-
-@test "a small family runs and passes quietly" {
- make_tests 3
- run_hook
- [ "$status" -eq 0 ]
-}
-
-@test "a failing test blocks, so a quiet pass means the tests really ran" {
- make_tests 3
- make_failing_test
- run_hook
- [ "$status" -eq 2 ]
- [[ "$output" == *"TESTS FAILED"* ]]
-}
-
-# ------------------------------ Boundary cases ------------------------------
-
-@test "at the old cap of 20 files: runs" {
- make_tests 20
- run_hook
- [ "$status" -eq 0 ]
-}
-
-@test "past the old cap: still runs, no longer skipped" {
- make_tests 21
- run_hook
- [ "$status" -eq 0 ]
- [[ "${output,,}" != *"skipped"* ]]
-}
-
-@test "well past the old cap: a failure in file 63 is still caught" {
- # The regression this guards: at 63 files the runner used to skip, so a red
- # test in a big family reported clean. calendar-sync is exactly this size.
- make_tests 63
- make_failing_test
- run_hook
- [ "$status" -eq 2 ]
- [[ "$output" == *"TESTS FAILED"* ]]
-}
-
-# -------------------------------- Error cases -------------------------------
-
-@test "no matching tests: exits clean without running anything" {
- run_hook
- [ "$status" -eq 0 ]
-}
diff --git a/working/hook-fail-open/test-validate-el-hook.bats.superseded b/working/hook-fail-open/test-validate-el-hook.bats.superseded
deleted file mode 100644
index 2dbcae7..0000000
--- a/working/hook-fail-open/test-validate-el-hook.bats.superseded
+++ /dev/null
@@ -1,73 +0,0 @@
-#!/usr/bin/env bats
-# Tests for .claude/hooks/validate-el.sh — specifically the auto-test cap.
-#
-# The hook runs the tests matching an edited file, but only when the match
-# count is between 1 and MAX_AUTO_TEST_FILES. Above the cap the whole block
-# was skipped with no else branch: nothing printed, exit 0, indistinguishable
-# from a passing run. That is live for the three largest families here
-# (calendar-sync 63 test files, music 45, ai-term 35), so every edit to those
-# modules ran parens and byte-compile and zero tests, silently.
-#
-# The cap itself is fine — running 63 files per keystroke is not wanted. The
-# defect is the silence, so these tests assert the skip announces itself and
-# names what to run.
-#
-# Each test builds a synthetic project in BATS_TEST_TMPDIR and points
-# CLAUDE_PROJECT_DIR at it, so nothing runs against the real tree.
-
-setup() {
- HOOK="${BATS_TEST_DIRNAME}/../.claude/hooks/validate-el.sh"
- PROJ="${BATS_TEST_TMPDIR}/proj"
- mkdir -p "$PROJ/modules" "$PROJ/tests"
- export CLAUDE_PROJECT_DIR="$PROJ"
- printf '(provide (quote widget))\n' > "$PROJ/modules/widget.el"
-}
-
-# Create N test files matching the widget stem. Each is trivially green so a
-# run below the cap succeeds and the only variable is the count.
-make_tests() {
- local n="$1" i
- for ((i = 1; i <= n; i++)); do
- printf '(require (quote ert))\n(ert-deftest test-widget-%d () (should t))\n' \
- "$i" > "$PROJ/tests/test-widget-${i}.el"
- done
-}
-
-hook_input() {
- printf '{"tool_input":{"file_path":"%s"}}' "$PROJ/modules/widget.el"
-}
-
-# ------------------------------- Normal cases -------------------------------
-
-@test "under the cap: runs the tests and stays quiet on success" {
- make_tests 3
- run bash -c "$(printf '%q' "$HOOK") <<< '$(hook_input)'"
- [ "$status" -eq 0 ]
- [[ "${output,,}" != *"skipped"* ]]
-}
-
-# ------------------------------ Boundary cases ------------------------------
-
-@test "exactly at the cap: still runs the tests" {
- make_tests 20
- run bash -c "$(printf '%q' "$HOOK") <<< '$(hook_input)'"
- [ "$status" -eq 0 ]
- [[ "${output,,}" != *"skipped"* ]]
-}
-
-# -------------------------------- Error cases -------------------------------
-
-@test "over the cap: says it skipped rather than exiting silently" {
- make_tests 21
- run bash -c "$(printf '%q' "$HOOK") <<< '$(hook_input)'"
- # Must not fail the edit — the cap is deliberate, the silence is not.
- [ "$status" -eq 0 ]
- [[ "${output,,}" == *"skipped"* ]]
-}
-
-@test "over the cap: names the count and how to run them" {
- make_tests 21
- run bash -c "$(printf '%q' "$HOOK") <<< '$(hook_input)'"
- [[ "$output" == *"21"* ]]
- [[ "$output" == *"make test-file"* ]]
-}
diff --git a/working/hook-fail-open/validate-el.diff b/working/hook-fail-open/validate-el.diff
deleted file mode 100644
index ab0fc15..0000000
--- a/working/hook-fail-open/validate-el.diff
+++ /dev/null
@@ -1,20 +0,0 @@
---- languages/elisp/claude/hooks/validate-el.sh 2026-07-13 23:37:55.474616165 -0500
-+++ working/hook-fail-open/validate-el.sh 2026-07-24 10:58:47.774106870 -0500
-@@ -39,8 +39,6 @@
- [ -z "$f" ] && exit 0
- [ "${f##*.}" = "el" ] || exit 0
-
--MAX_AUTO_TEST_FILES=20 # skip if more matches than this (large test suites)
--
- # --- Phase 1: syntax + byte-compile ---
- case "$f" in
- */init.el|*/early-init.el)
-@@ -96,7 +94,7 @@
- esac
-
- count="${#tests[@]}"
--if [ "$count" -ge 1 ] && [ "$count" -le "$MAX_AUTO_TEST_FILES" ]; then
-+if [ "$count" -ge 1 ]; then
- load_args=()
- for t in "${tests[@]}"; do load_args+=("-l" "$t"); done
- if ! output="$(emacs --batch --no-site-file --no-site-lisp \
diff --git a/working/hook-fail-open/validate-el.sh b/working/hook-fail-open/validate-el.sh
deleted file mode 100644
index 870eefe..0000000
--- a/working/hook-fail-open/validate-el.sh
+++ /dev/null
@@ -1,120 +0,0 @@
-#!/usr/bin/env bash
-# Validate and test .el files after Edit/Write/MultiEdit.
-# PostToolUse hook: receives tool-call JSON on stdin.
-#
-# On success: exit 0 silent.
-# On failure: emit JSON with hookSpecificOutput.additionalContext so Claude
-# sees a structured error in its context, THEN exit 2 to block the tool
-# pipeline. stderr still echoes the error for terminal visibility.
-#
-# Phase 1: check-parens + byte-compile
-# Phase 2: for non-test .el files, run matching tests/test-<stem>*.el
-
-set -u
-
-# Emit a JSON failure payload and exit 2. Arguments:
-# $1 — short failure type (e.g. "PAREN CHECK FAILED")
-# $2 — file path
-# $3 — emacs output (error body), always sent to Claude in additionalContext
-# $4 — optional compact terminal echo; when set, the terminal shows this
-# instead of the full $3 (Claude still gets the full $3). Used by the
-# test runner so a failing suite prints a short summary to the pane
-# rather than dumping every ERT backtrace.
-fail_json() {
- local ctx
- ctx="$(printf '%s: %s\n\n%s\n\nFix before proceeding.' "$1" "$2" "$3" \
- | jq -Rs .)"
- cat <<EOF
-{"hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": $ctx}}
-EOF
- printf '%s: %s\n%s\n' "$1" "$2" "${4:-$3}" >&2
- exit 2
-}
-
-# Portable project root: prefer Claude Code's env var, fall back to deriving
-# from this script's location ($project/.claude/hooks/validate-el.sh).
-PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/../.." && pwd)}"
-
-f="$(jq -r '.tool_input.file_path // .tool_response.filePath // empty')"
-[ -z "$f" ] && exit 0
-[ "${f##*.}" = "el" ] || exit 0
-
-# --- Phase 1: syntax + byte-compile ---
-case "$f" in
- */init.el|*/early-init.el)
- # Byte-compile here would load the full package graph. Parens only.
- if ! output="$(emacs --batch --no-site-file --no-site-lisp "$f" \
- --eval '(check-parens)' 2>&1)"; then
- fail_json "PAREN CHECK FAILED" "$f" "$output"
- fi
- ;;
- *.el)
- # -L the file's own directory (and a sibling project root for files
- # under a tests/ subdir) so cross-project edits compile against their
- # own modules, not just this project's.
- if ! output="$(emacs --batch --no-site-file --no-site-lisp \
- --eval '(setq load-prefer-newer t)' \
- -L "$(dirname "$f")" \
- -L "$(dirname "$f")/.." \
- -L "$PROJECT_ROOT" \
- -L "$PROJECT_ROOT/modules" \
- -L "$PROJECT_ROOT/tests" \
- -L "$PROJECT_ROOT/themes" \
- --eval '(package-initialize)' \
- "$f" \
- --eval '(check-parens)' \
- --eval "(or (byte-compile-file \"$f\") (kill-emacs 1))" 2>&1)"; then
- fail_json "VALIDATION FAILED" "$f" "$output"
- fi
- ;;
-esac
-
-# --- Phase 2: test runner ---
-# Determine which tests (if any) apply to this edit. Works for projects with
-# source at root, in modules/, or elsewhere — stem-based test lookup is the
-# common pattern.
-tests=()
-case "$f" in
- */init.el|*/early-init.el)
- : # Phase 1 handled it; skip test runner
- ;;
- "$PROJECT_ROOT/tests/testutil-"*.el)
- stem="$(basename "${f%.el}")"
- stem="${stem#testutil-}"
- mapfile -t tests < <(find "$PROJECT_ROOT/tests" -maxdepth 1 -name "test-${stem}*.el" 2>/dev/null | sort)
- ;;
- "$PROJECT_ROOT/tests/test-"*.el)
- tests=("$f")
- ;;
- *.el)
- # Any other .el under the project — find matching tests by stem
- stem="$(basename "${f%.el}")"
- mapfile -t tests < <(find "$PROJECT_ROOT/tests" -maxdepth 1 -name "test-${stem}*.el" 2>/dev/null | sort)
- ;;
-esac
-
-count="${#tests[@]}"
-if [ "$count" -ge 1 ]; then
- load_args=()
- for t in "${tests[@]}"; do load_args+=("-l" "$t"); done
- if ! output="$(emacs --batch --no-site-file --no-site-lisp \
- --eval '(setq load-prefer-newer t)' \
- -L "$PROJECT_ROOT" \
- -L "$PROJECT_ROOT/modules" \
- -L "$PROJECT_ROOT/tests" \
- -L "$PROJECT_ROOT/themes" \
- --eval '(package-initialize)' \
- --eval "(cd \"$PROJECT_ROOT/tests\")" \
- -l ert "${load_args[@]}" \
- --eval "(ert-run-tests-batch-and-exit '(not (tag :slow)))" 2>&1)"; then
- # Terminal gets a compact summary (the run tally + the failing test names);
- # Claude still gets the full backtrace via additionalContext. Keeps the
- # pane from drowning in ERT stack frames on every red test.
- summary="$(printf '%s\n' "$output" \
- | grep -E '^Ran [0-9]+ tests|unexpected results:|^[[:space:]]+FAILED' || true)"
- [ -n "$summary" ] && summary="${summary}"$'\n'"(full backtrace in Claude's context)"
- fail_json "TESTS FAILED ($count test file(s))" "$f" "$output" "$summary"
- fi
-fi
-
-exit 0
diff --git a/working/hook-fail-open/validate-el.sh.superseded b/working/hook-fail-open/validate-el.sh.superseded
deleted file mode 100644
index 045c93f..0000000
--- a/working/hook-fail-open/validate-el.sh.superseded
+++ /dev/null
@@ -1,142 +0,0 @@
-#!/usr/bin/env bash
-# Validate and test .el files after Edit/Write/MultiEdit.
-# PostToolUse hook: receives tool-call JSON on stdin.
-#
-# On success: exit 0 silent.
-# On failure: emit JSON with hookSpecificOutput.additionalContext so Claude
-# sees a structured error in its context, THEN exit 2 to block the tool
-# pipeline. stderr still echoes the error for terminal visibility.
-#
-# Phase 1: check-parens + byte-compile
-# Phase 2: for non-test .el files, run matching tests/test-<stem>*.el
-
-set -u
-
-# Emit a JSON failure payload and exit 2. Arguments:
-# $1 — short failure type (e.g. "PAREN CHECK FAILED")
-# $2 — file path
-# $3 — emacs output (error body), always sent to Claude in additionalContext
-# $4 — optional compact terminal echo; when set, the terminal shows this
-# instead of the full $3 (Claude still gets the full $3). Used by the
-# test runner so a failing suite prints a short summary to the pane
-# rather than dumping every ERT backtrace.
-fail_json() {
- local ctx
- ctx="$(printf '%s: %s\n\n%s\n\nFix before proceeding.' "$1" "$2" "$3" \
- | jq -Rs .)"
- cat <<EOF
-{"hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": $ctx}}
-EOF
- printf '%s: %s\n%s\n' "$1" "$2" "${4:-$3}" >&2
- exit 2
-}
-
-# Report something without failing the edit. Used when the hook declines to do
-# work it would normally do: a silent decline is indistinguishable from a clean
-# run, which is how the over-cap skip below hid missing test runs for months.
-# $1 — message for both the terminal and Claude's context
-notice_json() {
- local ctx
- ctx="$(printf '%s' "$1" | jq -Rs .)"
- cat <<EOF
-{"hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": $ctx}}
-EOF
- printf '%s\n' "$1" >&2
-}
-
-# Portable project root: prefer Claude Code's env var, fall back to deriving
-# from this script's location ($project/.claude/hooks/validate-el.sh).
-PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/../.." && pwd)}"
-
-f="$(jq -r '.tool_input.file_path // .tool_response.filePath // empty')"
-[ -z "$f" ] && exit 0
-[ "${f##*.}" = "el" ] || exit 0
-
-MAX_AUTO_TEST_FILES=20 # skip if more matches than this (large test suites)
-
-# --- Phase 1: syntax + byte-compile ---
-case "$f" in
- */init.el|*/early-init.el)
- # Byte-compile here would load the full package graph. Parens only.
- if ! output="$(emacs --batch --no-site-file --no-site-lisp "$f" \
- --eval '(check-parens)' 2>&1)"; then
- fail_json "PAREN CHECK FAILED" "$f" "$output"
- fi
- ;;
- *.el)
- # -L the file's own directory (and a sibling project root for files
- # under a tests/ subdir) so cross-project edits compile against their
- # own modules, not just this project's.
- if ! output="$(emacs --batch --no-site-file --no-site-lisp \
- --eval '(setq load-prefer-newer t)' \
- -L "$(dirname "$f")" \
- -L "$(dirname "$f")/.." \
- -L "$PROJECT_ROOT" \
- -L "$PROJECT_ROOT/modules" \
- -L "$PROJECT_ROOT/tests" \
- -L "$PROJECT_ROOT/themes" \
- --eval '(package-initialize)' \
- "$f" \
- --eval '(check-parens)' \
- --eval "(or (byte-compile-file \"$f\") (kill-emacs 1))" 2>&1)"; then
- fail_json "VALIDATION FAILED" "$f" "$output"
- fi
- ;;
-esac
-
-# --- Phase 2: test runner ---
-# Determine which tests (if any) apply to this edit. Works for projects with
-# source at root, in modules/, or elsewhere — stem-based test lookup is the
-# common pattern.
-tests=()
-case "$f" in
- */init.el|*/early-init.el)
- : # Phase 1 handled it; skip test runner
- ;;
- "$PROJECT_ROOT/tests/testutil-"*.el)
- stem="$(basename "${f%.el}")"
- stem="${stem#testutil-}"
- mapfile -t tests < <(find "$PROJECT_ROOT/tests" -maxdepth 1 -name "test-${stem}*.el" 2>/dev/null | sort)
- ;;
- "$PROJECT_ROOT/tests/test-"*.el)
- tests=("$f")
- ;;
- *.el)
- # Any other .el under the project — find matching tests by stem
- stem="$(basename "${f%.el}")"
- mapfile -t tests < <(find "$PROJECT_ROOT/tests" -maxdepth 1 -name "test-${stem}*.el" 2>/dev/null | sort)
- ;;
-esac
-
-count="${#tests[@]}"
-if [ "$count" -ge 1 ] && [ "$count" -le "$MAX_AUTO_TEST_FILES" ]; then
- load_args=()
- for t in "${tests[@]}"; do load_args+=("-l" "$t"); done
- if ! output="$(emacs --batch --no-site-file --no-site-lisp \
- --eval '(setq load-prefer-newer t)' \
- -L "$PROJECT_ROOT" \
- -L "$PROJECT_ROOT/modules" \
- -L "$PROJECT_ROOT/tests" \
- -L "$PROJECT_ROOT/themes" \
- --eval '(package-initialize)' \
- --eval "(cd \"$PROJECT_ROOT/tests\")" \
- -l ert "${load_args[@]}" \
- --eval "(ert-run-tests-batch-and-exit '(not (tag :slow)))" 2>&1)"; then
- # Terminal gets a compact summary (the run tally + the failing test names);
- # Claude still gets the full backtrace via additionalContext. Keeps the
- # pane from drowning in ERT stack frames on every red test.
- summary="$(printf '%s\n' "$output" \
- | grep -E '^Ran [0-9]+ tests|unexpected results:|^[[:space:]]+FAILED' || true)"
- [ -n "$summary" ] && summary="${summary}"$'\n'"(full backtrace in Claude's context)"
- fail_json "TESTS FAILED ($count test file(s))" "$f" "$output" "$summary"
- fi
-elif [ "$count" -gt "$MAX_AUTO_TEST_FILES" ]; then
- # Say so. Running this many files on every keystroke is not wanted, but a
- # silent skip reads exactly like a passing run, so an edit to one of the big
- # families looked verified when nothing had been tested.
- notice_json "TESTS SKIPPED: $count test files match $(basename "$f") — over the \
-auto-run cap of $MAX_AUTO_TEST_FILES, so none ran. This is NOT a pass. Run them \
-explicitly, e.g. make test-file FILE=$(basename "${tests[0]}")"
-fi
-
-exit 0