The reviewer approved send() on a prompt they never saw. Rubber-stamping every leftover tool isn't a removed leg of the lethal trifecta.
Why isn't human approval a security policy?
A policy is a repeatable decision: this tool, these arguments, this identity, allow or deny. A click is a person, this once.
Fatigue turns the hold into an allowlist. The reviewer sees twenty lookupOrder calls and one createPullRequest. They click through. The issue exists.
A person also can't fire on the calls that never reach them. Claude's bare allowedTools name auto-approves every call unless an ask rule or plan mode sends it back. The file is on disk. Nobody was asked.
protect() is the HTTP check on a route. guard() is the check on a tool or MCP call. Neither is a Slack DM to the on-call.
What does Eve's approval click do?
On Eve, the human click is onAllow: "user-approval" after a policy allow. It parks the run until a person answers. That click isn't guardApproval().
guardApproval() is the connection gate. OpenAPI and MCP connections have no local execute. The only enforcement point is approval. Hooks can't refuse a turn. arcjetHooks() observes. A person on send() doesn't screen the Slack body.
Don't compose approval with Eve's always(), once(), or never(). To also require a human, set onAllow: "user-approval" on the policy allow. The click is the hold after allow. It isn't the policy.
import { defineMcpClientConnection } from "eve/connections";import { tokenBucket } from "@arcjet/guard";import { guardApproval } from "@arcjet/guard/vercel-eve/v0";import { arcjet } from "../arcjet.js";
const mcpLimit = tokenBucket({ bucket: "mcp-access", refillRate: 30, intervalSeconds: 60, maxTokens: 30,});
export default defineMcpClientConnection({ url: "https://api.example.com/mcp", description: "Linear", approval: guardApproval(arcjet, { action: "linear.created", rules: (ctx) => [mcpLimit({ key: ctx.session.id, requested: 1 })], onAllow: "user-approval", }),});Is Mastra requireApproval a policy gate?
No. Mastra requireApproval pauses for a human. It isn't a remote policy.
beforeToolCall can deny without a person. Processors stop messages. They don't authorize createPullRequest. Mastra guardrails vs an action gate is that split.
On Mastra, Arcjet Guards wrap createTool with guardTool, or sit on beforeToolCall via guardHooks for MCP and workspace tools that you didn't wrap.
Is Claude canUseTool a policy gate?
No. canUseTool is the ask path. It runs when no earlier step resolved the call.
A bare allowedTools entry such as Bash or Write auto-approves every call unless an ask rule or plan mode sends it back. bypassPermissions skips the person for everything that reaches the mode step. canUseTool is not a policy gate is the Claude-only write-up.
On Claude Agent SDK, Arcjet Guards wrap authored tools with guardTool. Built-ins and MCP that you didn't wrap go through PreToolUse via guardHooks. Don't put the deny on canUseTool.
What has to sit in front of the click?
The policy. Screen inbound text. Deny the tool or the connection before the call reaches the host. Then, for the few irreversible allows, wait for a person.
Runtime security for LLM applications already splits prompt injection, exfiltration, and unsafe actions. The click is a hold after allow.
Helpers default to onGuardError: "deny". "allow" can be legitimate on inbound only (Eve's channel, Mastra's inbound processor, Claude's UserPromptSubmit) because failing closed there would stop the agent from answering during an outage. Pass a conversation identity that you already have.
Park the small set that can't be undone: a charge, a public pull request, a mail to a customer. Don't park every Read. Human approval gates is how to build that hold. A sandbox is not a tool policy is the isolation half of the same mistake. A person is the other half.
If the only control is the click, the reviewer approved send() on a prompt they never saw. The issue already exists.
Frequently asked questions
Why isn't human approval a security policy?
A policy is a repeatable allow or deny on this tool, these args, and this identity. A click is a person, this once. Fatigue turns the hold into an allow list, and a person cannot fire on the calls that never reach them.
What does Eve's approval click actually do?
Eve's human click is onAllow: "user-approval" after a policy allow. It parks the run until a person answers. That click is not guardApproval(). guardApproval() is the connection gate. Hooks cannot refuse a turn.
Is Mastra requireApproval a policy gate?
No. Mastra requireApproval pauses for a human. It is not a remote policy. beforeToolCall can deny without a person. Processors abort messages; they do not authorize createPullRequest.
Is Claude canUseTool a policy gate?
No. canUseTool is the ask path. A bare allowedTools name auto-approves every call unless an ask rule or plan mode sends it back. Put policy on guardTool or PreToolUse, not on the callback.
What has to sit in front of the click?
A remote allow or deny on the tool or connection, plus inbound screening, before the host is called. Then park the few irreversible allows for a person. Helpers default to onGuardError: "deny".
AI runtime security in your code
Protect your AI agent workflows with Arcjet
Get allow, deny, and redact on agent actions before the side effect.