Chapter 11: Steering a Session — Interrupting, Redirecting, and Approving Tool Use
By the end of this chapter, you will know how to take control of a running session — stop it, redirect it, approve or deny tool calls, and guide it toward better results mid-task.
The Big Idea
A well-defined task with a good system prompt should let an agent run to completion without intervention. That's the goal. But real work isn't always that clean: the agent takes an approach you didn't anticipate, or you realize mid-task that the task definition needs to change, or a tool call is about to do something you want to review first.
Steering is the set of controls that let you intervene when you need to. Not as a crutch — as a precision instrument.
Claude Managed Agents gives you four steering mechanisms:
- Sending follow-up messages — add instructions, provide context, change direction
- Interrupting — stop current work cleanly
- Approving or denying tool calls — gate specific actions with human judgment
- Redirecting after a pause — wait for
session.status_idleand then send a new instruction
Each serves a different purpose. Understanding when to use each one is what separates effective agents from frustrating ones.
The Analogy
Think of steering a session like managing a contractor working on a project at your office.
The follow-up message is leaving a note on their desk while they're working: "By the way, also check whether the report includes Q4 data." They pick it up at a natural break and incorporate it.
The interrupt is the tap on the shoulder: "Stop what you're doing for a moment — I need to redirect you." Clean, immediate, causes them to finish the current micro-step and check in.
Approving or denying tool calls is the approval workflow for high-impact actions: "Before you send that email to the client, show me the draft first." They can't proceed until you've reviewed it.
Redirecting after a pause is catching them when they step back and check in between tasks: they've finished one piece of work, they're asking what's next, and you give them a different direction than originally planned.
Good contractors are autonomous and don't need constant direction. But good project management means having these controls ready — and using them precisely when needed, not constantly.
How It Actually Works
Sending Follow-up Messages
The simplest form of steering: send another user.message to an ongoing session. You can do this at any point when the session is in idle status (after finishing a previous task or between tool calls that paused for confirmation).
# First task
client.beta.sessions.events.send(
session.id,
events=[{"type": "user.message", "content": [{"type": "text", "text": "Analyze the sales data in data.csv."}]}],
)
# Wait for idle, then add follow-up context
# (After session.status_idle with end_turn)
client.beta.sessions.events.send(
session.id,
events=[{"type": "user.message", "content": [{"type": "text", "text": "Also check whether Q4 data is included and note if it's missing."}]}],
)
The agent treats each follow-up user.message as a continuation of the same working session, with access to everything that happened before. This is the most common form of steering.
Interrupting a Running Session
When you need to stop the agent before it finishes its current work, send a user.interrupt event:
client.beta.sessions.events.send(
session.id,
events=[
{
"type": "user.interrupt",
},
],
)
The session transitions to idle status after the interrupt is processed. The agent stops mid-task and waits. The event log preserves everything that happened up to the interrupt.
After interrupting, you can:
- Send a new
user.messageto give the agent a different direction - Archive the session if you no longer need it
- Delete the session (now possible since it's no longer running)
When to interrupt:
- The agent is heading in a direction that doesn't serve the goal
- You realized the task definition was wrong and need to start fresh
- You need to update the task scope mid-execution
- A tool call pattern you're observing is concerning and you want to review before allowing further work
Approving Tool Calls
As covered in Chapter 8, tools with always_ask policy pause the session and wait for your confirmation. The steering aspect is: this pause is your chance to actively direct the agent's next step.
When you receive a requires_action stop, you're not just approving or denying — you're steering. The deny_message field lets you give specific instructions:
# Deny and redirect with specific instructions
client.beta.sessions.events.send(
session.id,
events=[
{
"type": "user.tool_confirmation",
"tool_use_id": mcp_tool_use_event.id,
"result": "deny",
"deny_message": "Don't create issues in the production project. Use the staging project.",
},
],
)
Claude receives your denial message and adjusts. It doesn't stop entirely — it incorporates your feedback and tries a different approach. A well-written deny_message is a steering instruction, not a hard stop.
The Requires-Action Flow
The full flow when a tool confirmation is needed:
- Agent tries to use a tool with
always_askpolicy - Session emits
agent.tool_useevent - Session pauses with
session.status_idle→stop_reason: requires_action stop_reason.requires_action.event_idscontains the IDs of blocking events- You review the
agent.tool_useevent to understand what the agent wants to do - You send
user.tool_confirmationwithresult: "allow"orresult: "deny"(+ optionaldeny_message) - Session transitions back to
running
This flow is documented in both the permission policies docs and the session event stream docs.
Building a Human-in-the-Loop Approval Interface
For applications where a human reviews tool calls before approving, the event stream gives you everything you need:
with client.beta.sessions.events.stream(session.id) as stream:
for event in stream:
if event.type == "session.status_idle" and (stop := event.stop_reason):
match stop.type:
case "requires_action":
for event_id in stop.event_ids:
# Approve the pending tool call
client.beta.sessions.events.send(
session.id,
events=[
{
"type": "user.tool_confirmation",
"tool_use_id": event_id,
"result": "allow",
},
],
)
case "end_turn":
break
In a production human-in-the-loop interface, you'd replace the automatic approval with something that surfaces the pending action to a human reviewer — a web UI, a Slack notification, an email — and waits for their input before sending the confirmation.
Redirecting After Idle
When a session reaches idle with end_turn, the agent has finished its current task and is waiting for what comes next. This is the cleanest steering point: no work in progress, clean slate for a new direction.
# After receiving session.status_idle with end_turn
# Send a redirect message
client.beta.sessions.events.send(
session.id,
events=[
{
"type": "user.message",
"content": [
{
"type": "text",
"text": "Good work on the analysis. Now take those findings and create an executive summary in a separate file — 3 paragraphs maximum.",
},
],
},
],
)
The agent picks up with full context from the previous work. It knows what it analyzed, what it found, and what tools it used. Your redirect message adds to that context and gives a new direction.
Session State Machine: Where Steering Actions Apply
The four statuses and when each steering action is available:
| Status | Send message? | Interrupt? | Confirm tool? |
|---|---|---|---|
idle (end_turn) |
Yes | No (not running) | No |
idle (requires_action) |
No | Yes | Yes |
running |
Yes | Yes | No (not paused) |
rescheduling |
No | No | No (recovering) |
terminated |
No | No | No |
Try It Yourself
Build an interactive approval loop. Create an agent with
bashonalways_ask. Give it a coding task that requires multiple bash commands. Build an event handler that prompts you in the terminal before approving each bash call:with client.beta.sessions.events.stream(session.id) as stream: client.beta.sessions.events.send(session.id, events=[...]) # your task for event in stream: if event.type == "agent.tool_use": if event.name == "bash": print(f"\nBash command requested: {event.input}") elif event.type == "session.status_idle": stop = event.stop_reason if stop and stop.type == "requires_action": for event_id in stop.event_ids: decision = input("Allow this bash command? (y/n/redirect): ") if decision == "y": client.beta.sessions.events.send(session.id, events=[ {"type": "user.tool_confirmation", "tool_use_id": event_id, "result": "allow"} ]) elif decision == "n": msg = input("Why? (your message becomes the deny_message): ") client.beta.sessions.events.send(session.id, events=[ {"type": "user.tool_confirmation", "tool_use_id": event_id, "result": "deny", "deny_message": msg} ]) elif stop and stop.type == "end_turn": breakTest an interrupt. Let a session start running on a long task, then interrupt it after a few seconds:
import time # Send a long task client.beta.sessions.events.send(session.id, events=[...]) time.sleep(3) # Let it start # Interrupt client.beta.sessions.events.send(session.id, events=[{"type": "user.interrupt"}])Observe the session status change. Then send a redirect message with a different (shorter) task.
Practice multi-turn redirection. Send an initial task to a session, let it complete, then send a follow-up that builds on the output. Then send a third message asking it to revise something. Observe how the agent uses the full history to handle each subsequent instruction.
Examine the event log after steering. After a session with interrupts and tool confirmations, list all events:
for event in client.beta.sessions.events.list(session.id): print(f"{event.type} | {event.processed_at}")Find your
user.interruptanduser.tool_confirmationevents in the log. Notice how they sit alongside the agent events, showing exactly when you intervened.
Common Pitfalls
Sending messages to a running session and expecting immediate acknowledgment. When a session is
running, sending auser.messagequeues the message for when the current turn completes. The agent won't immediately stop and read your message. If you need to change direction immediately, interrupt first.Writing vague denial messages. "No" is not a useful denial. "No — instead of creating a new file, append this content to the existing report.md" gives Claude actionable direction. Every denial should include a constructive redirect.
Interrupting too aggressively during early testing. If you interrupt every time the agent does something unexpected, you won't discover its natural problem-solving patterns. Let sessions run to completion during prototyping, study the event log, then improve your system prompt. Reserve interrupts for actual production issues.
Not building an explicit
requires_actionhandler. If your event loop doesn't handlerequires_action— it only checks forend_turn— your session will sit waiting indefinitely whenever a tool confirmation is needed. Every event loop needs both cases.Assuming the agent doesn't use context from previous turns. When you redirect after
end_turn, the agent has full access to everything that happened in the session. References to "what you just found" or "those files you analyzed" work. The agent knows what it did.
Toolkit
Human-in-the-Loop Event Handler Template — A complete Python script for interactive sessions: shows pending tool calls in a formatted preview, prompts for approve/deny/redirect, handles both custom tools and permission confirmations, and logs all decisions.
Steering Decision Flowchart — A one-page decision tree: "Is the agent running? → Interrupt or follow-up? → Is it paused? → Requires action type? → Which confirmation event?"
Chapter Recap
- Steering mechanisms: send follow-up messages (while idle), interrupt (while running), approve/deny tool calls (when
requires_action), redirect afterend_turn. Each suits a different situation. - The
user.interruptevent stops current work cleanly and transitions the session toidle. After interrupting, you can send a new message, archive the session, or close it. - Always implement both
end_turnandrequires_actionbranches in your event loop — missing either causes real operational problems. And always include a constructivedeny_messagewhen denying a tool call: Claude uses it to find a better path forward.