forked from bchanot/claude
chore(memory): BDR-112 + LRN-194..196 + journal — feat manual-push-guard run B
This commit is contained in:
@@ -1371,3 +1371,10 @@ Branch feature/user-writing-web-rules, UNMERGED (human gate).
|
||||
- **Alternatives rejected**: new `gitflow.mode auto|manual` (duplicates autopush); tty-only lock on `finish` (user wants local merges); `-D` after ancestor gate (statically denied form, reviewers' red flag); fail-closed in lib only (hooks would still push → inconsistent).
|
||||
- **Gates**: 3 lenses CONCERNS(1/2/2) + confirmation FATAL(4) → r2 fixes (T22j containing base, `-u` fixture, T18n before T18l); feater ×2; GATE 0 7/8 (AC6 = env red); verifier ECARTS(1) = AC6 only; security PASS ×2 (1 MEDIUM fail-open → run B).
|
||||
- **Refs**: contract `.claude/tasks/contracts/2026-10-06-manual-push-mode-1632.md`, plan `.claude/tasks/plans/2026-10-06-manual-push-mode-1632.md`, commit 2fc8830 (feature/manual-push-mode, UNMERGED). Extends [[BDR-095]] (c); [[LRN-161]], [[LRN-191]], [[LRN-192]], [[LRN-193]], [[BLK-022]].
|
||||
|
||||
## BDR-112 — push-guard: text-only PreToolUse deny of Claude's `git push` in manual-push mode, fail closed [accepted] (2026-10-07)
|
||||
- **Decision**: run B of [[BDR-111]]. `hooks/push-guard.sh` (own PreToolUse group `Bash|Monitor`, timeout 10) reads `tool_input.command` + `cwd`; folds `\`-newline in bash (BSD sed unsafe, [[LRN-195]]); three ERE matches on `/usr/bin/grep`: STRICT (git + `-opt [arg]`* + push|send-pack, boundaries `[^[:alnum:]_.-]` / `[^[:alnum:]_-]`), LOOSE on quote-stripped text (any later ` push` word in the same simple command), ALIAS (`alias.x=…push`). Candidate dirs = cwd + literal `-C`/`cd`/`pushd` tokens (quotes stripped, never eval/expand), dedup `sort -u`, >20 distinct → deny before any fork. Mode per dir via `git config --bool gitflow.autopush` rc: 0 → value, 1 → auto, else → deny; NO work-tree gate (global key = work-machine deployment). Deny = JSON `permissionDecision=deny` exit 0, reason carries `! <cmd>`; EXIT trap emits static deny + `exit 0` once a push is detected and nothing decided; jq missing → stderr + allow (sibling policy). User gated: fail-closed cases (cap, unenterable dir, git failure) fire in auto mode too. settings.json: 18 deny entries on WRITE forms of `gitflow.*` (any `git … config` spelling, remove/rename-section, `-c`, config env overrides, Edit/Write of git config files); soft_deny "pushing in manual-push mode, any form, no per-turn clearance"; routing-around hard_deny names hook refusals; banner `🔒 push : manual (autopush=false) — ! git push` (`%-46s`, bytes).
|
||||
- **Why**: `ask` inert under auto ([[LRN-155]]); `hooks/guard-bash.sh` withheld ([[BLK-022]]) → one narrow rule instead. Trailing ` *` glob matches end-of-string ([[LRN-194]]) → no infix rule spares the bare read → Claude loses `git config … gitflow.autopush`; hooks/lib keep it; run C gets `gitflow.sh push-mode`. Text-only guard cannot see scripts/aliases → soft_deny is the declared backstop.
|
||||
- **Alternatives rejected**: no-jq fallback (greps whole payload, denies in auto, untestable without shim; jq hard dep); `-C` dir unresolvable → allow (fail-open; now skipped, cwd still checked); `rev-parse` work-tree gate (drops the global key); `--default true` read (hides git failure); narrowing deny to spare the read (impossible with end-matching globs).
|
||||
- **Gates**: 3 lenses CONCERNS(2)/CONCERNS(5)/FATAL(7) + confirmation FATAL(8) → r2 (BSD sed, oracle naming denied tokens, read loss); feater ×3 (58 → 61 → 71 checks); GATE 0 MET ×3; verifier CONFORME, then ECARTS(1) on hardening closed by gated clarification; security PASS ×2 (3 MEDIUM closed: 20k-token flood denied in 0.15 s, git absent → deny, `bash -c 'cd … && git push'` extracted; residuals → run D).
|
||||
- **Refs**: contract `.claude/tasks/contracts/2026-10-07-manual-push-guard-1003.md`, plan `.claude/tasks/plans/2026-10-07-manual-push-guard-1003.md`, commits a2ac018 + 6468eda (feature/manual-push-mode, UNMERGED). Links [[BDR-095]], [[BDR-100]], [[LRN-069]], [[LRN-196]]. Open: user probe `! git push --dry-run` under autopush=false (bang commands assumed hook-free).
|
||||
|
||||
@@ -569,3 +569,7 @@ rules:
|
||||
- /doc global audit 2026-10-06 (opus): 45 items, 6 docs. Applied 34 (24 AUTO + 9 HUMAN drafts + clone URL → Gitea): README components/slash/flow, Makefile help (11 profiles), USAGE /health→make doctor + GSD 3.0.0, ARCHITECTURE layout, MIGRATION retitled + "Upgrading to 2.0.0", SETTINGS package-install guard, CHANGELOG SemVer + default model + upgrade pointer → c6fb2e4. 10 deferred logged in TODO (LICENSE, Known-residual vs release, README restructure, USAGE narrative, templates/settings.json ask inert).
|
||||
- Release 2.0.0 cut (user go x3: release, tag push, MIT): bugfix merged 370f35a; develop merged into release/2.0.0 (b47bba7, CHANGELOG conflict resolved: upgrade pointer under [2.0.0]); suite 46/46 green on release; 9ef66e2 MIT LICENSE + README License + Linux residual reworded; gitflow finish by release-executor (BLK-018 did not fire) -> main 4093cca, tag v2.0.0 pushed on user go. Open after release: Linux make test (TODO), make plugin + .env on this machine (BLK-027).
|
||||
- /feat manual-push-mode run A (user: work machine, same flow, never push alone): `gitflow.autopush false` = manual-push mode end to end. Plan challenged 3 lenses + 1 confirm → 2 MAJOR (`-d` re-arms on lagging upstream LRN-161; /close STEP 5C pushes develop) + 3 BLOCKER in r2 (T22j regress, develop untracked in fixture, T18l/T18n order) all closed by named changes. feater ×2 (gaps: pipefail flake `git log | grep -q`, 9 SC2034 suppressions removed), GATE 0 7/8, verifier ECARTS(1) = AC6 env red only (design-tool-gate, 21st CLI present, same on develop fa67664), security PASS ×2. Commit 2fc8830 on feature/manual-push-mode, UNMERGED. Runs B (push-guard hook, settings deny widening, banner) + C (skills that push) queued in TODO; do NOT enable manual mode at work before B+C.
|
||||
|
||||
## 2026-10-07
|
||||
- /feat manual-push-mode run B (user: "enchaine"): `hooks/push-guard.sh` PreToolUse denies Claude's `git push` when autopush false/unparseable/unreadable in cwd or literal -C/cd dirs (global config counts). Challenge: 3 lenses + robustness confirm FATAL(8) → BSD sed `N` fold empty on 1 line (hook dead), AC3 oracle naming denied tokens, glob trailing ` *` matches end → bare read lost, run C needs lib verb. feater ×3 (impl 58, no-jq test 61, hardening 71: cap 20 dirs, git rc → deny, quoted cd). GATE 0 MET ×3; verifier CONFORME then ECARTS(1) → user gated fail-closed also in auto for pathological commands; security PASS ×2 (3 MEDIUM closed, 2 residual → run D). Commits a2ac018 + 6468eda on feature/manual-push-mode, UNMERGED. settings.json live: 18 deny entries, soft_deny, Bash|Monitor hook group. User probe pending: `! git push --dry-run` bypasses hooks?
|
||||
|
||||
|
||||
@@ -1712,3 +1712,15 @@ Rule: when editing a doctrine file under structure locks, grep the test's lock s
|
||||
## LRN-193 — A revised plan gets a fresh challenger, not a re-read: r2 found 3 BLOCKERs inside r1's fixes
|
||||
- **Context**: manual-push-mode plan. r1 (3 lenses) → 2 MAJOR, I rewrote 5 checklist items. Confirmation pass (1 fresh correctness challenger on the REVISED file) → FATAL(4): my `--unset-upstream` fix broke T22j; my T18l fixture never set develop's upstream (`push` without `-u`, init creates develop untracked); my T18n/T18l order made offline silence vacuous. All three were in text I had just written and re-read.
|
||||
- **Apply**: `challenge-plan.md` "re-challenge once if materially changed" is load-bearing, never skip it to save a dispatch. Brief the confirmation challenger on the NEW mechanics explicitly (state machine of new tests, fixture preconditions, ordering). Fixes to tests need the same fixture trace as the code (`-u`, upstream, what an earlier test leaves behind).
|
||||
|
||||
## LRN-194 — Permission globs: trailing ` *` matches end-of-string; a denied token poisons every command that names it
|
||||
- **Context**: push-guard deny widening. Planned `Bash(git *config *gitflow.* *)` to deny writes (key + value) and spare the bare read for run C. Evidence: `git config --local core.hooksPath` (no value) is denied by `Bash(git config --local core.hooksPath *)` → ` *` also matches end. Second effect: once `Bash(*GIT_CONFIG_COUNT*)` style rules landed (settings.json symlinked = live), a contract CHECK, a grep and a commit message naming the tokens would all be denied — including the oracle meant to verify the rules.
|
||||
- **Apply**: (a) an infix/suffix glob cannot carve out a read of a denied key → give consumers a sanctioned reader (lib verb) instead; (b) a leading-`*` deny on a token makes the token unspeakable in command text → assertions about it live in test FILES (`make test`), never in CHECK commands, grep one-liners or commit subjects; (c) simplify: `Bash(git *config *gitflow.*)` already covers value writes, `--unset`, `--bool` forms. Links [[BDR-112]], [[BDR-100]].
|
||||
|
||||
## LRN-195 — BSD sed: `N` on the last line quits without printing → the `:a;N;$!ba` fold returns EMPTY on single-line input
|
||||
- **Context**: push-guard plan folded `\`-newline with `sed -e ':a' -e 'N' -e '$!ba' -e 's/\\\n[[:space:]]*/ /g'` (the [[LRN-190]] idiom, written on GNU). `/usr/bin/sed` on macOS is BSD: `printf 'git push' | sed …` prints NOTHING. Every single-line command would have read as empty → guard dead in production, while a 2-line test passed. Caught by the confirmation challenger, not by tests.
|
||||
- **Apply**: fold in bash (`one=${cmd//$'\\\n'/ }; one=${one//$'\n'/ }`) or `sed -e ':a' -e '$!N' -e '$!ba'`. Add a single-line positive control to any multi-line normaliser test. [[BDR-110]] census can't catch it (structural, not textual). Links [[LRN-190]], [[BDR-112]].
|
||||
|
||||
## LRN-196 — A fail-closed Claude Code hook: trap must `exit 0`, cap attacker-sized loops, read git's rc not its value
|
||||
- **Context**: push-guard hardening (security gate, 3 MEDIUM). (1) EXIT trap printed the static deny but kept the non-zero rc → Claude Code parses hook JSON only on exit 0 → deny ignored = allow. (2) Each literal `cd`/`-C` token cost a subshell + 3 git execs: 600 tokens = 12 s > 10 s hook timeout → timeout = non-blocking = allow. (3) `git config --bool --default true` returns empty on git absent / old git / unreadable dir → read as "auto" → allow.
|
||||
- **Apply**: `trap '… ; exit 0' EXIT`; deny path `out=$(jq …) || out=$STATIC; printf '%s' "$out"`; dedup (`sort -u`) + hard cap on command-controlled token counts, deny above the cap BEFORE any fork; distinguish `git config` rc 0/1/other (value / unset / failure → deny); record "decided" only after ≥1 clean evaluation. Lock each with a test (shim PATH without a tool, 25-token flood, chmod 000 dir with SKIP path). Measure the flood after the fix (20 000 tokens → 0.15 s). Links [[BDR-112]], [[BDR-087]], [[LRN-160]].
|
||||
|
||||
Reference in New Issue
Block a user