Skip to content

Human in the loop

Pause the agent before it acts and let a person approve, edit, or reject a tool call. The HumanInTheLoop intervention handler adds this oversight without custom interrupt logic: pass it to interventions and choose how to collect the human’s response.

The handler uses the confirm action to pause for human input. It builds on the SDK’s interrupt mechanism but abstracts away the manual interrupt/resume loop when you provide an ask option.

flowchart LR
A[Tool call] --> B{Allowed?}
B -->|Yes| C[Execute]
B -->|No| D{Trusted?}
D -->|Yes| C
D -->|No| G{Classifier?}
G -->|Safe| C
G -->|Risky/None| E{Human approves?}
E -->|Yes| C
E -->|No| F[Cancel]

Choose how to collect the human’s response:

ModeWhen to use
Interrupt/resume (default)Stateless deployments that persist state between requests
"stdio"CLI applications where the user types at a terminal
Custom callbackWeb UIs, Slack bots, or other async interfaces

Without an ask option, the handler raises an interrupt and the agent pauses. The caller presents the interrupt to the user, collects their response, and resumes the agent. This is the same interrupt/resume pattern used throughout the SDK. For stateless deployments, combine with a session manager to persist state between requests.

agent = Agent(
tools=[delete_files],
interventions=[HumanInTheLoop()],
)
# Agent pauses with stop_reason 'interrupt' when a tool needs approval
result = agent("Delete the temp files")
if result.stop_reason == "interrupt":
# Present the interrupt to the user (web UI, Slack, etc.)
print(result.interrupts[0].reason)
# Resume with the human's response
result = agent(
[
{
"interruptResponse": {
"interruptId": result.interrupts[0].id,
"response": "yes", # 'y', 'yes', or True -> approved
}
}
]
)

For CLI applications, pass ask="stdio"ask: 'stdio' to prompt the user inline via stdin. The agent blocks until the user responds, so no interrupt handling is needed on the caller side.

agent = Agent(
tools=[delete_files],
interventions=[HumanInTheLoop(ask="stdio")],
)
agent("Delete the temp files")
# Terminal prompt:
# Tool "delete_files" requires human approval. Input: {...} (y/n):

For web UIs, Slack bots, or other custom interfaces, pass a function to ask. The function receives a prompt string describing the tool call and must return the user’s response.

async def ask(prompt: str) -> str:
# Your UI: Slack DM, web modal, push notification, etc.
return await ask_user_via_slack(prompt)
agent = Agent(
tools=[delete_files],
interventions=[HumanInTheLoop(ask=ask)],
)
agent("Delete the temp files")

Use the classifier option to let an LLM decide per-call whether a tool needs approval. Tools in allowed_tools bypass the classifier entirely (fast path for known-safe tools).

agent = Agent(
tools=[read_file, delete_files],
interventions=[
HumanInTheLoop(
allowed_tools=["read_file"],
classifier=True,
),
],
)
agent("Read config.json then delete /tmp/old-logs")
# read_file: allowed (bypasses classifier)
# delete_files: classifier evaluates -> risky -> prompts human

You can customize the classifier’s system prompt or model, or provide your own classification function:

# Custom system prompt and model
agent = Agent(
tools=[delete_files],
interventions=[
HumanInTheLoop(
classifier=LLMClassifierConfig(
system_prompt="Only flag destructive operations.",
),
),
],
)
# Or provide your own classification function
def my_classifier(event, **kwargs):
from strands.vended_interventions.hitl.classifier import (
ClassifierResult,
)
is_dangerous = event.tool_use["name"].startswith("delete")
return ClassifierResult(
requires_human_in_the_loop=is_dangerous,
reason="destructive tool" if is_dangerous else None,
)
agent = Agent(
tools=[delete_files],
interventions=[HumanInTheLoop(classifier=my_classifier)],
)
ParameterTypeDefaultDescription
allowed_toolsallowedToolslist[str]string[]NoneundefinedTools that bypass approval. Supports "*" (all) and "!tool_name" (negation).
classifierbool, LLMClassifierConfig, or callableboolean, LlmClassifierConfig, or functionNoneundefinedLLM risk classifier. True for built-in defaults, config object for custom prompt/model, or a custom function.
enable_trustenableTrustboolbooleanFalsefalseWhen enabled, trust responses are remembered for the session.
evaluate_trustevaluateTrustFunctionAccepts "t" or "trust"Custom validator for trust responses. Only evaluated when trust is enabled.
evaluateFunctionAccepts True, "y", or "yes"true, 'y', or 'yes'Custom validator for approval responses.
askFunction or "stdio"Function or 'stdio'NoneundefinedPass a function for custom UIs, "stdio" for CLI prompting, or omit for interrupt/resume.

Use the allowed tools list to skip approval for safe, read-only tools:

agent = Agent(
tools=[read_file, delete_files],
interventions=[
HumanInTheLoop(
ask="stdio",
# Pattern syntax:
# "read_file" -> runs without approval
# "*" -> all tools run freely (disables handler)
# ["*", "!delete_files"] -> all except delete_files
allowed_tools=["read_file"],
),
],
)
agent("Read config.json then delete /tmp/old-logs")
# Only delete_files prompts; read_file executes immediately

When trust is enabled, a human can respond with 't' or 'trust' to approve the current tool call AND remember that decision for the rest of the session. Subsequent calls to the same tool skip the prompt entirely. Trust works in all modes: interrupt/resume, stdio, and custom callbacks.

Trust state is stored in agent.stateagent.appState and persists across turns within a session but resets when the agent is re-created. Negated tools ("!tool_name") cannot be trusted and always prompt.

agent = Agent(
tools=[delete_files],
interventions=[
HumanInTheLoop(
ask="stdio",
enable_trust=True,
),
],
)
agent("Delete all log files in /tmp")
# First call: user responds 't' -> approved AND remembered
# Subsequent calls: no prompt needed for the session

By default, the handler accepts True, "y", or "yes"true, 'y', or 'yes' as approval. Use evaluate to define your own approval logic, for example requiring the user to type “confirm”:

agent = Agent(
tools=[delete_files],
interventions=[
HumanInTheLoop(
ask="stdio",
# Only approve if the user types "confirm"
evaluate=lambda response: isinstance(response, str) and response.lower() == "confirm",
),
],
)
agent("Delete the temp files")
# Prompt: Tool "delete_files" requires human approval. Input: {...}
# User must type "confirm" to approve (not just "y" or "yes")

Use HumanInTheLoop when you want tool-level approval gating with minimal code: it handles allow-lists, trust, and collection mode out of the box. Use raw interrupts when you need full control: custom interrupt shapes, multi-step interactions, or workflows beyond simple approve/deny.

  • Interventions: The intervention handler framework that HITL is built on
  • Interrupts: Low-level interrupt/resume mechanism
  • Agent State: How trust decisions persist via agent.stateagent.appState
  • Session Management: Persisting interrupt state across sessions