Chapter 11 of 14 · Part 3: Run Your Agent

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:

  1. Sending follow-up messages — add instructions, provide context, change direction
  2. Interrupting — stop current work cleanly
  3. Approving or denying tool calls — gate specific actions with human judgment
  4. Redirecting after a pause — wait for session.status_idle and then send a new instruction

Each serves a different purpose. Understanding when to use each one is what separates effective agents from frustrating ones.

DiagramControl panel illustration with four labeled controls. (1) "Add Instructions" — a message bubble with + icon. (2) "Interrupt" — a stop/pause button, red. (3) "Approve/Deny" — a toggle switch with thumbs up/down. (4) "Redirect" — an arrow curving around an obstacle. Each control has a brief one-line caption explaining when to use it.

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.

DiagramOffice contractor scenario. Four small panels: (1) Note on desk (follow-up). (2) Shoulder tap (interrupt). (3) Manager reviewing a document before signing off (approval). (4) Morning standup redirecting to new priority (redirect). Each panel minimal and iconic.

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.message to 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.",
        },
    ],
)

(Permission policies)

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:

  1. Agent tries to use a tool with always_ask policy
  2. Session emits agent.tool_use event
  3. Session pauses with session.status_idlestop_reason: requires_action
  4. stop_reason.requires_action.event_ids contains the IDs of blocking events
  5. You review the agent.tool_use event to understand what the agent wants to do
  6. You send user.tool_confirmation with result: "allow" or result: "deny" (+ optional deny_message)
  7. 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

(Session event stream)

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
DiagramState machine diagram with five states (idle/end_turn, idle/requires_action, running, rescheduling, terminated). Directed arrows between states with labeled transitions: "user.message → running," "completes → idle/end_turn," "always_ask tool → idle/requires_action," "user.tool_confirmation → running," "user.interrupt → idle/end_turn," "transient error → rescheduling," "unrecoverable → terminated." Color-coded: green for active states, yellow for rescheduling, red for terminated.

Try it yourself

Try It Yourself

  1. Build an interactive approval loop. Create an agent with bash on always_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":
                    break
    
  2. Test 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.

  3. 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.

  4. 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.interrupt and user.tool_confirmation events in the log. Notice how they sit alongside the agent events, showing exactly when you intervened.

DiagramAnnotated event log from a steered session. Timeline showing: user.message → agent.tool_use → [user.tool_confirmation allow] → agent.tool_use → [user.tool_confirmation deny + message] → agent.message (revised approach) → session.status_idle (end_turn) → user.message (redirect) → running again. Highlight boxes at each steering point.

Common pitfalls

Common Pitfalls

  • Sending messages to a running session and expecting immediate acknowledgment. When a session is running, sending a user.message queues 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_action handler. If your event loop doesn't handle requires_action — it only checks for end_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

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 after end_turn. Each suits a different situation.
  • The user.interrupt event stops current work cleanly and transitions the session to idle. After interrupting, you can send a new message, archive the session, or close it.
  • Always implement both end_turn and requires_action branches in your event loop — missing either causes real operational problems. And always include a constructive deny_message when denying a tool call: Claude uses it to find a better path forward.