Content

Anthropic Agent SDK Lifecycle Hooks and Idempotency

PreToolUse fires before the tool function, which is before the HTTP call. It can block a repeat it can see. It cannot tell you the server already committed.

EngineeringOct 16, 2026

Key takeaways

  • -PreToolUse runs before the tool function. PostToolUse runs after a result. PostToolUseFailure runs after a failure. None of them is the server's commit.
  • -A pre-call hook cannot dedupe, because it has not seen whether the previous HTTP request succeeded.
  • -updatedInput can attach an idempotency key. The key has to be stable across retries, or the rewrite creates a new write.
  • -Denying a repeated tool_use_id does nothing useful. The retry has a new id.
  • -A hook that throws or times out is not a policy. Decide explicitly whether a failed hook blocks the tool.

A lifecycle hook is a good place to block a tool. It is a bad place to decide that a write already happened.

The Anthropic Agent SDK fires hooks in your process, around the tool function. PreToolUse runs when a tool call has been requested and has not run. PostToolUse runs when the tool has returned a result. PostToolUseFailure runs when the tool execution failed. Anthropic documents those three as the points where you can block, modify, log, or handle an error. The HTTP request, if your tool makes one, sits inside the tool function. The hook is on one side of that function or the other. It is not on the server.

That ordering is the whole idempotency problem. The companion page, Making Anthropic Agent SDK Tool Calls Idempotent, is about at-least-once versus exactly-once. This page is about where the hook sits on that path, and why a check that runs before the call cannot collapse two attempts into one.

The order, with the HTTP call drawn in

For a tool that refunds a charge, the sequence is:

  1. The model emits a tool call.
  2. PreToolUse hooks run. They can deny, ask, defer, or rewrite the input with updatedInput.
  3. Your tool function runs.
  4. The tool function sends the HTTP request. The server applies the refund and, if you sent an idempotency key it has seen, returns the stored result instead of refunding again.
  5. The function returns, or it throws, or it times out.
  6. PostToolUse runs on a result. PostToolUseFailure runs on a failure.

Step 4 is the only step that knows whether money moved. Step 2 has the arguments for the attempt that is about to start. Step 6 has whatever the function returned, which is empty when the process died between step 4 and step 5.

A diagram that puts "the hook" on one side and "the API" on the other is too coarse. The hook and the API are separated by your function. If the function has not been entered, the API has not been contacted by this attempt. If a previous attempt entered the function and the response was lost, this hook still has no receipt.

What PreToolUse can see

The callback receives the tool name, the tool input, the session id, and a tool_use_id. It can read those and return a decision. Anthropic's PreToolUse output supports permissionDecision of allow, deny, ask, or defer, a reason, and updatedInput when you want the tool to run with different arguments. Pair updatedInput with permissionDecision: "allow" if the rewritten input should proceed without another prompt.

That is a complete view of this attempt. It is not a view of the previous attempt's HTTP status. The previous attempt may have:

  • never left the machine
  • left the machine and been rejected
  • left the machine, been applied, and timed out on the way back
  • been applied under a different idempotency key because the previous hook minted a fresh one

Those four histories look identical to a PreToolUse hook that only has the new block in front of it. Denying the new block because the arguments match a block you remember is safe only when your memory includes a completed result. Denying it because the arguments merely look familiar drops the retry that would have picked up a lost response, or, if you allow it with a new key, performs the write twice. The hook has to choose without the receipt. That is the sense in which a pre-call hook cannot dedupe.

Why denying a repeat is not dedupe

Dedupe means the second attempt resolves to the first result. A deny means the second attempt does not run, and the model is told why.

Those are different outcomes. A deny is correct for "this tool may not delete production." It is the wrong tool for "this refund may already exist." The model receives the denial and may try a different operation, a different amount, or a rephrased call. You have not returned the original refund object. You have started a new decision.

tool_use_id makes the deny even less useful. The id on the retry is new. A hook that stores ids and denies exact repeats of an id never fires on the retry that matters. The retry matches on arguments, not on the id. Hash the tool name and the canonical business inputs if you need to recognise the action. Even then, the hash tells you that you have seen the action, not that the server finished it.

permissionDecisionReason is still worth setting. Anthropic notes that the reason tells the model why, so it can avoid a pointless retry. Use it for policy denials: wrong tool, path not allowed, amount over the limit. Do not use a deny as a substitute for an idempotency key on a write that is supposed to complete.

updatedInput is for a stable key, not a new one

updatedInput rewrites the arguments the tool function sees. That is the right place to attach an idempotency key the model should not invent. Compute the key from the business fields already in the input. Write it onto the input. Let the tool function send that field on the HTTP request and nowhere else.

A hook that sets the key to a random UUID on every PreToolUse manufactures a new write. The rewrite looks like safety in the code review, because a field named idempotency_key is now present. The value changes every attempt, so the server treats every attempt as new. The bug is the same as generating the UUID inside the tool function. The key has to be a pure function of the action.

If the server does not store idempotency keys, updatedInput does not create that behaviour. You cannot annotate a write into exactly-once. The function has to persist a record of its own, and the ambiguous crash window remains. For APIs that do store keys, the hook's job is only to make the value stable and present before the function builds the request.

The runtime that already retries with the same key, so the tool function does not implement that store itself, is covered in How Swytchcode Retries Failed API Calls. What the exec call returns when it is blocked or waiting on approval, which a hook author will otherwise misread as a tool failure, is in "swy exec" Explained.

PostToolUse is the audit

PostToolUse runs after the tool has a result. The write, if it was going to happen, has happened. The hook can record the result, redact it before it goes back to the model, or trigger a follow-up that is not itself an unguarded write. It cannot cancel the refund.

PostToolUseFailure runs when tool execution failed. Failure here means the tool function reported failure, or the execution did not complete in a way the SDK treats as success. It does not mean the server rejected the request. A timeout after the server accepted the write is a failure in this hook and a success in the ledger. Log it as unresolved. Do not automatically run the tool again from this hook with a newly minted key. That is the second refund, started from the hook that was supposed to notice the first one.

The Stripe-shaped version of that timeout is How to Connect Stripe to Claude Code. The rule for which attempt is the same attempt is Making Anthropic Agent SDK Tool Calls Idempotent.

A hook that errors is not a closed gate

Hooks have timeouts. Anthropic's default for most events is long, on the order of minutes, and a callback that exceeds it is cancelled. For PreToolUse, a timeout means the tool does not run and the model is told the hook did not respond, which is a better outcome than the call proceeding with no decision. That default is still something you should not lean on for a payment check. Set a timeout you mean, on the HookMatcher, short enough that a stuck approval does not pin the session.

Then decide what an exception means. If the hook throws while checking an amount, and your platform treats an exception as "allow," the check is decorative. Money tools should fail closed: an error in the hook is a deny with a reason, not a pass. Log the exception in the same place as the decision, or the next incident will show a successful refund and no rule.

Hooks also do not replace the policy file on the exec path. A hook in the SDK process can be skipped by another caller that uses the same API with a different client. A rule evaluated on the call itself still runs. How those rules are written is Policy-as-Code for AI Agents. The stages between a tool call and the HTTP request, of which a hook is only one, are in AI Agent Execution Layer Architecture.

FAQ

Where does PreToolUse fire relative to the HTTP call?

Before the tool function runs. If the HTTP call is inside that function, PreToolUse has not sent it yet. It sees the requested arguments. It does not see the server's response to this attempt or to a previous attempt whose response was lost.

Can a PreToolUse hook make a tool exactly-once?

No. It can refuse to start an invocation it recognises, and it can attach a stable idempotency key with updatedInput. Exactly-once requires the server, or your own store, to keep the first result and return it on the second request. The hook never holds that result at the moment the second request needs it, if the first response was lost.

What is updatedInput for?

It replaces the tool arguments before the function runs. Use it to add an idempotency key derived from the business inputs, or to force a path or an amount into a safe shape. Pair it with permissionDecision: "allow" when the rewritten call should proceed. A random key written here is a new write on every retry.

Does PostToolUseFailure mean the API rejected the call?

It means the tool execution failed from the SDK's point of view. The server may still have applied the write. Treat a timeout on a payment as unresolved until you read the charge or the refund back. Do not retry it with a new idempotency key.

Should I deny repeated tool_use_id values?

A repeated id is not how retries arrive. Each new tool call gets a new id. Deny on policy: the tool, the path, the amount. Recognise a repeated action by the canonical inputs, and resolve that action through the server's idempotency key rather than through a deny.

Which hooks exist on both Python and TypeScript?

PreToolUse, PostToolUse, and PostToolUseFailure are in both SDKs, along with UserPromptSubmit, Stop, subagent start and stop, PreCompact, PermissionRequest, and Notification. Several others, including PostToolBatch and SessionStart, are TypeScript-only. Check the current Agent SDK hooks page before you depend on one, because the list has kept moving.

More content