Human-in-the-loop (HITL) for AI agents means the agent stops before a high-impact action, shows a person exactly what it is about to do, and waits until that person approves, edits or rejects it. The pause itself is easy to build. The hard parts are asking rarely enough that people still read the question, asking clearly enough that they can answer from a phone, and deciding in advance what happens when nobody answers.
This guide covers the policy side first: which actions to gate, how the OWASP guidance maps onto them, why approving everything backfires, and how to design the ask and its deadline. The last sections show the same pattern in the Claude Agent SDK, the OpenAI Agents SDK, the Vercel AI SDK, LangGraph and MCP.
Why agents need a checkpoint
An agent with tools can do anything its credentials allow, and it decides what to do from text it reads. Most of the time that works. The failures are rare, and they are expensive.
- In July 2025 a Replit agent deleted a production database during a code freeze at SaaStr. Replit's CEO called it "unacceptable and should never be possible."
- Anthropic's write-up on Claude Code auto mode lists over-eager actions from its own internal incident log: attempting migrations against a production database, deleting remote git branches after misreading an instruction, and uploading an engineer's GitHub auth token to an internal compute cluster.
None of these needed a malicious model. An ambiguous instruction, a prompt injection hidden in an issue or a README, or a stale assumption about which environment the shell points at is enough. A checkpoint in front of the irreversible step turns each of those into a question instead of an incident.
What OWASP says about approvals
OWASP's guidance is the closest thing the industry has to a shared checklist, and it is specific about approvals.
- LLM06:2025 Excessive Agency. Mitigation 6, "Require user approval", says to use human-in-the-loop control so a human approves high-impact actions before they are taken. Mitigation 7, "Complete mediation", says to enforce authorization in downstream systems instead of trusting the LLM to decide what is allowed.
- LLM03:2026 Excessive Agency. The 2026 edition of the LLM Top 10 (August 2026) moves Excessive Agency to third place. Its complete-mediation item adds a graduated enforcement policy (audit, warn, block, escalate): low-consequence or easily reversible actions auto-approve, while high-consequence or irreversible ones go to human review.
- Top 10 for Agentic Applications (December 2025). ASI02, Tool Misuse and Exploitation, asks for human confirmation of high-impact or destructive actions ("delete, transfer, publish") and a pre-execution plan or dry-run diff before approval. ASI03, Identity and Privilege Abuse, asks for human approval before privilege escalation. ASI09, Human-Agent Trust Exploitation, covers how the ask itself can be abused, which we come back to below.
The same document introduces "Least-Agency": don't grant autonomy where the task doesn't need it. An approval prompt is not a substitute for least privilege. A read-only database role beats a "may I run this DELETE?" prompt on a superuser connection, because the role still holds when the human clicks the wrong button.
The problem with approving everything
If approvals are good, why not approve every tool call? Because people stop reading.
- In the same auto mode write-up (March 2026), Anthropic reports that with manual permission prompts in Claude Code, "users accept 93% of them anyway". It names the result: approval fatigue, where people stop paying close attention to what they approve.
- A later post puts the figure at 97% of permission prompts (August 2026, updated October 2026). The same post shows that people do engage with bigger decisions: they reject 39% of plans Claude presents, but only 3% of individual permission requests.
- In a study with 1,053 paid testers, Anthropic swapped one permission prompt per session for a clearly dangerous command. Human review caught it 13.6% of the time; the auto mode classifier blocked 89% of the same commands. The testers knew they were being evaluated, and approved commands were never actually run.
OWASP treats this as a threat in its own right. ASI09 cross-references "Overwhelming the Human in the Loop" (T10 in OWASP's Agentic AI Threats and Mitigations guide), where attackers flood reviewers until approvals become a reflex. ASI08 describes "governance drift": oversight weakens after repeated success, and bulk approvals let bad changes through.
Three conclusions follow. Ask less often, so each ask carries weight. Ask about plans and outcomes, which people actually evaluate, rather than individual keystrokes. And automate the bottom of the risk range with policy (allowlists, sandboxes, classifiers) so humans only see the top. That is the route Anthropic took: auto mode is now the default starting permission mode in interactive Claude Code sessions.
What to gate: a risk-tiered model
Sort every action your agent can take into one of four tiers. The tier depends on the action and its input, not on the tool name: run_sql with a SELECT is tier 1, and the same tool with a DELETE is tier 3.
| Tier | Examples | What the agent does | OWASP hook |
|---|---|---|---|
| 1. Auto | Read files, search, run tests, edit on a branch, local builds | Proceeds, logs the call | LLM03:2026 graduated policy: reversible actions auto-approve |
| 2. Notify | Task finished or failed, PR opened, staging deploy done | Proceeds, then tells you | "Monitor tool use" (LLM03:2026) |
| 3. Ask | Delete data, prod deploy or migration, spend money, send or publish externally, change permissions or secrets, act outside the task's scope | Waits for approve, edit or reject | ASI02 (delete, transfer, publish), ASI03 (privilege escalation), LLM06:2025 #6 |
| 4. Deny | Force-push to main, read the secrets directory, disable logging | Refused by policy; no ask | Complete mediation (LLM06:2025 #7) |
A few rules make the table hold up in practice.
- Gate on parameters, not just tools. A payment under a threshold can run; one over it asks. The Vercel AI SDK docs use exactly this example, with approval only for amounts over $1,000.
- Treat scope changes as tier 3. Touching another repo, another environment or another customer's data is a new decision, even if each step looks routine.
- Keep tier 4 out of the model's hands. Enforce it in credentials and downstream systems, so a persuasive prompt injection can't talk its way past it.
- Revisit the tiers. If a tier 3 action gets approved every single time, it may belong in tier 2 with better guardrails.
How to ask well
A good ask can be answered correctly in ten seconds, on a phone, by someone who wasn't watching the session.
- One decision per ask. Phrase it so a button answers it: "Drop table
legacy_eventson prod-eu?", not "Proceed?" - Safest option first. If a timeout or a careless tap picks a default, it should be the option that changes nothing: "Cancel" before "Drop it".
- Show the effect, not the story. Include the exact command or diff, the target environment and the blast radius (row counts, number of files, recipients). ASI02 asks for a dry-run diff. ASI09 warns that a model-generated rationale can be fabricated, and recommends plain-language risk summaries instead. Prefer facts your tool computed over the agent's explanation of why it's fine.
- Separate preview from effect. Rendering the approval must not trigger anything. ASI09's example is a "read-only" preview pane that fires webhooks when opened.
- Offer edit and reject-with-reason. A reason ("archive them instead") lets the agent adapt instead of retrying the same call. The Vercel docs suggest a system instruction telling the model not to retry a denied tool.
- Say who is asking, and by when. Agent name, repo, branch, environment and the deadline.
- Never collect secrets through the ask. MCP's elicitation spec forbids form-mode requests for passwords, API keys or tokens; the same rule is sensible for any approval channel. Our MCP explainer covers elicitation in more detail.
When nobody answers
Every ask needs a deadline and a defined outcome. The person might be asleep, in a meeting or on a plane, and an agent that waits forever is a stuck job.
There are three sane outcomes, and you should pick one per action type before you ship:
- Deny, or take the safest option. Tell the agent nobody approved, and let it report back or move on to other work.
- Escalate. Send the same question to a second approver or the on-call admin.
- Stop and report. End the run with a clear summary of what is waiting.
What you must never do is proceed with the risky option because nobody objected. Silence is not consent.
Size the deadline to the action. A hotfix deploy might wait fifteen minutes before escalating; a weekly cleanup job can wait a day. Long waits also change the engineering: no process should hold a callback open overnight.
- The Claude Agent SDK's
canUseToolcallback "can stay pending indefinitely", but for long waits the docs recommend aPreToolUsehook that returnsdefer, so the process can exit and resume from the persisted session. - The OpenAI Agents SDK serializes pending approvals with
state.to_json()and resumes them later. - LangGraph saves the whole graph state in a checkpointer.
Two more rules apply when the answer finally arrives. First, re-check before acting: the branch may have moved or the table may have changed during a six-hour wait, and an approval covers a specific input, not whatever the input becomes. Second, design for retries. The OpenAI Agents SDK docs are blunt: "Approval permits a tool call to execute; it does not guarantee exactly-once execution." Use idempotency keys for anything with external effects.
Reaching a human who is away
Where the question shows up matters as much as how it's worded. There are three broad options.
In-session prompts. The harness's own permission dialog in Claude Code, Cursor or Codex, or an MCP elicitation request rendered by the client. These work well when you're at the keyboard and not at all when you're not.
Vendor-native remote control. Claude Code Remote Control can push to your phone when an action needs approval, Cursor has an iOS app, and Codex runs in the ChatGPT app. These answer the harness's own permission prompts, which nothing else can do for you. Our Claude Code Remote Control guide compares them.
Out-of-band channels. Push services, Slack, Telegram or email. Sending a one-way message is easy with ntfy, Pushover or a Slack webhook (see ntfy vs Pushover vs Slack webhooks). The round trip, getting a button press back into the waiting agent with a deadline attached, is the part you would otherwise build yourself.
Out-of-band asks with AlertMCP
This round trip is what we built AlertMCP for. It is an MCP server for AI agent notifications: once it's connected, the agent calls ask with a question and two to four buttons, safest option first, and your phone or desktop shows it. The agent then calls get_answer, which waits up to 50 seconds per call and returns answered, still pending or timed out, so the agent loops until it has a result.
Each workspace sets a maximum response time from 5 minutes to 24 hours, with per-topic overrides, and a live countdown shows on every client. On timeout, depending on your setting, the agent is told nobody answered, continues with the first (safest) option, or the ask escalates to your team admins. Priorities, quiet hours and per-topic rate limits keep tier 2 notifications from burying tier 3 asks.
Be clear about what it does not do. AlertMCP is agent-initiated: the model calls ask because your instructions tell it to. It does not intercept or answer Claude Code's, Cursor's or Codex's built-in permission dialogs; for those, use the vendor-native options above. Other MCP servers cover similar ground, including Pushary and community servers built on ntfy, Telegram and Discord.
Human-in-the-loop in five frameworks
The TypeScript snippets below share one stand-in approval channel, and the Python one uses input(). Swap them for Slack, a web UI or a push service. The TypeScript files are ES modules (they use top-level await); run them with npx tsx.
// ask-human.ts
import { createInterface } from "node:readline/promises";
export async function askHuman(question: string): Promise<boolean> {
const rl = createInterface({ input: process.stdin, output: process.stdout });
const answer = await rl.question(`${question} [y/N] `);
rl.close();
return answer.trim().toLowerCase() === "y";
}
Claude Agent SDK
canUseTool fires whenever Claude wants a tool that no permission rule or mode has already approved. It never fires for auto-approved tools, so use a PreToolUse hook for logic that must see every call.
import { query } from "@anthropic-ai/claude-agent-sdk";
import { askHuman } from "./ask-human.js";
for await (const message of query({
prompt: "Delete the remote branches that were merged more than 90 days ago",
options: {
canUseTool: async (toolName, input) => {
const ok = await askHuman(`Allow ${toolName}: ${JSON.stringify(input)}?`);
return ok
? { behavior: "allow", updatedInput: input }
: { behavior: "deny", message: "The user declined this action." };
},
},
})) {
if ("result" in message) console.log(message.result);
}
OpenAI Agents SDK
Mark a tool with needs_approval (a boolean, or an async callable for input-based rules). Pending calls appear on result.interruptions; approve or reject them on the run state and run again.
import asyncio
from agents import Agent, Runner
from agents.decorators import tool
@tool(needs_approval=True)
def delete_branch(name: str) -> str:
"""Delete a remote git branch."""
return f"deleted {name}" # your real git call goes here
agent = Agent(name="repo-janitor", instructions="Keep the repo tidy.", tools=[delete_branch])
async def main() -> None:
result = await Runner.run(agent, "Delete the branch old-experiment")
while result.interruptions:
state = result.to_state() # state.to_json() persists it for long waits
for item in result.interruptions:
if input(f"Allow {item.name} {item.arguments}? [y/N] ") == "y":
state.approve(item)
else:
state.reject(item, rejection_message="The user declined.")
result = await Runner.run(agent, state)
print(result.final_output)
asyncio.run(main())
Vercel AI SDK
In current versions, approval moved from the deprecated needsApproval tool property to the toolApproval option on generateText, streamText or ToolLoopAgent. The AI SDK docs describe a two-call flow: the first call returns tool-approval-request parts, and you answer with tool-approval-response messages.
import { openai } from "@ai-sdk/openai";
import { generateText, tool, type ModelMessage, type ToolApprovalResponse } from "ai";
import { z } from "zod";
import { askHuman } from "./ask-human.js";
const runCommand = tool({
description: "Run a shell command",
inputSchema: z.object({ command: z.string() }),
execute: async ({ command }) => `ran: ${command}`,
});
const settings = {
model: openai("gpt-5.5"),
tools: { runCommand },
toolApproval: { runCommand: "user-approval" as const },
};
const messages: ModelMessage[] = [{ role: "user", content: "Remove the most recent file in ~/Downloads" }];
const first = await generateText({ ...settings, messages });
messages.push(...first.responseMessages);
const approvals: ToolApprovalResponse[] = [];
for (const part of first.content) {
if (part.type === "tool-approval-request" && !part.isAutomatic) {
const approved = await askHuman(`Run ${JSON.stringify(part.toolCall.input)}?`);
approvals.push({ type: "tool-approval-response", approvalId: part.approvalId, approved });
}
}
messages.push({ role: "tool", content: approvals });
const second = await generateText({ ...settings, messages });
console.log(second.text);
LangGraph
LangGraph pauses inside a node with interrupt() and resumes with Command(resume=...). The core of it fits in a few lines:
from langgraph.types import Command, interrupt
def review(state): # a node in a graph compiled with a checkpointer
decision = interrupt({"question": "Deploy to prod?", "options": ["Reject", "Approve"]})
return {"approved": decision == "Approve"}
# later: graph.invoke(Command(resume="Approve"), {"configurable": {"thread_id": "t1"}})
The node re-runs from the top when it resumes, which catches most people once. The full pattern, with a durable checkpointer and approvals sent to your phone, is in our LangGraph human-in-the-loop tutorial.
MCP with AlertMCP
With MCP, the policy lives in your instructions and the round trip lives in the server. Add the server (Claude Code shown; other clients are in the remote MCP server setup guide):
claude mcp add --transport http alertmcp https://api.alertmcp.com/mcp --header "Authorization: Bearer amcp_sk_…"
Then write your tier 3 list into CLAUDE.md or AGENTS.md:
## Approvals
Before you delete data, run a migration, deploy to production, or send anything outside this repo:
1. Call alertmcp.ask with one question, the exact command or diff in `body`,
and the safest option first, e.g. ["Cancel", "Run it"].
2. Call alertmcp.get_answer with the returned id until the status is no longer "pending".
3. Continue only if the answer is the approving option. Otherwise stop and report.
Audit and accountability
An approval is also a record. Log enough to answer "who allowed this, and what exactly did they see?" six months later.
- Log the full decision. The exact input shown, the approver, the option chosen, the time asked and answered, and the channel. Log timeouts and their outcome too. ASI09 asks for tamper-proof records of user queries and agent actions.
- Authenticate the approver. Only people with the right role should be able to approve a production deploy. A viewer on a shared channel is not an approver.
- Protect paused state. The OpenAI docs warn that serialized run state contains approvals, tool arguments and app context, and that loading it doesn't authenticate the snapshot. Store it like a credential.
- Watch the numbers. If a tier is approved nearly 100% of the time, automate it or improve the question. If rejections cluster on one action, fix the agent's instructions.
Regulation points the same way. Article 14 of the EU AI Act requires high-risk AI systems to be designed so that natural persons can effectively oversee them, including the ability to interrupt the system through a "stop" button or a similar procedure. It applies to high-risk systems only, and after the Digital Omnibus those obligations apply from December 2, 2027 (Annex III systems) and August 2, 2028 (Annex I). Most coding agents are not high-risk systems, but the article is a useful benchmark for any agent that can change production.
Frequently asked questions
- What is human-in-the-loop for AI agents?
- It is a control where the agent pauses before an action and a person approves, edits or rejects it before it runs. It differs from reviewing the agent's output after the fact, because the decision happens before anything changes.
- Which agent actions should require human approval?
- High-impact, irreversible or out-of-scope actions: deleting data, deploying or migrating production, moving money, publishing or sending messages outside the team, and changing permissions or secrets. OWASP's agentic guidance names delete, transfer and publish explicitly.
- Doesn't approving every action make an agent safer?
- Not in practice. Anthropic reports that Claude Code users approve 93 to 97 percent of permission prompts, and OWASP lists overwhelming the human in the loop as a threat. Gate the risky few actions and automate the rest with policy.
- What should happen if nobody answers an approval request?
- Decide before you ship. Sensible choices are to deny or take the safest option, to escalate to another approver, or to stop and report. An unanswered request should never default to the risky action.
- Is human oversight legally required for AI agents?
- The EU AI Act's Article 14 requires human oversight for high-risk AI systems, with those obligations now applying from December 2027 or August 2028 depending on the category. Most coding agents are not high-risk systems, but the article is a useful design benchmark.