Workflows, Hooks, and Sessions: The Control Plane

Domain 1.4, 1.5, and 1.7: enforcing multi-step workflows with handoff, intercepting tool calls with Agent SDK hooks (PreToolUse/PostToolUse and their decisions), and managing session state, resumption, and forking.

The last three task statements in Domain 1 are the control plane around the loop: enforcing that a workflow’s steps happen in order and hand off cleanly (1.4), intercepting tool calls to inspect or change them (1.5), and managing the session so a run can pause, resume, and branch (1.7). These are what turn a bare agent loop into something you can govern and operate. Built against claude-agent-sdk 0.2.128; hook types and session functions are introspection-verified, live runs marked ⚠️ for the key-based pass.

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. Enforcing that order is Domain 1.4. The agentic loop gives the model freedom to choose its next action, which is 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 (a step’s tool isn’t offered until its prerequisite is done) and hooks (a tool call is blocked or redirected when it’s out of order). The “handoff” half is the clean transfer of state between stages — one stage’s output becoming the next stage’s input, which in a multi-agent design means the coordinator passing a completed stage’s result into the next subagent’s context. The exam-ready point is that 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, 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. This is Domain 1.5, and it’s the mechanism behind guardrails, audit logging, data redaction, and workflow enforcement alike.

The two you’ll reach for most are PreToolUse (fires before a tool runs — your chance to allow, deny, or rewrite the call) and PostToolUse (fires after — your chance to inspect or transform the result). Verified — claude-agent-sdk 0.2.128 exposes a broad set of hook events: PreToolUse, PostToolUse, PostToolUseFailure, UserPromptSubmit, Stop, SubagentStart, SubagentStop, PreCompact, PermissionRequest, and Notification (with several more 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. (⚠️ Hook firing at runtime is pending the live pass; the event types, HookMatcher, and the return-shape fields are introspection-verified.)

The exam framing: 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. This is Domain 1.7, and it’s three distinct operations.

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

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

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

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:

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. That distinction is the exam point: forking is how you explore two approaches from the same starting state (the same time-travel idea that a checkpointed graph gives you), while 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(), tag_session() — verified as real functions in claude-agent-sdk 0.2.128. (⚠️ live resume/fork behavior pending the key pass; the options fields and session functions are introspection-verified.)

Sessions also underpin reliability: because the conversation is persisted, a crashed or interrupted agent can resume from its last saved state rather than restarting — the durable-execution property that 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 exam 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, where 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