Merging a PR whose ## Done-when boxes are ticked and whose review is satisfied still forced two confirmations: a conversational ask and a redundant mechanical MCP permission prompt.
Root cause
pretooluse-merge-consent.sh's satisfied path did a bare exit 0. A PreToolUse hook that exits 0 with no JSON does not auto-approve — it only declines to block, so control falls through to the normal permission system and the raw mcp__gitea__pull_request_write prompt still fires. The gate therefore only ever added a deny/ask net; it never removed the baseline prompt on the happy path.
Fix
Only the genuinely-satisfied merge path (a=CI green, b=all Done-when ticked, c=positive Review-verdict @ current head) now emits {"hookSpecificOutput":{"permissionDecision":"allow", ...}} (new grant decision) — the derived state IS the consent. Deny (unticked/red/negative/stale) and ask (non-derivable) are unchanged: the gate still fails closed.
Two paths deliberately do not auto-grant and keep the bare exit 0 passthrough (one normal prompt): non-merge pull_request_write methods, and the docs/process-only exemption. The exemption's set includes .claude//.gitea//.husky/ — the gate, CI, and hooks themselves — so a PR that weakens the gate must still get a human sign-off, not silently self-merge (addresses the first-pass adversarial-review nit).
Verification
8 pipe tests via a fake curl on PATH: satisfied→allow, docs-only→passthrough, unticked→deny, stale-sha→deny, red-CI→deny, no-verdict→ask, no-creds→ask, non-merge→passthrough. bash -n + shellcheck clean.
Docs (same PR)
CLAUDE.md Task Completion Protocol — auto-grant behaviour + "don't also ask conversationally on the satisfied path".
docs/handoffs/chicorytv-issue-queue.md HARD CONSTRAINTS — merge consent is derived; ask only when the gate asks.
docs/decisions.md — new #314/#317-lineage entry.
Docs/process-only PR (.claude/, docs/, CLAUDE.md) → this PR itself hits the docs-exemption passthrough, so merging it still prompts once — an intentional human sign-off on a change to the merge gate.
Closes #317.
## Problem (user-reported)
Merging a PR whose `## Done-when` boxes are ticked and whose review is satisfied still forced **two** confirmations: a conversational ask **and** a redundant mechanical MCP permission prompt.
## Root cause
`pretooluse-merge-consent.sh`'s satisfied path did a bare `exit 0`. A PreToolUse hook that exits 0 with no JSON does **not** auto-approve — it only declines to block, so control falls through to the normal permission system and the raw `mcp__gitea__pull_request_write` prompt still fires. The gate therefore only ever *added* a deny/ask net; it never *removed* the baseline prompt on the happy path.
## Fix
**Only the genuinely-satisfied merge path (a=CI green, b=all Done-when ticked, c=positive Review-verdict @ current head)** now emits `{"hookSpecificOutput":{"permissionDecision":"allow", ...}}` (new `grant` decision) — the derived state IS the consent. Deny (unticked/red/negative/stale) and ask (non-derivable) are unchanged: the gate still fails closed.
Two paths deliberately do **not** auto-grant and keep the bare `exit 0` passthrough (one normal prompt): non-merge `pull_request_write` methods, and the **docs/process-only exemption**. The exemption's set includes `.claude/`/`.gitea/`/`.husky/` — the gate, CI, and hooks themselves — so a PR that weakens the gate must still get a human sign-off, not silently self-merge (addresses the first-pass adversarial-review nit).
## Verification
8 pipe tests via a fake `curl` on PATH: satisfied→`allow`, docs-only→passthrough, unticked→`deny`, stale-sha→`deny`, red-CI→`deny`, no-verdict→`ask`, no-creds→`ask`, non-merge→passthrough. `bash -n` + shellcheck clean.
## Docs (same PR)
- `CLAUDE.md` Task Completion Protocol — auto-grant behaviour + "don't also ask conversationally on the satisfied path".
- `docs/handoffs/chicorytv-issue-queue.md` HARD CONSTRAINTS — merge consent is derived; ask only when the gate asks.
- `docs/decisions.md` — new #314/#317-lineage entry.
Docs/process-only PR (`.claude/`, `docs/`, `CLAUDE.md`) → this PR itself hits the docs-exemption **passthrough**, so merging it still prompts once — an intentional human sign-off on a change to the merge gate.
timothy
added the bugci-cd labels 2026-07-12 20:53:05 +02:00
The H6/H10 merge-consent hook's satisfied path did a bare `exit 0`, which does
not auto-approve a PreToolUse tool call — it only declines to block, so control
fell through to the normal permission system and the raw MCP prompt still fired.
A ready-to-merge PR was therefore confirmed twice (conversationally + a redundant
mechanical prompt). Emit permissionDecision "allow" (new `grant` decision) on the
satisfied and docs-exempt paths so the derived state IS the consent; deny/ask
unchanged (fail-closed); non-merge methods keep the exit-0 passthrough.
Docs: CLAUDE.md, kickoff HARD CONSTRAINTS, docs/decisions.md (append-only, pure insert).
Verified: 7 pipe tests (satisfied->allow, unticked->deny, stale->deny, red-CI->deny,
no-verdict->ask, no-creds->ask, non-merge->passthrough).
fixes#317
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial-review nit on the first commit: auto-granting the docs-only exemption
silently self-merges process-control PRs (.claude/.gitea/.husky — including the
gate hook itself) with no prompt and no review, bypassing human-in-the-loop for
exactly the files that control the gate. Restrict auto-grant to the genuinely-
satisfied (a+b+c) merge path; the docs/process exemption reverts to bare exit-0
passthrough (one normal prompt). Corrects this PR's own decisions.md entry
accordingly ([decisions-edit]: a not-yet-merged draft entry, not history).
8 pipe tests green (adds docs-only -> passthrough).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pass 1 — initial diff (8565f731): MERGEABLE-WITH-NITS. No false-open; all deny/ask paths byte-identical to main; non-merge methods still bare exit 0 passthrough; emitted JSON valid. Nit (acted on): the docs-only exemption was changed to grant too, which would silently auto-merge process-control PRs (.claude//.gitea//.husky/ — including the gate hook itself) with no prompt and no review.
Fix (90c8348e): reverted the docs-exemption to a bare exit 0 passthrough (one human prompt). Auto-grant is now scoped to ONLY the genuinely-satisfied (a=CI green, b=all Done-when ticked, c=positive Review-verdict @ head) path.
Pass 2 — re-review of the fix commit (90c8348e): MERGEABLE, no nits. Confirmed the docs-exemption revert is the only behavioral change vs 8565f731, that it is a strict reduction of the auto-grant surface (monotonic tightening → cannot introduce a new false-open), that the docs-exemption now genuinely prompts a human (incl. PRs touching the gate itself), and that the satisfied a+b+c auto-grant path is unchanged and sound.
## Adversarial review (2 independent cold passes)
**Pass 1 — initial diff (`8565f731`)**: MERGEABLE-WITH-NITS. No false-open; all deny/ask paths byte-identical to main; non-merge methods still bare `exit 0` passthrough; emitted JSON valid. **Nit (acted on):** the docs-only exemption was changed to `grant` too, which would *silently* auto-merge process-control PRs (`.claude/`/`.gitea/`/`.husky/` — including the gate hook itself) with no prompt and no review.
**Fix (`90c8348e`)**: reverted the docs-exemption to a bare `exit 0` passthrough (one human prompt). Auto-grant is now scoped to ONLY the genuinely-satisfied (a=CI green, b=all Done-when ticked, c=positive Review-verdict @ head) path.
**Pass 2 — re-review of the fix commit (`90c8348e`)**: MERGEABLE, no nits. Confirmed the docs-exemption revert is the *only* behavioral change vs `8565f731`, that it is a strict *reduction* of the auto-grant surface (monotonic tightening → cannot introduce a new false-open), that the docs-exemption now genuinely prompts a human (incl. PRs touching the gate itself), and that the satisfied a+b+c auto-grant path is unchanged and sound.
Verification: 8 pipe tests (satisfied→allow, docs-only→passthrough, unticked→deny, stale→deny, red-CI→deny, no-verdict→ask, no-creds→ask, non-merge→passthrough); `bash -n` + shellcheck clean.
Review-verdict: MERGEABLE @ 90c8348e
timothy
merged commit 90c8348efe into main2026-07-12 21:05:28 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #317.
Problem (user-reported)
Merging a PR whose
## Done-whenboxes are ticked and whose review is satisfied still forced two confirmations: a conversational ask and a redundant mechanical MCP permission prompt.Root cause
pretooluse-merge-consent.sh's satisfied path did a bareexit 0. A PreToolUse hook that exits 0 with no JSON does not auto-approve — it only declines to block, so control falls through to the normal permission system and the rawmcp__gitea__pull_request_writeprompt still fires. The gate therefore only ever added a deny/ask net; it never removed the baseline prompt on the happy path.Fix
Only the genuinely-satisfied merge path (a=CI green, b=all Done-when ticked, c=positive Review-verdict @ current head) now emits
{"hookSpecificOutput":{"permissionDecision":"allow", ...}}(newgrantdecision) — the derived state IS the consent. Deny (unticked/red/negative/stale) and ask (non-derivable) are unchanged: the gate still fails closed.Two paths deliberately do not auto-grant and keep the bare
exit 0passthrough (one normal prompt): non-mergepull_request_writemethods, and the docs/process-only exemption. The exemption's set includes.claude//.gitea//.husky/— the gate, CI, and hooks themselves — so a PR that weakens the gate must still get a human sign-off, not silently self-merge (addresses the first-pass adversarial-review nit).Verification
8 pipe tests via a fake
curlon PATH: satisfied→allow, docs-only→passthrough, unticked→deny, stale-sha→deny, red-CI→deny, no-verdict→ask, no-creds→ask, non-merge→passthrough.bash -n+ shellcheck clean.Docs (same PR)
CLAUDE.mdTask Completion Protocol — auto-grant behaviour + "don't also ask conversationally on the satisfied path".docs/handoffs/chicorytv-issue-queue.mdHARD CONSTRAINTS — merge consent is derived; ask only when the gate asks.docs/decisions.md— new #314/#317-lineage entry.Docs/process-only PR (
.claude/,docs/,CLAUDE.md) → this PR itself hits the docs-exemption passthrough, so merging it still prompts once — an intentional human sign-off on a change to the merge gate.Adversarial review (2 independent cold passes)
Pass 1 — initial diff (
8565f731): MERGEABLE-WITH-NITS. No false-open; all deny/ask paths byte-identical to main; non-merge methods still bareexit 0passthrough; emitted JSON valid. Nit (acted on): the docs-only exemption was changed togranttoo, which would silently auto-merge process-control PRs (.claude//.gitea//.husky/— including the gate hook itself) with no prompt and no review.Fix (
90c8348e): reverted the docs-exemption to a bareexit 0passthrough (one human prompt). Auto-grant is now scoped to ONLY the genuinely-satisfied (a=CI green, b=all Done-when ticked, c=positive Review-verdict @ head) path.Pass 2 — re-review of the fix commit (
90c8348e): MERGEABLE, no nits. Confirmed the docs-exemption revert is the only behavioral change vs8565f731, that it is a strict reduction of the auto-grant surface (monotonic tightening → cannot introduce a new false-open), that the docs-exemption now genuinely prompts a human (incl. PRs touching the gate itself), and that the satisfied a+b+c auto-grant path is unchanged and sound.Verification: 8 pipe tests (satisfied→allow, docs-only→passthrough, unticked→deny, stale→deny, red-CI→deny, no-verdict→ask, no-creds→ask, non-merge→passthrough);
bash -n+ shellcheck clean.Review-verdict: MERGEABLE @
90c8348e