aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xlanguages/bash/githooks/pre-commit24
-rwxr-xr-xlanguages/elisp/claude/hooks/validate-el.sh4
-rwxr-xr-xlanguages/elisp/githooks/pre-commit23
-rw-r--r--languages/elisp/tests/test-pre-commit-hook.bats (renamed from working/hook-fail-open/test-pre-commit-hook.bats)0
-rw-r--r--languages/elisp/tests/test-validate-el-hook.bats (renamed from working/hook-fail-open/test-validate-el-hook.bats)5
-rwxr-xr-xlanguages/go/githooks/pre-commit24
-rwxr-xr-xlanguages/python/githooks/pre-commit24
-rwxr-xr-xlanguages/typescript/githooks/pre-commit27
-rw-r--r--scripts/tests/pre-commit-secret-scan.bats55
-rw-r--r--todo.org14
-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-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
18 files changed, 170 insertions, 567 deletions
diff --git a/languages/bash/githooks/pre-commit b/languages/bash/githooks/pre-commit
index 880d5cf..1520690 100755
--- a/languages/bash/githooks/pre-commit
+++ b/languages/bash/githooks/pre-commit
@@ -18,8 +18,18 @@ cd "$REPO_ROOT" || exit 1
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 — the scan searched an
+# empty string, found nothing, and the secret went in.
+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,8 +47,14 @@ if [ -n "$secret_hits" ]; then
fi
# --- 2. shellcheck on staged .sh / .bash files ---
-staged_sh="$(git diff --cached --name-only --diff-filter=AM \
- | grep -E '\.(sh|bash)$' || true)"
+# Same split as the secret scan above: a git failure must not read as "no files
+# staged", which would skip the language 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 check" >&2
+ exit 1
+fi
+
+staged_sh="$(printf '%s\n' "$staged_names" | grep -E '\.(sh|bash)$' || true)"
if [ -n "$staged_sh" ] && command -v shellcheck >/dev/null 2>&1; then
failed=""
diff --git a/languages/elisp/claude/hooks/validate-el.sh b/languages/elisp/claude/hooks/validate-el.sh
index d028789..870eefe 100755
--- a/languages/elisp/claude/hooks/validate-el.sh
+++ b/languages/elisp/claude/hooks/validate-el.sh
@@ -39,8 +39,6 @@ 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)
@@ -96,7 +94,7 @@ case "$f" in
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/languages/elisp/githooks/pre-commit b/languages/elisp/githooks/pre-commit
index 2c0cf0b..a87bedf 100755
--- a/languages/elisp/githooks/pre-commit
+++ b/languages/elisp/githooks/pre-commit
@@ -18,8 +18,18 @@ cd "$REPO_ROOT" || exit 1
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 — the scan searched an
+# empty string, found nothing, and the secret went in.
+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 +47,14 @@ if [ -n "$secret_hits" ]; then
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 files
+# staged", which would skip the language 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 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/test-pre-commit-hook.bats b/languages/elisp/tests/test-pre-commit-hook.bats
index 413c71d..413c71d 100644
--- a/working/hook-fail-open/test-pre-commit-hook.bats
+++ b/languages/elisp/tests/test-pre-commit-hook.bats
diff --git a/working/hook-fail-open/test-validate-el-hook.bats b/languages/elisp/tests/test-validate-el-hook.bats
index 43c3569..d4d6f23 100644
--- a/working/hook-fail-open/test-validate-el-hook.bats
+++ b/languages/elisp/tests/test-validate-el-hook.bats
@@ -18,7 +18,10 @@
# runs against the real tree.
setup() {
- HOOK="${BATS_TEST_DIRNAME}/../.claude/hooks/validate-el.sh"
+ # Bundle layout: the hook ships at claude/hooks/ here and installs to
+ # .claude/hooks/ in a consuming project. This test was written against the
+ # installed layout, so re-homing it needed the path adjusted.
+ 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"
diff --git a/languages/go/githooks/pre-commit b/languages/go/githooks/pre-commit
index 26b0db0..7d93949 100755
--- a/languages/go/githooks/pre-commit
+++ b/languages/go/githooks/pre-commit
@@ -18,8 +18,18 @@ cd "$REPO_ROOT" || exit 1
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 — the scan searched an
+# empty string, found nothing, and the secret went in.
+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)"
@@ -39,8 +49,14 @@ fi
# --- 2. gofmt check on staged .go files ---
# gofmt -l lists files that aren't gofmt-clean. Skip generated and vendored
# files the same way the rest of the toolchain does.
-staged_go="$(git diff --cached --name-only --diff-filter=AM \
- | grep '\.go$' \
+# Same split as the secret scan above: a git failure must not read as "no files
+# staged", which would skip the language 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 check" >&2
+ exit 1
+fi
+
+staged_go="$(printf '%s\n' "$staged_names" | grep '\.go$' \
| grep -vE '(^|/)vendor/' || true)"
if [ -n "$staged_go" ] && command -v gofmt >/dev/null 2>&1; then
diff --git a/languages/python/githooks/pre-commit b/languages/python/githooks/pre-commit
index 0a578b4..03536db 100755
--- a/languages/python/githooks/pre-commit
+++ b/languages/python/githooks/pre-commit
@@ -18,8 +18,18 @@ cd "$REPO_ROOT" || exit 1
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 — the scan searched an
+# empty string, found nothing, and the secret went in.
+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,8 +47,14 @@ if [ -n "$secret_hits" ]; then
fi
# --- 2. Syntax check on staged Python files ---
-staged_py="$(git diff --cached --name-only --diff-filter=AM \
- | grep -E '\.pyi?$' || true)"
+# Same split as the secret scan above: a git failure must not read as "no files
+# staged", which would skip the language 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 check" >&2
+ exit 1
+fi
+
+staged_py="$(printf '%s\n' "$staged_names" | grep -E '\.pyi?$' || true)"
if [ -n "$staged_py" ] && command -v python3 >/dev/null 2>&1; then
failed=""
diff --git a/languages/typescript/githooks/pre-commit b/languages/typescript/githooks/pre-commit
index 1628080..fd494d2 100755
--- a/languages/typescript/githooks/pre-commit
+++ b/languages/typescript/githooks/pre-commit
@@ -18,8 +18,18 @@ cd "$REPO_ROOT" || exit 1
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 — the scan searched an
+# empty string, found nothing, and the secret went in.
+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)"
@@ -42,10 +52,15 @@ fi
# it rejects valid TS (an `interface` reads as a syntax error) and accepts
# broken TS. Measured on node v26.4.0, 2026-07-23. tsc is the only correct
# parser for .ts; node is correct and much faster for .js.
-staged_js="$(git diff --cached --name-only --diff-filter=AM \
- | grep -E '\.(js|jsx|mjs|cjs)$' || true)"
-staged_ts="$(git diff --cached --name-only --diff-filter=AM \
- | grep -E '\.(ts|tsx|mts|cts)$' || true)"
+# Same split as the secret scan above: a git failure must not read as "no files
+# staged", which would skip the language 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 check" >&2
+ exit 1
+fi
+
+staged_js="$(printf '%s\n' "$staged_names" | grep -E '\.(js|jsx|mjs|cjs)$' || true)"
+staged_ts="$(printf '%s\n' "$staged_names" | grep -E '\.(ts|tsx|mts|cts)$' || true)"
failed=""
diff --git a/scripts/tests/pre-commit-secret-scan.bats b/scripts/tests/pre-commit-secret-scan.bats
index 013129e..4647556 100644
--- a/scripts/tests/pre-commit-secret-scan.bats
+++ b/scripts/tests/pre-commit-secret-scan.bats
@@ -14,7 +14,13 @@
# random base64 blob matched. Measured at ~6% of 100KB blobs; case-sensitive
# matching drops it to 0 across ~10MB.
-VARIANTS="elisp bash go"
+# Discovered, never enumerated. This list read "elisp bash go" while python and
+# typescript also shipped pre-commit hooks, so every "in every variant" test
+# below silently skipped two bundles from the day they were added — the same
+# enumerate-instead-of-discover failure these tests exist to catch. A new bundle
+# is now covered the moment it has a hook.
+VARIANTS="$(cd "${BATS_TEST_DIRNAME}/../../languages" && \
+ for d in */githooks/pre-commit; do [ -f "$d" ] && printf '%s ' "${d%%/*}"; done)"
setup() {
REPO="$(mktemp -d)"
@@ -142,3 +148,50 @@ run_hook() {
[ "$status" -eq 0 ] || { echo "$v blocked a removal: $output"; return 1; }
done
}
+
+# ---- Fail-closed: a broken git must never read as "nothing to scan" ----
+
+# Put a stub `git` ahead of the real one that fails only the staged-diff call
+# and delegates everything else, so just the pipeline under test breaks.
+break_git() {
+ mkdir -p "$REPO/bin"
+ cat > "$REPO/bin/git" <<'STUB'
+#!/usr/bin/env bash
+if [ "${1:-}" = "diff" ] && [ "${2:-}" = "--cached" ]; then
+ echo "simulated git failure" >&2
+ exit 128
+fi
+exec /usr/bin/git "$@"
+STUB
+ chmod +x "$REPO/bin/git"
+}
+
+@test "secret-scan: a broken git refuses rather than passing blind, in every variant" {
+ # The scan built its input as `git diff ... | grep ... || true`. With no
+ # pipefail, a git failure yielded an empty string, so the scan searched
+ # nothing, found nothing, and reported clean with a real secret staged.
+ # Found by .emacs.d in elisp 2026-07-24; all five variants had it.
+ stage 'aws_key = "AKIAIOSFODNN7EXAMPLE"'
+ break_git
+ for v in $VARIANTS; do
+ run env PATH="$REPO/bin:$PATH" bash \
+ "${BATS_TEST_DIRNAME}/../../languages/$v/githooks/pre-commit"
+ [ "$status" -ne 0 ] || {
+ echo "$v FAILED OPEN: exited 0 with a secret staged and git broken"
+ return 1
+ }
+ done
+}
+
+@test "secret-scan: the refusal says why, in every variant" {
+ stage 'aws_key = "AKIAIOSFODNN7EXAMPLE"'
+ break_git
+ for v in $VARIANTS; do
+ run env PATH="$REPO/bin:$PATH" bash \
+ "${BATS_TEST_DIRNAME}/../../languages/$v/githooks/pre-commit"
+ [[ "$output" == *"cannot read"* ]] || {
+ echo "$v refused without naming the cause: $output"
+ return 1
+ }
+ done
+}
diff --git a/todo.org b/todo.org
index bd5f16e..023f991 100644
--- a/todo.org
+++ b/todo.org
@@ -55,10 +55,15 @@ Grading: Minor severity (nothing broken now; the exposure is a future regression
Fix: add =claude-templates/bin/*= to the =check_hook= loop, the same shape =languages/*/githooks/*= already uses for extensionless files. A no-op today by design — it passes immediately — which is exactly what a guard should do.
-** VERIFY [#A] Parked: the secret-scan pre-commit fails open in ALL FIVE bundles (from .emacs.d)
+** DONE [#A] Applied: the secret-scan pre-commit fails open in ALL FIVE bundles (from .emacs.d)
:PROPERTIES:
:LAST_REVIEWED: 2026-07-24
:END:
+CLOSED: [2026-07-24 Fri]
+Applied 2026-07-24 across all five bundles (11 sites: the secret-scan input in each, plus each bundle's staged-file list, typescript having two). Verified on all five: refuses when the diff can't be read, still blocks a real staged secret, still passes a clean commit. Adopted .emacs.d's elisp bats suite (8 tests) and extended the repo-level cross-bundle suite with two fail-closed assertions.
+
+Separate defect found while wiring that up: the cross-bundle suite's VARIANTS list read "elisp bash go" while python and typescript also shipped hooks, so every "in every variant" assertion had silently skipped two bundles since I added them. VARIANTS is now discovered from the tree rather than enumerated — the same failure class the tests exist to catch.
+
What arrived: .emacs.d found that =languages/elisp/githooks/pre-commit= builds its scan input as =added_lines="$(git diff --cached -U0 ... | grep '^+' | grep -v '^+++' || true)"=. With no pipefail and =|| true= swallowing everything, any git failure yields an empty string, so the scan searches nothing, finds nothing, reports clean, and the commit proceeds with the secret in it. The staged-file list feeding the paren check has the identical hole.
*Verified independently, and it is worse than reported.* I reproduced the fail-open with a stub git that fails only the staged-diff call: exit 0 with an AWS-shaped key staged. Then I checked the other bundles, which the sender did not: *bash, go, python, and typescript all carry the same pattern and all fail open the same way.* Confirmed live on all five. Two of those (python, typescript) are hooks I wrote on 2026-07-24 by copying the bash one, so I propagated the defect while closing a different gap.
@@ -70,12 +75,14 @@ The sender's fix is correct and I verified all three axes on it: refuses to proc
Decision needed: the fix as sent covers elisp only. It should be applied to all five bundles, which is my scope expansion rather than the sender's proposal — hence a VERIFY rather than a silent apply. Also unresolved: where the two attached bats suites live, since the hooks are rulesets-owned and the tests currently sit in the consuming project.
Prepared: [[file:working/hook-fail-open/pre-commit.diff]], plus =test-pre-commit-hook.bats= (8 tests) in the same dir.
-Say "approve the hook fail-open fix" and it gets applied across all five bundles.
-** VERIFY [#B] Parked: remove the validate-el auto-test cap (from .emacs.d, Craig's call)
+** DONE [#B] Applied: remove the validate-el auto-test cap (from .emacs.d, Craig's call)
:PROPERTIES:
:LAST_REVIEWED: 2026-07-24
:END:
+CLOSED: [2026-07-24 Fri]
+Applied 2026-07-24. Cap and the unreachable notice helper removed; the gate is now count -ge 1. Adopted the rewritten bats suite (6 tests), with its hook path corrected from the installed .claude/hooks/ layout to the bundle's claude/hooks/ — the re-homing mismatch the sender flagged.
+
What arrived: a superseding handoff. The first proposed a loud notice when the test count exceeds =MAX_AUTO_TEST_FILES=20= (above the cap the block was skipped, nothing printed, exit 0 — indistinguishable from a pass). Craig chose in the .emacs.d session to remove the cap instead.
The reasoning is the valuable part. Measured, a whole family runs in under two seconds (calendar-sync 63 files / 633 tests / 0.9s), so the cap bought nothing. Worse, it was *concealing* a real cross-test pollution bug: calendar-sync exits 1 when its 63 files run in one process, because one test marks a calendar as syncing and never resets it, and a sibling file's test then hits the stale guard. Invisible from both directions — =make test= runs each file in its own Emacs, and the hook skipped the family for being over the cap.
@@ -85,7 +92,6 @@ Verified the superseding file drops the cap entirely (zero references, gate is n
Since Craig already made this call, the remaining decision is only adoption scope: removing the cap means other consuming projects run every stem-matched test file per edit, and one may go red on first use by surfacing pollution that per-file runs hid. The sender flags that as intended.
Prepared: [[file:working/hook-fail-open/validate-el.diff]], plus =test-validate-el-hook.bats= (6 tests, two of which stage a deliberately failing test so a quiet pass proves the run happened).
-Say "approve the cap removal" and it gets applied.
** TODO [#B] cj-remove-block still can delete the WRONG cj block :bug:
:PROPERTIES:
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-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