forked from bchanot/claude
Merge feature/config-protection-hook into develop
This commit is contained in:
@@ -776,3 +776,15 @@ rules:
|
|||||||
- **Reference**: read-only clone (scratchpad), 4 parallel analyzer agents +
|
- **Reference**: read-only clone (scratchpad), 4 parallel analyzer agents +
|
||||||
eval-harness spike, this session. No branch on ECC, no import. See [[BDR-045]]
|
eval-harness spike, this session. No branch on ECC, no import. See [[BDR-045]]
|
||||||
(chore/ aiguillage), [[BDR-009]] (caveman registries).
|
(chore/ aiguillage), [[BDR-009]] (caveman registries).
|
||||||
|
- **Corroboration 2026-07-03** (Opus 4.8 re-audit; repo UNCHANGED — HEAD 81af407
|
||||||
|
2026-06-29, 2232 commits identical, zero commits since 01/07): 6 parallel analyzer
|
||||||
|
agents re-verified every BDR-047 fact w/ fresh file:line. rules/ inert (paths: 0
|
||||||
|
consumers, rules/README.md:333 "cannot distribute rules automatically"); contexts/
|
||||||
|
overwrite (the-longform-guide.md:68-74 `--system-prompt`); eval-harness no runner
|
||||||
|
(/eval absent; gan-harness.sh + skill-improvement/evaluate.js exist but hors-scope,
|
||||||
|
deliver NEITHER pass@k nor model-upgrade battery); memory auto-capture conflicts
|
||||||
|
approve-first (continuous-learning-v2 observer-loop.sh:160-164 "Do NOT ask for
|
||||||
|
permission"); distribution = product scaffolding, N/A. ZERO factual divergence.
|
||||||
|
ONE scope gap: BDR-047 never opened hooks/ — ECC's only WIRED subsystem. Fruit:
|
||||||
|
config-protection hook (own idiom, NOT ECC import), shipped
|
||||||
|
feature/config-protection-hook. Lesson holds + refined by [[LRN-090]].
|
||||||
|
|||||||
@@ -308,3 +308,7 @@ rules:
|
|||||||
- Same branch, 3 doctor false-warns fixed ([[LRN-047]] corrob — a doctor that cries false is ignored): cargo "(RTK unavailable)" → optional info (RTK prebuilt, detect_rtk); check_symlink passes children of dir-level symlinks (hooks/session-start.sh); gstack counts 34 per-skill symlinks not a mythical skills/gstack link (link.sh removes it); token budget vs 200k context window not bogus 11k "session budget" → killed false "92% CRITICAL" (measured ~11.4k [[LRN-088]]; 200k confirmed by user — 1M pin revoked at audit #7, calibrate on default not the exceptional session).
|
- Same branch, 3 doctor false-warns fixed ([[LRN-047]] corrob — a doctor that cries false is ignored): cargo "(RTK unavailable)" → optional info (RTK prebuilt, detect_rtk); check_symlink passes children of dir-level symlinks (hooks/session-start.sh); gstack counts 34 per-skill symlinks not a mythical skills/gstack link (link.sh removes it); token budget vs 200k context window not bogus 11k "session budget" → killed false "92% CRITICAL" (measured ~11.4k [[LRN-088]]; 200k confirmed by user — 1M pin revoked at audit #7, calibrate on default not the exceptional session).
|
||||||
- Suites green: gitflow 71/71 (+7), deterministic 13, doc-commit 32, doc-shape 19, reconcile 20, deploy-commit 13, release-candidate 5/5 tag-mode. doctor: 0 false-warn (1 legit survivor = gstack tracks branch=main advisory). shellcheck clean. T12 named to dodge collision with reconcile's own T6c (darwin path, audit #3).
|
- Suites green: gitflow 71/71 (+7), deterministic 13, doc-commit 32, doc-shape 19, reconcile 20, deploy-commit 13, release-candidate 5/5 tag-mode. doctor: 0 false-warn (1 legit survivor = gstack tracks branch=main advisory). shellcheck clean. T12 named to dodge collision with reconcile's own T6c (darwin path, audit #3).
|
||||||
- 3 atomic commits (fix gitflow / fix doctor / docs changelog Unreleased) + memory. finish bugfix→develop on GO; user pushes develop.
|
- 3 atomic commits (fix gitflow / fix doctor / docs changelog Unreleased) + memory. finish bugfix→develop on GO; user pushes develop.
|
||||||
|
- ECC 2nd-look (Opus 4.8, 6 agents, repo unchanged since 01/07): all [[BDR-047]] facts corroborated w/ file:line, zero divergence. Scope gap = hooks/ (only wired subsystem) unaudited 01/07 → [[LRN-090]] wired > declarative.
|
||||||
|
- Shipped config-protection hook (feature/config-protection-hook): PreToolUse blocks Edit/Write to quality-gate files (settings/gitflow/.githooks/doctor/hooks-self/lib-tests/lint). One-shot sentinel .claude/.config-edit-ok (non-empty reason, logged+consumed) — NOT env-var (launch-time = set-and-forget = garde mort). Own idiom, not ECC import. shellcheck clean, test 20/20.
|
||||||
|
- Live dogfood: hook went active mid-session via symlinked settings (link.sh); v1 (no self-guard) let its OWN edit through → v2 added hooks/*.sh + lib/tests/* self-guard, then blocked the test-file edit; recovered via sentinel. User's self-guard requirement vindicated.
|
||||||
|
- Next: #2 design-toolchain trigger fix (residual false-fires post-ed2408e, 5× this session).
|
||||||
|
|||||||
@@ -965,3 +965,9 @@ rules:
|
|||||||
- **context**: audit 2026-07-02, `lib/gitflow.sh:257` `finish) gitflow_finish "$@"` passed args the function never consulted. Surfaced when a finish "for" one branch merged another (LOT3). [[BLK-015]].
|
- **context**: audit 2026-07-02, `lib/gitflow.sh:257` `finish) gitflow_finish "$@"` passed args the function never consulted. Surfaced when a finish "for" one branch merged another (LOT3). [[BLK-015]].
|
||||||
- **future application**: any wrapper/dispatcher forwarding args to a callee that resolves its target from ambient state — either (a) make the callee USE the args as the target, or (b) if the ambient-state contract is deliberate, treat passed args as an ASSERTION and refuse loudly when they disagree with the state. Never let forwarded args be silently dropped: silent-drop = the caller believes they steered, the callee ignored them. Sibling of "presence-flag ≠ capability" [[LRN-087]] — both = a visible signal lying about the real behavior.
|
- **future application**: any wrapper/dispatcher forwarding args to a callee that resolves its target from ambient state — either (a) make the callee USE the args as the target, or (b) if the ambient-state contract is deliberate, treat passed args as an ASSERTION and refuse loudly when they disagree with the state. Never let forwarded args be silently dropped: silent-drop = the caller believes they steered, the callee ignored them. Sibling of "presence-flag ≠ capability" [[LRN-087]] — both = a visible signal lying about the real behavior.
|
||||||
- **Reference**: `lib/gitflow.sh` gitflow_finish arg-guard, `lib/gitflow-test.sh` T12. [[BLK-015]].
|
- **Reference**: `lib/gitflow.sh` gitflow_finish arg-guard, `lib/gitflow-test.sh` T12. [[BLK-015]].
|
||||||
|
|
||||||
|
## LRN-090 — external-repo audit: open WIRED subsystems before declarative
|
||||||
|
- **pattern**: auditing external config/framework repo for transferable value → rank subsystems WIRED (executable: hooks/, runners, dispatchers) vs DECLARATIVE (docs, rules/, aspirational frontmatter). Wired > declarative: declarative often inert (ECC rules/ `paths:` = 0 consumers; eval-harness = SKILL.md, no runner — "belle méthodo / vaporware"); wired = a real mechanism worth adapting.
|
||||||
|
- **context**: ECC 2nd-look 2026-07-03 (Opus 4.8, 6 agents, repo unchanged since 01/07). [[BDR-047]] audit (01/07) inventoried the declarative surface + concluded zero import — right on facts, but hooks/ (ECC's only live subsystem) was OUT of scope and held the sole real adaptation → config-protection PreToolUse guard.
|
||||||
|
- **future application**: next external-repo value audit → enumerate hooks/, scripts/, runners FIRST; treat rules/docs/SKILL.md as claims to verify ("is it wired?"), not value. Described capability ≠ wired capability.
|
||||||
|
- **cousin**: [[LRN-087]] presence-flag ≠ capability; [[LRN-089]] forwarded-args silently dropped — same family: a visible signal (a file, a flag, a `paths:`) lying about real behavior.
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
|
## 2026-07-03 — config-protection hook (feature/config-protection-hook)
|
||||||
|
Goal: PreToolUse hook blocks Edit/Write to this config's quality-gate files
|
||||||
|
(guardrails an agent must not weaken to make an error pass). Adaptation from ECC
|
||||||
|
second-look (BDR-047 corrob, Opus 4.8 re-audit) — MY idiom (~15-line bash), NOT
|
||||||
|
ECC's Node dispatcher. Extends config's own doctrine ("backstops déterministes
|
||||||
|
car l'advisory s'oublie"). Guarded: settings.json (+ .claude/settings*.json),
|
||||||
|
lib/gitflow.sh, .githooks/*, doctor.sh, lint configs (preemptive, absent today).
|
||||||
|
Bypass: CONFIG_EDIT_OK="reason" (logged). Mid-session env caveat flagged at gate.
|
||||||
|
|
||||||
|
- [x] hooks/config-protection.sh — case-match guarded path, exit 2 else 0; fail-open
|
||||||
|
- [x] Guarded: settings.json(+.claude/settings*), lib/gitflow.sh, .githooks/*, doctor.sh, hooks/*.sh (self-guard), lib/tests/* (T6c/LRN-077), lint (preemptive)
|
||||||
|
- [x] Bypass: one-shot sentinel .claude/.config-edit-ok (non-empty reason, logged+consumed) — NOT env-var (launch-time env = set-and-forget = garde mort)
|
||||||
|
- [x] lib/tests/config-protection.test.sh — block/allow/self-guard/near-miss/fail-open/sentinel-one-shot/empty-refuse (17 checks)
|
||||||
|
- [x] settings.json — register PreToolUse matcher Edit|Write|MultiEdit -> hook
|
||||||
|
- [x] Verify — shellcheck clean + 17/17 PASS + bash -n + bootstrap-safe (hook fires on Edit/Write only, not shell cp/ln)
|
||||||
|
- [x] GATE passed — guarded list +2 (hooks/, tests/), sentinel over env-var
|
||||||
|
- [ ] Capitalize (BDR-047 corrob + LRN-090 câblé>déclaratif) + finish this branch only
|
||||||
|
|
||||||
## 2026-06-23 — install self-sufficient + gstack on-demand par profil
|
## 2026-06-23 — install self-sufficient + gstack on-demand par profil
|
||||||
Goal: `make install`/`make plugin`/`make update` installent TOUT sans étape
|
Goal: `make install`/`make plugin`/`make update` installent TOUT sans étape
|
||||||
manuelle. Plus le profil-driven gstack on-demand (option 1 user : gstack OFF
|
manuelle. Plus le profil-driven gstack on-demand (option 1 user : gstack OFF
|
||||||
|
|||||||
Executable
+65
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# config-protection.sh
|
||||||
|
#
|
||||||
|
# PreToolUse hook (Edit|Write|MultiEdit). Blocks edits to this config's
|
||||||
|
# quality-gate files — the guardrails an agent must not silently weaken to make
|
||||||
|
# an error "pass" (permission/hook registry, gitflow enforcement, the git
|
||||||
|
# pre-commit guard, the hooks themselves, the test suite, the health diagnostic,
|
||||||
|
# lint config). Exit 2 blocks the tool call and feeds the message back to the
|
||||||
|
# model (Claude Code PreToolUse contract).
|
||||||
|
#
|
||||||
|
# It fires only on the model's Edit/Write tool calls — never on shell-level file
|
||||||
|
# ops (the cp/ln in install.sh, link.sh), so bootstrap/deploy is unaffected.
|
||||||
|
#
|
||||||
|
# One-shot escape hatch: create .claude/.config-edit-ok (CWD-relative) with a
|
||||||
|
# NON-EMPTY reason inside; the hook logs the reason, consumes (rm) the sentinel,
|
||||||
|
# and allows that single edit. It never persists — a lingering sentinel would be
|
||||||
|
# a footgun. Discipline, per CLAUDE.md "Root causes only. No temp fixes.": fix
|
||||||
|
# the code, don't loosen the gate. Fails OPEN (exit 0) on parse failure so it can
|
||||||
|
# never wedge editing.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
log="${HOME}/.claude/logs/config-protection.log"
|
||||||
|
sentinel="${PWD}/.claude/.config-edit-ok"
|
||||||
|
|
||||||
|
input="$(cat)"
|
||||||
|
path="$(printf '%s' "$input" \
|
||||||
|
| python3 -c 'import sys, json; print(json.load(sys.stdin).get("tool_input", {}).get("file_path", ""))' \
|
||||||
|
2>/dev/null || true)"
|
||||||
|
[ -z "$path" ] && exit 0
|
||||||
|
|
||||||
|
# Guardrail files, matched by path suffix (covers both the repo source and the
|
||||||
|
# deployed ~/.claude copy). Precise: lib/gitflow.sh only, not gitflow-migrate.sh.
|
||||||
|
case "$path" in
|
||||||
|
*/.claude/settings.json|*/.claude/settings.local.json|*/claude/settings.json) ;;
|
||||||
|
*/lib/gitflow.sh|*/.githooks/*|*/doctor.sh) ;;
|
||||||
|
*/hooks/*.sh|*/lib/tests/*) ;;
|
||||||
|
*/.shellcheckrc|*/.markdownlint.json|*/.editorconfig) ;;
|
||||||
|
*) exit 0 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# One-shot sentinel bypass: non-empty reason required; consumed on sight.
|
||||||
|
if [ -f "$sentinel" ]; then
|
||||||
|
reason="$(head -c 500 "$sentinel" 2>/dev/null | tr '\n\r\t' ' ' || true)"
|
||||||
|
rm -f "$sentinel"
|
||||||
|
if printf '%s' "$reason" | grep -q '[^[:space:]]'; then
|
||||||
|
mkdir -p "$(dirname "$log")"
|
||||||
|
printf '%s\tBYPASS\t%s\treason=%s\n' "$(date -Iseconds)" "$path" "$reason" >> "$log"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
printf '%s\n' "[config-protection] .claude/.config-edit-ok had an EMPTY reason -> refused (sentinel consumed). Recreate it with a non-empty reason." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat >&2 <<EOF
|
||||||
|
[config-protection] BLOCKED edit to a quality-gate file:
|
||||||
|
$path
|
||||||
|
This is a guardrail (permission/hook registry, gitflow enforcement, git
|
||||||
|
pre-commit guard, a hook, the test suite, health diagnostic, or lint config).
|
||||||
|
Don't weaken the gate to make an error pass — fix the root cause instead
|
||||||
|
(CLAUDE.md: "Root causes only. No temp fixes."). To make one intended edit,
|
||||||
|
create .claude/.config-edit-ok with a non-empty reason; it is logged and
|
||||||
|
consumed (one-shot).
|
||||||
|
EOF
|
||||||
|
exit 2
|
||||||
Executable
+57
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# lib/tests/config-protection.test.sh
|
||||||
|
set -u
|
||||||
|
H="$(cd "$(dirname "$0")/../.." && pwd)/hooks/config-protection.sh"
|
||||||
|
pass=0; fail=0
|
||||||
|
check() { if [ "$2" = "$3" ]; then pass=$((pass+1)); else fail=$((fail+1));
|
||||||
|
printf 'FAIL %s: got[%s] want[%s]\n' "$1" "$2" "$3"; fi; }
|
||||||
|
# Run hook for a file_path with NO sentinel present (CWD = a clean temp dir).
|
||||||
|
run() { local c r; c="$(mktemp -d)"; ( cd "$c" && printf \
|
||||||
|
'{"tool_name":"Edit","tool_input":{"file_path":"%s"}}' "$1" | bash "$H" ) \
|
||||||
|
>/dev/null 2>&1; r=$?; rm -rf "$c"; return "$r"; }
|
||||||
|
|
||||||
|
# --- Guarded quality-gate files -> blocked (exit 2) ---
|
||||||
|
run "/home/u/Documents/claude/lib/gitflow.sh"; check T1-gitflow "$?" 2
|
||||||
|
run "/home/u/.claude/settings.json"; check T2-live-settings "$?" 2
|
||||||
|
run "/home/u/Documents/claude/.claude/settings.local.json"; check T3-local-settings "$?" 2
|
||||||
|
run "/home/u/Documents/claude/settings.json"; check T4-root-settings "$?" 2
|
||||||
|
run "/home/u/Documents/claude/.githooks/pre-commit"; check T5-githook "$?" 2
|
||||||
|
run "/home/u/Documents/claude/doctor.sh"; check T6-doctor "$?" 2
|
||||||
|
run "/home/u/Documents/claude/.shellcheckrc"; check T7-shellcheckrc "$?" 2
|
||||||
|
# self-guard: the hook itself, other hooks, and the test suite are guarded
|
||||||
|
run "/home/u/Documents/claude/hooks/config-protection.sh"; check T8-self-guard "$?" 2
|
||||||
|
run "/home/u/.claude/hooks/session-start.sh"; check T9-deployed-hook "$?" 2
|
||||||
|
run "/home/u/Documents/claude/lib/tests/config-protection.test.sh"; check T10-tests-guarded "$?" 2
|
||||||
|
|
||||||
|
# --- Non-guarded -> allowed (exit 0) ---
|
||||||
|
run "/home/u/Documents/claude/lib/gitflow-migrate.sh"; check T11-near-miss "$?" 0
|
||||||
|
run "/home/u/project/src/app.js"; check T12-code "$?" 0
|
||||||
|
run "/home/u/project/settings.json"; check T13-foreign-settings "$?" 0
|
||||||
|
|
||||||
|
# --- Fail-open on malformed input (no file_path) -> allowed ---
|
||||||
|
c="$(mktemp -d)"; ( cd "$c" && printf '{}' | bash "$H" ) >/dev/null 2>&1
|
||||||
|
check T14-fail-open "$?" 0; rm -rf "$c"
|
||||||
|
|
||||||
|
# --- Sentinel one-shot: non-empty reason -> allow + log + consume; 2nd edit blocked ---
|
||||||
|
tmp="$(mktemp -d)"; mkdir -p "$tmp/.claude"
|
||||||
|
printf 'fixing eslint false-positive' > "$tmp/.claude/.config-edit-ok"
|
||||||
|
( cd "$tmp" && printf '{"tool_name":"Edit","tool_input":{"file_path":"/x/doctor.sh"}}' \
|
||||||
|
| HOME="$tmp" bash "$H" ) >/dev/null 2>&1
|
||||||
|
check T15-sentinel-allow "$?" 0
|
||||||
|
check T15-consumed "$([ -e "$tmp/.claude/.config-edit-ok" ] && echo present || echo gone)" gone
|
||||||
|
check T15-logged "$(grep -c 'BYPASS.*doctor.sh.*fixing eslint' \
|
||||||
|
"$tmp/.claude/logs/config-protection.log" 2>/dev/null)" 1
|
||||||
|
( cd "$tmp" && printf '{"tool_name":"Edit","tool_input":{"file_path":"/x/doctor.sh"}}' \
|
||||||
|
| HOME="$tmp" bash "$H" ) >/dev/null 2>&1
|
||||||
|
check T16-second-blocked "$?" 2
|
||||||
|
rm -rf "$tmp"
|
||||||
|
|
||||||
|
# --- Sentinel with EMPTY reason -> refused + consumed ---
|
||||||
|
tmp="$(mktemp -d)"; mkdir -p "$tmp/.claude"; : > "$tmp/.claude/.config-edit-ok"
|
||||||
|
( cd "$tmp" && printf '{"tool_name":"Edit","tool_input":{"file_path":"/x/doctor.sh"}}' \
|
||||||
|
| HOME="$tmp" bash "$H" ) >/dev/null 2>&1
|
||||||
|
check T17-empty-refused "$?" 2
|
||||||
|
check T17-consumed "$([ -e "$tmp/.claude/.config-edit-ok" ] && echo present || echo gone)" gone
|
||||||
|
rm -rf "$tmp"
|
||||||
|
|
||||||
|
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
|
||||||
@@ -245,6 +245,16 @@
|
|||||||
"command": "bash ~/.claude/hooks/rtk-rewrite.sh"
|
"command": "bash ~/.claude/hooks/rtk-rewrite.sh"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "Edit|Write|MultiEdit",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "bash ~/.claude/hooks/config-protection.sh",
|
||||||
|
"timeout": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"UserPromptSubmit": [
|
"UserPromptSubmit": [
|
||||||
|
|||||||
Reference in New Issue
Block a user