chore(memory): BDR-113 + LRN-197..199 + journal — feat manual-push-mode run C
This commit is contained in:
@@ -1378,3 +1378,10 @@ Branch feature/user-writing-web-rules, UNMERGED (human gate).
|
||||
- **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).
|
||||
|
||||
## BDR-113 — Skills never push; truth from `rev-list` facts, mode verb only words the reason [accepted] (2026-10-07)
|
||||
- **Decision**: run C of [[BDR-111]]. New lib verb `gitflow.sh push-mode` (stdout `auto|manual|invalid`, rc 0, raw value on stderr, printable ≤64 chars; ignores GITFLOW_NO_PUSH) = the one reader skills may call (bare `git config … gitflow.*` denied, [[BDR-112]]). Skills push NOTHING on their own except the release tag in auto mode on explicit go. Every "on origin / not pushed" line comes from `git rev-list --count origin/<br>..<br>` (or `<br> --not --remotes=origin` for the tour) read AFTER the action, in its own Bash call; the verb only words the reason (manual vs push FAILED vs invalid). Removed as redundant since BDR-095: `/close` STEP 5C `git push origin develop` (finish pushes develop itself, mode-aware since run A) and `/client-handover`'s "Push to origin now?" question + push block (hooks had pushed; push-guard denies in manual). User hints are complete `! git …` commands (`-u`, `--atomic origin main develop vX`, `-C <abs project>`), branch name allowlisted `^[A-Za-z0-9._/][A-Za-z0-9._/-]*$` before any interpolation. `/release-candidate` manual/invalid or any count ≠ 0 → one user command, STOP, no question; tag gate kept for auto with both counts 0. `/tour`: one `-C` fact per project after the report commit, suffix-aware branch name, summary row `on origin | local only → cmd`.
|
||||
- **Why**: a shell gate `[ "$mode" = auto ] && git push …` is denied WHOLE by the text-only push-guard and `$mode` dies between Bash calls ([[LRN-199]]); the push it gated was redundant anyway ([[LRN-197]]); invalid value: lib/hooks still push (fail-open until run D) so "not pushed" from the mode word would lie — the count tells the truth.
|
||||
- **Alternatives rejected**: gate the existing push on the verb (denied/stateless); keep the GO question (gated nothing); word invalid as manual (false in the lib); per-skill `git config` read (denied).
|
||||
- **Gates**: C1: 3 lenses (1 BLOCKER: shell gate) + confirm CONCERNS(3); feater; GATE 0 MET; verifier CONFORME; security PASS. C2: 3 lenses (BLOCKER: deploy brief said "push done") + confirm CONCERNS(4); feater; verifier CONFORME; security BLOCK(1) branch-name injection ([[LRN-198]]) → fixed → CONFORME + PASS. Polish pass: CONFORME + PASS.
|
||||
- **Refs**: contracts `.claude/tasks/contracts/2026-10-07-manual-push-skills-c1-1304.md`, `…-c2-1325.md`; plans same slugs; commits 5cf049d, 6104545, 3881f46 (feature/manual-push-mode, UNMERGED). Links [[BDR-068]], [[BDR-095]], [[BDR-042]], [[LRN-069]].
|
||||
|
||||
@@ -572,4 +572,5 @@ rules:
|
||||
|
||||
## 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?
|
||||
- /feat manual-push-mode run C (user: "enchaine"), split C1+C2. C1: lib verb `gitflow.sh push-mode` (auto|manual|invalid, value on stderr, rc 0) + T11b; /close STEP 5C `git push origin develop` REMOVED (finish has pushed develop since BDR-095; shell gate would be denied whole by push-guard, `$mode` dies between Bash calls) → finish, verb, `rev-list --count origin/develop..develop`, prose outcomes incl. finish-failure; `--no-push` line from the branch's own count. Challenge 3 lenses (1 BLOCKER) + confirm CONCERNS(3); verifier CONFORME; security PASS. C2: client-handover GO question + push block removed (hooks pushed already in auto; guard denies in manual) → PUSH STATE READ re-run before every claim, branch-name allowlist (security BLOCK(1) → fixed, PASS); release-candidate: two counts + verb, `! git push --atomic origin main develop vX`, tag gate kept for auto/0/0; tour: `-C` fact per project, suffix-aware branch, `--remotes=origin`. Verifier CONFORME ×2. Commits 5cf049d + 6104545, UNMERGED. Polish pass in flight.
|
||||
|
||||
|
||||
@@ -1724,3 +1724,15 @@ Rule: when editing a doctrine file under structure locks, grep the test's lock s
|
||||
## 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]].
|
||||
|
||||
## LRN-197 — A skill's own `git push` after a lib finish or a hook-pushed commit is redundant since BDR-095: delete it, don't gate it
|
||||
- **Context**: `/close` STEP 5C ran `gitflow finish` then `git push origin develop` (added 2026-07-16); the lib push landed 2026-09-22 and 5C was never revisited. `/client-handover` asked "Push to origin now?" after commit-change, whose commits the post-commit hook had already pushed. Both runs' first plan GATED the push on the mode; the simplicity lens found both pushes redundant.
|
||||
- **Apply**: before gating an action, ask whether it still does anything. Grep every `git push` in skills/agents after any change to hooks/lib push behaviour (BDR-100 surface rule); replace a redundant push + its question by a FACT read afterwards (`git rev-list --count origin/<br>..<br>`) and a user hint. Links [[BDR-113]], [[BDR-095]], [[LRN-113]].
|
||||
|
||||
## LRN-198 — Text read from git and pasted into a later Bash call is an injection sink: allowlist before interpolating
|
||||
- **Context**: C2 replaced one Bash block (`CURRENT_BRANCH=$(git branch --show-current); git push origin "$CURRENT_BRANCH"`, quoted variable, safe) by three separate calls where the branch name is pasted as text into `git rev-list --count origin/<br>..<br>` and into `! git push -u origin <br>`. `git check-ref-format --branch 'x$(id)y'` rc 0: a hostile branch (PR checkout, crafted remote) runs its payload. Security gate BLOCK(1).
|
||||
- **Apply**: any name an agent READS (branch, tag, path from repo state) and later WRITES into command text must pass an allowlist first (`^[A-Za-z0-9._/][A-Za-z0-9._/-]*$`; leading `-` excluded = option injection); on mismatch interpolate nothing and say so. Prefer a quoted shell variable inside ONE call when the flow allows; when prose branching forces multi-call, the allowlist replaces the quotes. `<abs project>` from user args: same class, lower trust gap. Links [[BDR-113]], [[LRN-196]].
|
||||
|
||||
## LRN-199 — Agent-level branching is prose on a printed word: shell state dies between Bash calls, and a text guard denies the whole call
|
||||
- **Context**: plan wrote `mode=$(gitflow.sh push-mode)` then `[ "$mode" = auto ] && git push origin develop`. Two failures: (a) separate calls → `$mode` empty → silent skip, rc 1 misread as "push FAILED"; (b) one call → push-guard's STRICT regex matches `&& git push origin` in the TEXT and denies the WHOLE call, so even `finish` never runs. Three lenses hit it independently.
|
||||
- **Apply**: a skill reads a word from a command's visible stdout, then branches in PROSE ("printed `auto` → run X as its own call; anything else → never issue X"). Never a shell variable across calls, never a conditional that contains a guarded token. Any text-only PreToolUse guard turns `cmd-you-wanted-to-avoid` inside a conditional into a denial of the surrounding command. Links [[BDR-112]], [[BDR-113]], [[LRN-191]].
|
||||
|
||||
Reference in New Issue
Block a user