Skip to content

Interventions

Interventions are a composable control layer for agents. They provide a typed action model for common control concerns — authorization, guardrails, steering, and content transformation — with ordered evaluation and short-circuiting. Unlike raw hooks and plugins which mutate event objects directly, intervention handlers return typed decisions (proceed, deny, guide, confirm, transform) that the framework applies with well-defined semantics — enabling automatic short-circuiting, feedback accumulation, and conflict resolution.

Create an intervention handler by extending InterventionHandler and overriding the lifecycle methods you need. Register handlers via the interventions option in agent configuration:

from strands import Agent
from strands.interventions import Deny, InterventionHandler, Proceed
class ToolGuard(InterventionHandler):
name = "tool-guard"
def __init__(self, blocked_tools: list[str]):
self.blocked_tools = blocked_tools
def before_tool_call(self, event: BeforeToolCallEvent):
if event.tool_use["name"] in self.blocked_tools:
name = event.tool_use["name"]
return Deny(
reason=f"Tool '{name}' is not allowed"
)
return Proceed()
agent = Agent(
tools=[search, delete_file],
interventions=[ToolGuard(blocked_tools=["delete_file"])],
)
# The agent can search freely, but any attempt to call delete_file
# is blocked before execution — the model sees the denial reason
# and adjusts its approach
agent("Clean up the temp directory")

Handlers only need to override the lifecycle methods relevant to their concern — all methods default to Proceed() proceed() .

Each lifecycle method returns one of five typed actions:

ActionClassDescription
ProceedProceed()Allow the operation to continue unchanged
DenyDeny(reason="...")Block the operation. Short-circuits remaining handlers
GuideGuide(feedback="...")Cancel and provide feedback for the model to retry with
ConfirmConfirm(prompt="...")Pause for human approval
TransformTransform(apply=fn)Modify event content in-place before execution continues

The following examples show each action type in a realistic handler:

from strands.interventions import (
Confirm, Deny, Guide, InterventionHandler,
Proceed, Transform,
)
# Deny — block tool calls that access production resources
class EnvironmentGuard(InterventionHandler):
name = "environment-guard"
def before_tool_call(self, event: BeforeToolCallEvent):
tool_input = event.tool_use.get("input", {})
if "prod" in tool_input.get("database", ""):
return Deny(reason="Production database access is not allowed")
return Proceed()
# Guide — steer the model when it tries to send emails without a subject
class EmailValidator(InterventionHandler):
name = "email-validator"
def before_tool_call(self, event: BeforeToolCallEvent):
if event.tool_use["name"] == "send_email":
tool_input = event.tool_use.get("input", {})
if not tool_input.get("subject"):
return Guide(feedback="All emails must include a subject line.")
return Proceed()
# Confirm — require human approval before deleting files
class DeleteApproval(InterventionHandler):
name = "delete-approval"
def before_tool_call(self, event: BeforeToolCallEvent):
if event.tool_use["name"] == "delete_file":
tool_input = event.tool_use.get("input", {})
return Confirm(prompt=f"Approve deleting \"{tool_input.get('path')}\"?")
return Proceed()
# Transform — redact PII from outgoing email bodies
class PiiRedactor(InterventionHandler):
name = "pii-redactor"
def before_tool_call(self, event: BeforeToolCallEvent):
if event.tool_use["name"] == "send_email":
import re
def redact(e: BeforeToolCallEvent):
tool_input = e.tool_use.get("input", {})
body = tool_input.get("body", "")
ssn_pattern = r"\b\d{3}-\d{2}-\d{4}\b"
tool_input["body"] = re.sub(
ssn_pattern, "[REDACTED]", body
)
return Transform(apply=redact)
return Proceed()

Intervention handlers can override five lifecycle methods. Each method supports a specific subset of actions:

MethodValid ActionsWhen it Runs
before_invocation beforeInvocation Proceed, Deny, Guide, TransformBefore the agent loop starts
before_tool_call beforeToolCall Proceed, Deny, Guide, Confirm, TransformBefore each tool execution
after_tool_call afterToolCall Proceed, TransformAfter each tool execution
before_model_call beforeModelCall Proceed, Deny, Guide, TransformBefore each model API call
after_model_call afterModelCall Proceed, Guide, TransformAfter each model response

How actions behave depends on the lifecycle method:

ActionBefore eventsAfter events
DenySets event.cancel, short-circuits remaining handlersNo effect (warns at runtime)
GuideOn before_tool_call beforeToolCall / before_invocation beforeInvocation : cancels with accumulated feedback. On before_model_call beforeModelCall : injects feedback as user messageInjects feedback and retries
ConfirmPauses agent via interrupt/resume for human approval; denied responses set event.cancelNot supported
TransformCalls action.apply(event) — later handlers see modified contentCalls action.apply(event)

On after_model_call afterModelCall , Guide triggers a model retry. Handlers must ensure convergence (e.g., by tracking retry count and escalating to Deny after repeated failures). The framework imposes no retry cap on guide-triggered retries.

Handlers evaluate in registration order. If any handler returns Deny, remaining handlers are skipped — the operation is blocked immediately. This enables efficient pipelines where fast checks (like authorization) run first and prevent expensive evaluations (like LLM-based steering) from running unnecessarily.

from strands import Agent
from strands.interventions import Deny, Guide, InterventionHandler, Proceed
class RateLimiter(InterventionHandler):
name = "rate-limiter"
def __init__(self):
self.call_count = 0
def before_tool_call(self, event: BeforeToolCallEvent):
self.call_count += 1
if self.call_count > 10:
# Deny short-circuits: handlers registered after this one are skipped
return Deny(reason="Rate limit exceeded")
return Proceed()
class ToneSteering(InterventionHandler):
name = "tone-steering"
def after_model_call(self, event: AfterModelCallEvent):
# This handler never runs for denied tool calls
return Guide(feedback="Use a more professional tone.")
# Handlers evaluate in registration order
agent = Agent(
tools=[search],
interventions=[
RateLimiter(), # Evaluates first
ToneSteering(), # Skipped if RateLimiter denies
],
)

For Guide actions, all handlers continue to run and their feedback is accumulated — the model receives combined guidance from all guiding handlers.

The on_error onError property controls what happens when a handler throws an exception:

ValueBehavior
'throw'Rethrow the error (default). The invocation fails.
'proceed'Log the error and continue as if Proceed() was returned.
'deny'Log the error and treat it as a Deny (fail-closed).
from strands.interventions import Deny, InterventionHandler, OnError, Proceed
# 'proceed' — if this handler throws, continue as if Proceed() was returned
class BestEffortLogger(InterventionHandler):
name = "best-effort-logger"
@property
def on_error(self) -> OnError:
return "proceed"
def before_tool_call(self, event: BeforeToolCallEvent):
# If the logging service is unreachable, the agent continues normally
print(f"Tool called: {event.tool_use['name']}")
return Proceed()
# 'deny' — if this handler throws, treat it as a Deny (fail-closed)
class StrictAuth(InterventionHandler):
name = "strict-auth"
@property
def on_error(self) -> OnError:
return "deny"
def before_tool_call(self, event: BeforeToolCallEvent):
# If the auth service is down (throws), the operation is denied
if not self._check_permission(event.tool_use["name"]):
return Deny(reason="Unauthorized")
return Proceed()
def _check_permission(self, tool_name: str) -> bool:
# ... call external auth service
return True
# 'throw' (default) — errors propagate and fail the invocation
class CriticalValidator(InterventionHandler):
name = "critical-validator"
# on_error defaults to 'throw'
def before_tool_call(self, event: BeforeToolCallEvent):
# If this throws, the entire invocation fails
return Proceed()

Use 'deny' for security-critical handlers where a failure should block execution. Use 'proceed' for non-critical handlers like logging where availability is more important than enforcement.

The Confirm action is only supported on before_tool_call beforeToolCall . It integrates with the SDK’s interrupt/resume system to pause for human approval before a tool executes.

Confirm supports two modes depending on whether response is provided:

  • With response: the value is passed directly to the evaluate function — the agent never pauses.
  • Without response: breaks out of the agent loop to pause for external resume via the interrupt system.

The evaluate function determines whether the response counts as approval. The default accepts True, "y", or "yes" (case-insensitive).

from strands.interventions import Confirm, InterventionHandler, Proceed
class SensitiveToolApproval(InterventionHandler):
name = "sensitive-tool-approval"
def before_tool_call(self, event: BeforeToolCallEvent):
if event.tool_use["name"] in ("delete_file", "send_email"):
return Confirm(
prompt=f"Allow {event.tool_use['name']}?"
)
return Proceed()
# Preemptive approval — agent doesn't pause
class AutoApprove(InterventionHandler):
name = "auto-approve"
def before_tool_call(self, event: BeforeToolCallEvent):
if event.tool_use["name"] == "search":
return Confirm(
prompt="Allow search?",
response="yes",
)
return Proceed()

Interventions are built on top of the hooks system — under the hood, each lifecycle method registers a hook callback. The difference is in how they communicate with the framework.

Hooks and plugins mutate event properties directly (e.g., setting event.cancel = "reason"). The framework doesn’t know why something was cancelled — was it a hard authorization denial or soft guidance to retry differently? Multiple plugins modifying the same event can conflict silently with last-write-wins semantics.

Interventions return typed actions that the framework interprets. This enables:

  • Short-circuiting — a Deny from an authorization handler skips all remaining handlers automatically. With hooks, each plugin must independently check event.cancel before doing work.
  • Feedback accumulation — multiple handlers can return Guide and their feedback is combined into a single message to the model, rather than overwriting each other.
  • Human-in-the-loopConfirm integrates with the SDK’s interrupt/resume system to pause for approval without the handler needing to manage interrupt lifecycle.
  • Ordered evaluation — handlers always run in registration order with well-defined precedence (deny > confirm > guide > transform > proceed).
  • Error policies — each handler declares its own failure mode via on_error onError . A logging handler can use 'proceed' (skip on failure), while an auth handler can use 'deny' (fail closed). Hooks have no equivalent — a thrown error always propagates.
  • Steering — LLM-based contextual guidance using the steering handler
  • Human in the Loop — Ready-to-use intervention handler for tool approval workflows
  • Hooks — Low-level event callbacks for observing and modifying agent behavior
  • Plugins — Bundle related hooks and tools into reusable modules
  • Interrupts — The interrupt/resume system that Confirm builds on