Workflows, Hooks, and Sessions: The Control Plane

The control plane around the agent loop: enforcing multi-step workflows with clean handoff, intercepting tool calls with Agent SDK hooks (PreToolUse/PostToolUse and their decisions), and persisting session state so a run can resume and fork.

An agent loop, left to itself, does whatever the model decides at each step. That freedom is the point of the loop, and it’s what lets a coordinator hand a subagent an open-ended job. But freedom is not the same as fitness for production. The moment you run an agent for real, three questions show up that the loop alone doesn’t answer. How do you make sure a required sequence of steps actually happens in order? How do you observe, block, or rewrite what the agent is about to do? And how do you let a run pause, survive a crash, and resume — or branch to try a second approach — without starting over?

Those three questions are the control plane: the machinery around the loop that lets you govern and operate it. This chapter covers three tools — enforcing an ordered workflow, intercepting tool calls with hooks, and persisting a session. They share one idea worth stating first.

Governance belongs in structure, not the prompt

You can always ask the model to behave: validate the input before processing, never write outside the workspace, remember where we left off. Sometimes that is enough. For low-stakes behavior an instruction in the prompt is the cheapest thing that works. But an instruction is advice the model can misread, forget under a full context window, or reason its way around. When a step is mandatory, an action is consequential, or state must survive a restart, you want a guarantee. A guarantee lives in structure outside the model’s reach: a step it literally cannot take out of order, a hook that blocks the call, a session store that holds the conversation whether the process lives or dies. The whole control plane is that one move repeated, turning a hope into a wall. Each of the three mechanisms below is a different wall.

Multi-step workflows with enforcement and handoff

Some tasks have a required order: validate input, then process, then notify, where skipping or reordering a step is a bug. The agentic loop gives the model freedom to choose its next action. That’s exactly what you want for open-ended work, and exactly what you don’t want when a step is mandatory. So workflow enforcement is about constraining that freedom at the points that matter, without turning the whole agent into a rigid script.

Two mechanisms do the enforcing, and you’ll meet both below. Tool availability means a step’s tool isn’t offered until its prerequisite is done. Hooks block or redirect a tool call when it’s out of order. The “handoff” half is the clean transfer of state between stages: one stage’s output becomes the next stage’s input. In a multi-agent design that means the coordinator passing a completed stage’s result into the next subagent’s context. Enforcement should be structural — a step the agent literally cannot take out of order — rather than an instruction in a prompt the model might ignore. It’s the same gate-not-a-suggestion principle that governs any consequential action.

Hooks: intercepting tool calls

Hooks are the Agent SDK’s interception points: callbacks that fire around the agent’s actions so you can observe, block, or modify them without touching the loop. They are the mechanism behind guardrails, audit logging, data redaction, and workflow enforcement alike. They are also the cleanest example of the structure-over-prompt idea, because a hook runs in your code, not the model’s.

The two you’ll reach for most are PreToolUse and PostToolUse. PreToolUse fires before a tool runs — your chance to allow, deny, or rewrite the call. PostToolUse fires after — your chance to inspect or transform the result. claude-agent-sdk 0.2.128 exposes a broad set of hook events: PreToolUse, PostToolUse, PostToolUseFailure, UserPromptSubmit, Stop, SubagentStart, SubagentStop, PreCompact, PermissionRequest, and Notification. Several more are available in the TypeScript SDK — a version-and-language difference worth checking before you rely on a specific one.

You attach hooks via a HookMatcher, which pairs a matcher (which tools to fire on) with the callback(s):

from claude_agent_sdk import ClaudeAgentOptions, HookMatcher

async def block_writes_outside_workspace(input, tool_use_id, context):
    if input["tool_name"] == "Write" and not input["tool_input"]["path"].startswith("/workspace"):
        return {"hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "deny",          # block the call
            "additionalContext": "Writes are restricted to /workspace.",
        }}
    return {}   # return nothing to allow the call unchanged

options = ClaudeAgentOptions(
    hooks={"PreToolUse": [HookMatcher(matcher="Write|Edit", hooks=[block_writes_outside_workspace])]},
)

The matcher is a filter: a pipe-separated list of exact tool names ("Write|Edit"), a regex ("^mcp__" to match all MCP tools), or omitted to match everything. The return value is where the power is. A PreToolUse hook can return a permissionDecision of deny to block the call, an updatedInput to rewrite the arguments before the tool runs (redact a secret, clamp a value), or additionalContext to inject a note for the model. A PostToolUse hook can return updatedToolOutput to transform a result before the model sees it. (A registered PreToolUse hook with a HookMatcher on Write fires when the model attempts a Write and returns a deny decision that blocks the call.)

Hooks are how you enforce policy the model can’t override, because they sit outside the model’s control. A prompt saying “don’t write outside the workspace” is advice. A PreToolUse hook that denies the call is a wall. When an item asks how to guarantee a tool is never called with certain arguments, the hook answer is the right one, and “instruct the model not to” is the trap.

Sessions: state, resumption, and forking

A session is a persisted conversation the SDK can save and reload: the mechanism behind an agent that survives a restart, resumes a paused task, or branches to explore alternatives. Where a hook governs a single action, a session governs the run’s memory, so it can outlive one process. It comes down to three distinct operations.

Capture the session id when a run ends — it’s on the final ResultMessage:

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions

options = ClaudeAgentOptions()

async def main():
    async for message in query(prompt="Start the migration audit.", options=options):
        if type(message).__name__ == "ResultMessage":
            session_id = message.session_id
            print(session_id)

asyncio.run(main())

Resume a specific past session by passing its id — the agent picks up with the full prior context:

from claude_agent_sdk import ClaudeAgentOptions

session_id = "sess_..."   # captured from a prior run's ResultMessage

options = ClaudeAgentOptions(resume=session_id)          # continue this exact session
# or, to continue the most recent session without naming it:
options = ClaudeAgentOptions(continue_conversation=True)

Fork to branch a session — create a new session that starts from a past one’s state, so you can try a different path without disturbing the original:

from claude_agent_sdk import ClaudeAgentOptions

session_id = "sess_..."   # captured from a prior run's ResultMessage

options = ClaudeAgentOptions(resume=session_id, fork_session=True)   # branch, don't overwrite

Resume continues a session in place; fork branches it into a new one. Forking is how you explore two approaches from the same starting state — the same time-travel idea a checkpointed graph gives you. Resume is how you carry one conversation forward. The SDK also ships utilities to manage them: list_sessions(), get_session_messages(), get_session_info(), rename_session(), and tag_session(), all real functions in claude-agent-sdk 0.2.128. (Resuming a session keeps the same session id and recalls a fact set on the first turn; forking recalls the same fact under a new session id, continuing in place versus branching, exactly as described.)

Sessions also underpin reliability. Because the conversation is persisted, a crashed or interrupted agent can resume from its last saved state rather than restarting. That’s the durable-execution property Domain 5 cares about, delivered by the session store.

Final thoughts

The control plane around the loop is three things: workflow enforcement that makes required steps structural rather than advisory; hooks (PreToolUse/PostToolUse and friends, attached by HookMatcher) that intercept tool calls to allow, deny, rewrite, or transform them from outside the model’s control; and sessions that capture, resume, and fork a run’s state. The unifying lesson across all three, and across Domain 1 as a whole, is that governance belongs in structure, not in prompts. A hook denial, a mandatory-tool gate, and a persisted session are guarantees; an instruction to the model is a hope.

That completes Domain 1, the heaviest on the exam. Next we turn to the tools the agent actually calls.

Next: Arc 2 opens with tool interfaces and structured errors — designing tools a model uses correctly, and error responses it can recover from.

Comments