Fully autonomous AI agents sound great in demos. In production, theyβre terrifying. An agent that can deploy code, send emails, or modify databases without human approval is one hallucination away from a disaster.
Human-in-the-loop (HITL) patterns give you the best of both worlds: agents handle routine work autonomously, but pause for human approval on high-stakes actions. The trick is knowing where to draw the line.
The autonomy spectrum
Full manual ββββββββββββββββββββββββββββββββΊ Full autonomous
β β β β
Human does Human Human Human
everything approves monitors reviews
every alerts after
action only the fact
Most production agents should sit in the middle: autonomous for low-risk actions, approval-required for high-risk ones.
Pattern 1: Approval gates
Block specific actions until a human approves:
HIGH_RISK_TOOLS = {"deploy", "delete_file", "send_email", "modify_database", "create_pr"}
async def execute_with_approval(tool_name, args, user_id):
if tool_name in HIGH_RISK_TOOLS:
# Request approval
approval_id = await create_approval_request(
user_id=user_id,
action=tool_name,
details=args,
)
# Notify via Slack/email
await notify_user(user_id, f"Agent wants to: {tool_name}\nDetails: {json.dumps(args, indent=2)}\nApprove: /approve {approval_id}")
# Wait for approval (with timeout)
approved = await wait_for_approval(approval_id, timeout=3600)
if not approved:
return {"error": "Action not approved", "action": tool_name}
# Execute the action
return await tools[tool_name](**args)
The OpenAI Agents SDK has built-in human-in-the-loop support. Claude Code implements this natively β it asks for permission before running shell commands or modifying files.
Pattern 2: Confidence thresholds
Let the agent self-assess. High-confidence actions proceed automatically; low-confidence ones pause for review:
async def run_with_confidence_check(agent, message):
# First pass: get the action plan
plan = await Runner.run(planning_agent, f"""
For this request: {message}
What actions would you take?
Rate your confidence (0-100) for each action.
Format: ACTION | CONFIDENCE | REASON
""")
actions = parse_plan(plan.final_output)
for action in actions:
if action.confidence >= 90:
await execute(action) # Auto-approve
elif action.confidence >= 60:
await execute_with_approval(action) # Human approval
else:
await escalate_to_human(action) # Human takes over
This works well for coding agents where formatting changes are high-confidence but architectural decisions are low-confidence.
Pattern 3: Escalation flows
When the agent gets stuck or encounters something unexpected, escalate to a human:
ESCALATION_TRIGGERS = [
"I'm not sure how to proceed",
"This could have unintended consequences",
"I need access to",
"I don't have enough context",
]
async def check_for_escalation(agent_response: str) -> bool:
return any(trigger in agent_response.lower() for trigger in ESCALATION_TRIGGERS)
async def agent_loop(agent, message, user_id):
result = await Runner.run(agent, message)
if await check_for_escalation(result.final_output):
await notify_user(user_id,
f"Agent needs help:\n{result.final_output}\n\nReply with guidance or take over.")
human_input = await wait_for_human_input(user_id, timeout=7200)
if human_input:
result = await Runner.run(agent, human_input, session=result.session)
return result
Pattern 4: Review after the fact
For lower-risk actions, let the agent proceed but flag everything for human review:
async def execute_and_log(tool_name, args, result):
review_item = {
"tool": tool_name,
"args": args,
"result": str(result)[:500],
"timestamp": datetime.utcnow().isoformat(),
"reviewed": False,
}
await db.insert("agent_review_queue", review_item)
# Daily digest of agent actions
# Reviewed by human each morning
This is how our AI Startup Race works β agents act autonomously, but every action is logged and reviewable on the dashboard.
Pattern 5: Staged rollout
New agent capabilities start with full approval, then graduate to autonomous:
CAPABILITY_STAGES = {
"read_file": "autonomous", # Proven safe
"write_file": "autonomous", # Proven safe
"run_tests": "autonomous", # Proven safe
"create_pr": "approval", # Still validating
"deploy": "approval", # Always requires approval
"delete_branch": "disabled", # Not ready yet
}
Promote capabilities from disabled β approval β autonomous as you build confidence. Demote back to approval if issues arise.
Implementing in different frameworks
OpenAI Agents SDK
The SDK has native HITL support:
from agents import Agent, Runner
agent = Agent(
name="Deploy Agent",
instructions="Help deploy applications. Always ask for confirmation before deploying.",
human_in_the_loop=True, # Pauses for approval on tool calls
)
Claude Code
Claude Code asks for permission by default. You can configure auto-approve patterns in .claude/settings.json:
{
"permissions": {
"allow": ["read_file", "search", "list_directory"],
"ask": ["write_file", "shell_command"],
"deny": ["delete_file"]
}
}
Gemini CLI
Gemini CLI subagents can have restricted tool access per-agent, which is an implicit form of HITL β the agent simply canβt perform actions you havenβt authorized.
Choosing the right pattern
| Scenario | Pattern | Why |
|---|---|---|
| Deploying to production | Approval gate | Too risky for auto |
| Code formatting | Autonomous | Low risk, high volume |
| Sending customer emails | Approval gate | Reputation risk |
| Reading log files | Autonomous | Read-only, no risk |
| Database migrations | Approval gate + review | Irreversible |
| Creating GitHub issues | Confidence threshold | Usually fine, sometimes wrong |
| Deleting resources | Always approval | Irreversible |
The rule of thumb: if the action is reversible and low-impact, automate it. If itβs irreversible or high-impact, require approval.
Related: AI Agent Security Β· When NOT to Use AI Agents Β· Agent vs Workflow Β· How to Debug AI Agents Β· Test AI Agents Before Production Β· AI Agent Error Handling Β· Claude Code Routines