fix(guard): refuse every asked-about call where no one can be asked

In the permission mode that shows no prompts, pre-bash-executor refused only a
merge into main and a push. A tag, a package publish and destructive git still
went through unattended, which is what the approval guard exists to stop. Now
every call it would ask about, and a command it could not read, is refused
there with the way forward (operator decision 2026-10-09). No new exemption:
the mode is still read in one place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-09 19:36:21 +02:00
commit bb0fb361a5
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
5 changed files with 19 additions and 11 deletions

View file

@ -22,8 +22,8 @@
// tag/release, destructive git (hooks/lib/git-approval.mjs). The prompt is
// labelled [plugin:voyage] and also appears in auto mode (hooks reference
// section PreToolUse decision control). Advisories are added to the prompt.
// In the permission mode that shows no prompts, a merge into main and a
// push are BLOCKED instead: there is no one to ask (operator, 2026-10-09).
// In the permission mode that shows no prompts, every call it would ask
// about is BLOCKED instead: there is no one to ask (operator, 2026-10-09).
// - WARN (exit 0): risky but recoverable operations — advisory to stderr
// - Allow (exit 0): everything else
//
@ -274,13 +274,14 @@ try {
}
// In the permission mode where Claude Code shows no prompts, an "ask" reaches
// no one and the call runs unattended. A merge into main and a push are refused
// there instead, with the way forward (operator decision 2026-10-09: nothing
// goes to main without a person, in any mode). /trekexecute relies on this: its
// main-merge gate is a lock, and this guard's question is the one question.
// no one and the call runs unattended. Everything this guard would ask about —
// merge into main, push, tag/release, destructive git, and a command it could
// not read — is refused there instead, with the way forward (operator
// decisions 2026-10-09: first merge and push, then the other two classes).
// /trekexecute relies on this: its main-merge gate is a lock, and this guard's
// question is the one question.
const NO_PROMPT_MODE = 'bypassPermissions';
const unattended = input.permission_mode === NO_PROMPT_MODE
? approvals.filter((m) => m.cls === 'merge' || m.cls === 'push') : [];
const unattended = input.permission_mode === NO_PROMPT_MODE ? approvals : [];
if (unattended.length > 0) {
process.stderr.write(
`[voyage] BLOCKED: ${unattended.map((m) => m.name).join(', ')} with no one to ask\n` +