Skip to content

fix: a guardrail with no objection was disabling the user's permission prompts - #62

Merged
jothimani-rajendran merged 1 commit into
mainfrom
claude/agent-memory-governance-gu5n6z
Aug 28, 2026
Merged

jothimani-rajendran merged 1 commit into
mainfrom
claude/agent-memory-governance-gu5n6z

Conversation

@jothimani-rajendran

Copy link
Copy Markdown
Collaborator

The most serious thing found in the un-itemised remainder, and a behaviour change — flagging that explicitly.

What was happening

On claude_code and vscode_copilot, a bare Decision.allow() at pre_tool emitted:

{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "allow"}}

"The handler has no objection" and "approve this without asking the user" are different statements. The difference is the user's confirmation dialog.

A handler returns allow for everything its policy does not match — which on a normal session is every tool call. So installing agentseam turned the user's confirmation prompts off for the whole session. The exact inverse of what a guardrail is for.

The evidence, which differs by vendor

VS Code — proven from source, not inferred. languageModelToolsService.ts:

if (hookResult?.permissionDecision === 'allow') {
    this._logService.debug(`Tool ${dto.toolId} auto-approved by preToolUse hook`);
    return { autoConfirmed: { type: ToolConfirmKind.ConfirmationNotNeeded,
                              reason: localize('hookAllowed', "Allowed by hook") }, ... };
}

Claude Code — the vendor documents what silence does, verbatim:

"Exit code 0 with no output means the hook has no decision to report, so the tool call continues through the normal permission flow."

and says nothing about what an explicit allow does. So one option has recorded, safe semantics and the other is a guess. That is not a guess worth making with the user's protection — the same reasoning that kept tabnine's vocabulary unchanged rather than swapped for another unrecorded word.

The change

A bare allow is now silence. Unchanged:

outcome before after
allow permissionDecision: "allow" ""
deny permissionDecision: "deny" same
ask permissionDecision: "ask" same
rewrite allow + updatedInput same

Rewrite deliberately keeps the explicit allow: updatedInput is the only way to express a rewrite, and approving the substituted call is precisely what the handler asked for.

codex_cli already behaved this way, for an unrelated vendor reason — it rejects permissionDecision: allow without updatedInput. So the repo was inconsistent, and the odd one out was the safe one.

Behaviour change

Anyone relying on allow to auto-approve loses that. On Claude Code that reliance was on undocumented semantics; on VS Code it was on prompt-suppression. Neither is something a guardrail library should do by default, and neither was ever stated as a feature.

Verification

ruff check, ruff format --check, full suite: 857 passed. The test that pinned the old behaviour now pins the new one, with the reasoning in its docstring.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Y9DXWmRN9A2q5BXAiexapu


Generated by Claude Code

…n prompts

On claude_code and vscode_copilot a bare Decision.allow() at pre_tool emitted
hookSpecificOutput.permissionDecision:"allow". That is not the same statement as "the
handler has no objection", and the difference is the user's confirmation dialog.

For VS Code this is proven from the vendor's source rather than inferred:
languageModelToolsService returns autoConfirmed {ConfirmationNotNeeded, "Allowed by
hook"} on exactly that value. For Claude Code the vendor documents what SILENCE does,
verbatim -- "Exit code 0 with no output means the hook has no decision to report, so the
tool call continues through the normal permission flow" -- and says nothing about an
explicit allow. One option has recorded, safe semantics; the other is a guess, and it is
not a guess worth making with the user's protection.

A handler returns allow for everything its policy does not match, which on a normal
session is every tool call. So installing agentseam turned the user's confirmation
prompts off for the whole session -- the exact inverse of what a guardrail is for.

A bare allow is now silence. deny, ask and rewrite are unchanged, and rewrite keeps the
explicit allow because updatedInput is the only way to express one and approving the
substituted call is what the handler asked for. codex_cli already behaved this way, for
a different vendor reason: it rejects permissionDecision:allow without updatedInput.

This is a behaviour change for anyone relying on allow to auto-approve. That reliance
was on undocumented semantics on Claude Code, and on prompt-suppression on VS Code.

Signed-off-by: Claude <noreply@anthropic.com>
@jothimani-rajendran
jothimani-rajendran marked this pull request as ready for review August 28, 2026 15:35
@jothimani-rajendran
jothimani-rajendran merged commit c726541 into main Aug 28, 2026
12 checks passed
jothimani-rajendran pushed a commit that referenced this pull request Aug 28, 2026
Only CHANGELOG.md conflicted -- both sides prepend to the same '### Fixed' list -- and
both entries are kept, #62's first as the older one. The two adapters #62 also touched,
claude_code.py and vscode_copilot.py, auto-merged cleanly.

Verified after resolving that both changes survive: a bare allow is still silence
(#62), and no adapter crashes on a non-dict tool_input (#63).

Signed-off-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants