AnthropicClaude Certified Architect – Foundations2026-101.5 Apply Agent SDK hooks for tool call interception and data normalization
Normalize results without breaking tool shapes
Completed by the knowledge check
Learning objective
Normalize heterogeneous tool results in PostToolUse with shape-compatible updatedToolOutput before Claude reads them.
Core idea
A research agent that combines several Model Context Protocol (MCP) tools should not have to reinterpret each provider’s timestamp units and status codes on every turn. Put deterministic conversion in the result path, then give Claude a representation it can compare consistently. In the Claude Agent SDK, PostToolUse can replace a completed tool’s output through updatedToolOutput before that output reaches Claude.
Preserve the tool’s output contract while normalizing the data inside it. A consistent business record and a valid tool-result envelope solve different problems. Flattening every result into an arbitrary string can discard error evidence or produce a replacement the runtime rejects.
This is a read-side transformation. The tool has already executed. For preventing a refund or other side effect, use PreToolUse; see 1.5.1. A normalized result cannot undo the operation that produced it.
How it works
Intercept the completed result
Both Python and TypeScript Agent SDKs support PostToolUse callbacks. Register the callback in hooks under PostToolUse and use matcher to select the relevant tool names. For MCP tools, names follow mcp__<server>__<tool>. The illustrative research tools mcp__archive__lookup and mcp__registry__lookup are application-defined examples. A matcher containing a character other than letters, digits, _, -, |, , or spaces is an unanchored regular expression, so mcp__archive__.* also covers a tool the archive server adds later; an exact name does not.
The input contains tool_name, tool_input, and tool_response. In the TypeScript SDK, PostToolUseHookInput.tool_response is unknown, because its shape depends on the tool. Validate the result against the selected adapter’s contract before extracting a field. Do not assume an object cast proves that a timestamp or status is present.
Return the replacement under hookSpecificOutput, alongside hookEventName: "PostToolUse". Python hook output uses these same camelCase field names: updatedToolOutput is not spelled updated_tool_output. The Python and TypeScript reference pages both define it.
Preserve the envelope and change the payload
The replacement must fit the output shape of the tool you intercepted. Built-in tools use structured output objects. For example, the documented Bash result includes stdout, stderr, interrupted, and isImage. Replacing that object with a plain string does not satisfy its output schema.
For built-in tools, a replacement that fails schema validation is ignored and the original output is used. MCP output is passed through without that schema validation. That difference makes application validation more important for MCP, not less: the runtime accepting a value does not establish that the value preserves your tool contract.
For an application-owned MCP tool whose documented contract uses text content to carry a JSON record, normalize the JSON within that text content and preserve the surrounding result shape. Keep error markers and other meaningful fields. If the tool exposes the record in multiple representations, update them consistently rather than leaving contradictory copies.
The return envelope is the same regardless of the adapter’s domain fields. This illustrative callback fragment assumes normalizedResult has already been validated against the intercepted tool’s output contract:
return {
hookSpecificOutput: {
hookEventName: "PostToolUse",
updatedToolOutput: normalizedResult,
},
};This fragment shows the SDK interface, not a complete normalization implementation. normalizedResult is the application’s replacement value; it is not an SDK-provided variable or a universally valid MCP envelope.
Define conversion by source contract
Suppose the archive tool emits a Unix timestamp in seconds and a provider-specific numeric status, while the registry tool emits an ISO 8601 timestamp with an offset and a different status vocabulary. The application chooses a canonical payload with observed_at, status, and source. Those are illustrative business fields, not Agent SDK fields.
A source-specific adapter converts the archive’s seconds to an instant and serializes it in the chosen UTC format. Another adapter parses the registry’s offset-bearing timestamp and serializes the same instant format. Keep the source identifier so Claude can attribute the evidence after both records have the same visible shape.
Do not infer seconds versus milliseconds from a convenient guess. Require the tool’s contract to state the unit. Likewise, a timestamp without an offset cannot establish a unique instant unless the source contract supplies a timezone. Mark that record as unusable for time comparison instead of silently inventing one.
Map numeric statuses using the provider’s documented meaning. A code that means “record found” for one source might represent an unrelated state elsewhere. In this scenario, the adapters map each known source value to the application’s vocabulary, such as available or unavailable. Preserve an unknown value as unknown; do not convert every unfamiliar code into success or absence.
These conversion rules are application choices. Anthropic documents where the replacement is delivered, not the semantics of your providers’ dates or codes. Define and test those semantics from the tool contracts you own before relying on the normalized representation for a decision.
Separate replacement, annotation, and failure handling
additionalContext appends information alongside a result; it does not replace the result. Use it to explain that a source value is ambiguous, but use updatedToolOutput when the desired outcome is the normalized result itself.
A top-level decision: "block" in PostToolUse adds feedback beside the result. It does not hide the original output or reverse the completed call. The updatedMCPToolOutput field is restricted to MCP tools; prefer updatedToolOutput, which covers both MCP and built-in tools.
A failed tool execution has its own event, PostToolUseFailure. Its event-specific output accepts additionalContext to annotate the failure, but has no output-replacement field. Do not route failures through a normalizer that manufactures a successful record. Preserve the evidence needed to distinguish “the source reports no record” from “the lookup failed.”
The hook must return before Claude proceeds if the replacement is required. A background asynchronous hook output cannot modify the operation. If a PostToolUse callback times out, the runtime keeps the tool result and continues; see 1.5.1 for the default timeout and its configuration. Therefore, this hook alone is not a guarantee that unnormalized or sensitive data never reaches Claude. Test the failure path and use a tool implementation that returns the required representation directly when that contract must hold even without a successful hook.
The replacement reaches Claude only when the callback returns in time and, for a built-in tool, passes schema validation.
Key distinctions and trade-offs
Semantic normalization versus shape compatibility: Converting a date correctly does not make an arbitrary replacement compatible with a built-in tool’s schema. Verify both the value and the envelope.
Replacement versus annotation: updatedToolOutput substitutes the value sent to Claude. additionalContext adds guidance alongside it. Choose the former when heterogeneous records must actually become comparable.
Model-visible output versus telemetry: The rewrite changes what Claude receives. Tool telemetry records the original output before the hook runs. Do not treat this mechanism as redaction of every audit or analytics surface.
Architecture / decision scenario
A multi-agent research coordinator combines archive and registry evidence about an incident. Both lookup tools are matched by a PostToolUse callback. The archive adapter converts documented Unix seconds and maps its known status code. The registry adapter parses its ISO 8601 offset and maps its status vocabulary. Each replacement preserves the corresponding MCP tool’s result shape.
Claude receives comparable timestamps, canonical statuses, and source identifiers. An unknown code remains visibly unknown instead of becoming “available.” A failed registry request stays a failure, so the coordinator seeks additional evidence rather than concluding that no record exists.
The team also tests a wrong-shaped built-in replacement and observes the original output reaching Claude. A deliberately timed-out normalization callback retains its original tool result too. If normalized output is mandatory for downstream consumers, the team moves that requirement into the tool’s return contract instead of assuming every hook attempt succeeds.
Common exam traps
- Using
additionalContextwhen the requirement is to replace the result. - Replacing a structured built-in output with an arbitrary string.
- Assuming MCP passthrough validates the application’s normalized schema.
- Guessing Unix timestamp units or provider-specific status meanings.
- Treating
PostToolUseblocking as prevention of a completed side effect. - Assuming the rewrite also changes telemetry or survives callback timeout.
- Explaining every provider’s units and codes in the prompt instead of converting them in code before Claude reads them.
Where the exam guide differs
Task Statement 1.5 frames PostToolUse normalization as a deterministic guarantee that heterogeneous formats are converted “before the agent processes them.” The product scopes that guarantee. A callback that times out leaves the original result in place; a replacement that fails a built-in tool’s output schema is ignored; MCP output is not schema-checked at all; and telemetry records the original. In an exam item, PostToolUse with updatedToolOutput remains the answer for normalizing results, but a design that must never expose a raw format returns the canonical shape from the tool itself.
What to remember
Read tool_response, normalize through the correct source adapter, and return shape-compatible hookSpecificOutput.updatedToolOutput. Preserve unknowns and failures. The rewrite prepares evidence for Claude after execution; it neither prevents side effects nor guarantees replacement when the hook fails.
Knowledge check
A research agent reads two MCP tools: the archive returns Unix timestamps in seconds with numeric status codes, and the registry returns ISO 8601 timestamps with offsets and text statuses. The tools have fixed output formats. Claude sometimes orders events wrongly when it compares them. Which change replaces the heterogeneous results with comparable records before Claude reads them, when the hook completes successfully?
PreToolUse hook to request ISO 8601PostToolUse hook that sets updatedToolOutputadditionalContextadditionalContext note leaves Claude to convert each value itself, because additionalContext appends beside the result rather than replacing it. A PreToolUse hook changes what goes into a call, not the format the tool returns.A PostToolUse hook on the built-in Bash tool returns a plain string as updatedToolOutput. Claude keeps receiving the original output. What explains this?
PostToolUse applies only from the next call onwarddecision: "block" to hide the original outputBash has stdout , stderr , interrupted and isImage — and a value that fails that schema is ignored. updatedToolOutput works for built-in and MCP tools alike, and decision: "block" only adds feedback beside the original result.The archive adapter receives a status code that the archive’s documented mapping does not list. How should the adapter represent that record?
unavailableunknownA registry lookup fails with a network error. The team wants Claude to tell this apart from a registry that reports no record. Which hook design supports that?
updatedToolOutput from PostToolUseFailure with status unavailableadditionalContext in PostToolUseFailurePostToolUse normalizer, which also receives failed callsPostToolUseFailure , not PostToolUse , and that event’s output accepts additionalContext but has no replacement field. Annotating the error keeps “the lookup failed” distinct from “the source reports no record”; manufacturing an empty or unavailable record would let the coordinator conclude that no record exists.A downstream consumer must never receive the archive’s raw timestamp format, including on runs where a hook fails or times out. Where does the conversion belong?
PostToolUse hook configured with a longer timeoutPostToolUse hook plus a prompt rule to ignore raw valuesPostToolUse hook output that runs in the backgroundPostToolUse remains the mechanism for normalizing results, but its guarantee is scoped: a timed-out callback leaves the original result in place, so a longer timeout only narrows the gap. A prompt rule is guidance, and an async output cannot modify anything. A contract that must hold even without a successful hook moves into the tool’s own return value.References
- Anthropic, Control execution with hooks
- Anthropic, Hooks reference
- Anthropic, TypeScript SDK reference
- Anthropic, Python SDK reference
- Anthropic, Claude Certified Architect – Foundations Exam Guide