# CONTRACT โ manual-push-guard (run B of manual-push mode) - date: 2026-10-07 | flow: feat | branch: feature/manual-push-mode (run A landed as 2fc8830; run C = skills that push, separate) - status: active ## REQUEST (verbatim โ IMMUTABLE) User (fr): "ok enchaine sur le run B" Run B as scoped in `.claude/tasks/contracts/2026-10-06-manual-push-mode-1632.md` CLARIFICATIONS and `.claude/tasks/TODO.md` "manual-push-mode": `hooks/push-guard.sh` PreToolUse (deny `git push` in manual mode) + test + settings.json (hook wiring, widen `gitflow.*` deny: `git config * gitflow.*`, `git -c gitflow.*`, `GIT_CONFIG_COUNT=*`; environment prose ~480/~499) + session-start banner push mode. User decisions (2026-10-06): mechanical block of `git push` chosen; only `! git push` (the user, in the terminal) passes; hook name `hooks/push-guard.sh` + `lib/tests/push-guard.test.sh`. ## CLARIFICATIONS Q: hook deny form / A: documented JSON on stdout, exit 0: `hookSpecificOutput.permissionDecision = "deny"` + `permissionDecisionReason` (code.claude.com/docs/en/hooks.md). Reason reaches Claude as the tool error. [orchestrator, internal] Q: middle wildcards in `permissions.deny` Bash patterns / A: supported (`Bash(git * main)` documented), `*` matches any text incl. spaces, literal match on the whole command string. [orchestrator, verified via docs] Q: fail-CLOSED on an unparseable `gitflow.autopush` value / A: user: refuse the push. In the GUARD only (deny, reason names the invalid value); lib and emitted hooks stay fail-open until run D (every reader at once, emitters included). [gated 2026-10-07] Q: banner wording / A: user picked `push : manual`; final line (43 chars, fits the 44-char box): `๐ push : manual (autopush=false) โ ! git push`. [gated 2026-10-07] Q: `git push --dry-run` / `-n` in manual mode / A: denied like any push (one rule, no carve-out; the user runs it). [orchestrator โ simplest, stated] Q: challenge r1 โ deny widening vs run C's read / A: widen WRITE forms only (`git *config *gitflow.* *`, `*unset*`, `-c`, `--config-env`, `GIT_CONFIG_PARAMETERS`, `GIT_CONFIG_COUNT`, Edit/Write of `.git/config` and `.gitconfig`); the read `git config --bool --default true gitflow.autopush` stays reachable for run C. `Bash(env GIT_CONFIG_COUNT*)` dropped (covered by the existing `env GIT_CONFIG*`). [gated 2026-10-07, orchestrator โ scope] Q: challenge r1 โ no-jq fallback / A: dropped; jq is a hard dependency (install-plugins.sh); the guard warns on stderr and allows, like every sibling hook. Fail-closed EXIT trap kept for internal errors once a push is detected. [orchestrator โ internal] Q: challenge r1 โ mode read outside a repo / A: no work-tree gate; `git config` reads global/system there (work-machine `--global` deployment). Candidate dirs = cwd + literal `-C`/`cd` tokens; unresolvable โ skipped, never an allow. [orchestrator โ internal, fail-closed] Q: challenge r1 โ classifier coverage / A: one soft_deny entry added for pushes in manual mode in any form (scripts, aliases, subshells, sub-agents); env prose no longer names the hook as the whole defence. Matcher `Bash|Monitor` in its own hook group, timeout 10 s. [orchestrator] Q: confirmation r2 โ bare read / A: a trailing ` *` in a permission glob also matches end-of-string (evidence in plan Context), so the bare read `git config โฆ gitflow.autopush` is denied for Claude after run B; hooks and lib keep it (not tool calls). Run C reads the mode through a lib verb (`gitflow.sh push-mode`), recorded in TODO. The deny list is simplified to `Bash(git *config *gitflow.*)` + section-level and env/edit forms (18 entries). [gated 2026-10-07, orchestrator โ scope, surfaced to the user] Q: confirmation r2 โ oracles / A: settings.json assertions live in the test file (T40โT43), never in a CHECK command or a commit message: the new tokens would deny the command that names them. [orchestrator] Q: hardening gate โ two cases where the mode cannot be read safely (more than 20 distinct `cd`/`-C` dir tokens in one command; a named dir that exists but cannot be entered) deny the push even when the cwd is in auto mode; the verifier flagged this against criterion 2's "zero noise outside manual mode" / A: user: refuse the push (fail closed). Criterion 2 is read with this exception: auto-mode silence holds for every command whose named dirs can all be evaluated and number at most 20. [gated 2026-10-07] Q: full-suite criterion / A: every suite except `lib/tests/design-tool-gate.test.sh`, a pre-existing environmental red on this machine (21st CLI present; reproduced on develop fa67664 without run A; TODO "test hermeticity"). Declared upfront, not loosened after a red. [orchestrator] ## ACCEPTANCE CRITERIA 1. `hooks/push-guard.sh` (PreToolUse) denies any Bash command that runs `git push` โ plain, `git -C