---
key: release.merge-consent-autogrant
title: 2026-07-12 — Merge-consent gate auto-grants when satisfied (no redundant prompt); state IS the consent (#314)
status: active
since: '2026-07-12'
supersedes: none
superseded-by: none
rule: 'When Done-when boxes are ticked, CI is green, and a fresh positive Review-verdict references head, the merge-consent hook emits `permissionDecision: allow` to actually suppress the redundant mechanical prompt — the derived state IS the consent, no separate conversational confirmation on that path.'
signals: 'auto-grant, permissionDecision allow, merge consent · paths: `.claude/settings.json` · issues: #314, #303, #317'
mechanics: '`pretooluse-merge-consent.sh`; CLAUDE.md → Task Completion Protocol'
---

Completes the #303 H6/H10 intent — *derive merge-consent from state* — which the original hook only
half-delivered. The rule the user set: **merge permission is auto-granted for the session when the
linked issue's `## Done-when` boxes are all ticked, a fresh positive `Review-verdict` references the
current head, and CI is green** — no separate confirmation, conversational or mechanical.

**Root cause of the bug this fixes:** `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 permission prompt
still fires (the merge tool isn't allow-listed). So the gate only ever *added* a deny/ask net; it never
*removed* the baseline prompt on the happy path. Net effect for the operator: a ready-to-merge PR was
confirmed twice — once conversationally (the per-session merge-consent norm) and again by a redundant
mechanical prompt the gate was supposed to have subsumed.

**Fix:** ONLY the genuinely-satisfied merge path (a+b+c all true) now emits
`{"hookSpecificOutput":{"permissionDecision":"allow", ...}}` (a new `grant` decision), which actually
suppresses the prompt. Deny (unticked/red/negative/stale) and ask (non-derivable: no creds, Gitea
down, no linked issue, no `## Done-when`, no verdict) are unchanged — the gate still fails closed, not
open. Two paths deliberately do **not** auto-grant and keep the bare `exit 0` **passthrough** (normal
permissioning → one prompt): non-merge `pull_request_write` methods (auto-grant is scoped to
method=merge only), and the **docs/process-only exemption**. The exemption is a file-TYPE bypass, not
the a+b+c "provably reviewed & ready" proof, so it must not *silently* self-merge — critically, its set
includes `.claude/`/`.gitea/`/`.husky/` (the gate, CI workflows, and git hooks themselves), so a PR
that weakens the gate still gets a human prompt (ersatztv#317 review nit). Verified by 8 pipe tests
(satisfied→allow, docs-only→passthrough, unticked→deny, stale→deny, red-CI→deny, no-verdict→ask,
no-creds→ask, non-merge→passthrough).

**Process consequence:** the state-derived gate *is* the consent on the satisfied path — do **not**
also ask conversationally to merge a PR whose gate auto-grants. A separate human confirmation is still
warranted only when the gate **asks** (state not derivable). This supersedes the "always confirm merge
consent in-conversation per session" phrasing in the kickoff HARD CONSTRAINTS (updated in the same PR).
