Eve: gate the dangerous tool behind a human, in one field
Make an Eve agent's irreversible tool stop and wait for a person above a threshold, using the needsApproval predicate, and verify the file is shaped right before you trust it.
Run this workflow
CI-verified, 2/2 fixtures passing.
Build this with your agent
One copy-paste hands Claude Code, Codex, or Cursor the full recipe, steps included, nothing to fetch.
Intended Use
Anyone giving an Eve agent a tool that costs money or cannot be undone. CI writes the files and runs a node structure check: refund_payment.ts exports a defineTool with a Zod inputSchema and a needsApproval function, and agent.ts declares a model. A type-and-structure check, no npm install, no model run. The agent actually pausing for approval is fenced.
Not for
- Treating approval as containment, it limits blast radius, it does not contain a bad tool; the model still decides when to call it, so scope the tool narrowly
- Gating everything, if every call needs approval people rubber-stamp the prompts and the gate stops meaning anything; gate the consequential subset
The Stack
Tested Against
github.com/vercel/eve + vercel.com/docs/eve (2026-06, eve@0.11.x)node@20Side effects & data flow
- Network
- none, local only
- Writes
- ./agent/agent.ts, ./agent/tools/refund_payment.ts, ./lint.mjs
- Credentials
- none required
Prerequisites
- Node 20+
- An Eve project (npx eve@latest init) to actually run it
Steps
- 1
Author the gated tool and structure-check it
Write agent.ts (names the model) and agent/tools/refund_payment.ts (a typed defineTool whose needsApproval predicate returns true above a threshold). CI runs a node check that the files are shaped right. The agent pausing for a human only happens on a live run, so that step is fenced.
mkdir -p agent/tools cat > agent/agent.ts <<'TS' import { defineAgent } from "eve"; export default defineAgent({ model: "anthropic/claude-opus-4.8", name: "billing-assistant", }); TS cat > agent/tools/refund_payment.ts <<'TS' import { defineTool } from "eve/tools"; import { z } from "zod"; import { processRefund } from "../lib/billing"; export default defineTool({ description: "Refund a payment to a customer by payment id.", inputSchema: z.object({ paymentId: z.string(), amountUsd: z.number().positive(), }), needsApproval: ({ toolInput }) => toolInput.amountUsd >= 100, async execute({ paymentId, amountUsd }) { const result = await processRefund(paymentId, amountUsd); return { ok: result.ok, refundId: result.id }; }, }); TS cat > lint.mjs <<'MJS' import { readFileSync } from "node:fs"; const agent = readFileSync("agent/agent.ts", "utf8"); const tool = readFileSync("agent/tools/refund_payment.ts", "utf8"); function fail(m) { console.error("BAD: " + m); process.exit(1); } if (!agent.includes("defineAgent")) fail("agent.ts has no defineAgent"); const mi = agent.indexOf("model:"); if (mi < 0) fail("agent.ts declares no model"); const mseg = agent.slice(mi, mi + 60); if (!mseg.includes(String.fromCharCode(34)) && !mseg.includes("'")) fail("model has no string value"); if (!tool.includes("defineTool")) fail("refund_payment.ts has no defineTool"); if (!tool.includes("inputSchema") || !tool.includes("z.object")) fail("tool has no Zod inputSchema"); const ai = tool.indexOf("needsApproval"); if (ai < 0) fail("tool has no needsApproval"); if (!tool.slice(ai, ai + 80).includes("=>")) fail("needsApproval is not a function"); console.log("config OK: agent.ts declares a model, refund_payment.ts exports a defineTool with a Zod inputSchema and a needsApproval function"); MJS node lint.mjs - 2
Run it and approve a refund (the live step, not checked by CI)
Scaffold a real project with npx eve@latest init, run eve dev, and ask it to refund an amount over the threshold; the agent pauses at refund_payment and waits for your approval, then resumes. The model deciding and the tool firing are fenced.
Eval, 2 fixtures
Last passed: verified todayapproval-okcontainstimeout 30s · max $0Expected:
config OK: agent.ts declares a model, refund_payment.ts exports a defineTool with a Zod inputSchema and a needsApproval functionclean-exitexit_codetimeout 30s · max $0Expected:
0
Results
An agent that can touch real systems should not do the irreversible thing unsupervised. In Eve a tool is one typed file and needsApproval is one predicate: return true and the agent pauses at that call, waits for a human without burning compute, then resumes exactly where it stopped once approved.
Did this work for you?
Our CI checks the setup runs. You tell us if the whole thing worked. Tell us straight.
Liked this workflow?
Get new verified workflows in WebAfterAI, three issues a week (Tue, Thu, Sat).