If an agent can trigger a business action, the most important design question is where each check runs. In the Claude Agent SDK, hooks give you two points around every tool call. Only one of them can stop the action.
A check that must prevent a side effect belongs in PreToolUse. It runs before the tool executes and returns a decision. PostToolUse runs after execution. It can add context, replace the result the agent sees, or record an event, but it cannot undo what the tool already did.
| Decision | Use it when | What happens |
|---|---|---|
deny |
Required data is missing or a business rule fails | The tool does not run. Return a reason the agent and the operator can act on |
ask |
The request is valid but needs a person's approval | Your approval handler ( canUseTool in TypeScript,can_use_tool in Python) gets the decision. Returningask alone does not create an approval screen |
| allow | The request meets the hook's policy | The call continues to the tool service |
A PreToolUse hook can also return changed input. Use that sparingly and log it, because the tool then runs something the agent did not ask for.
Async hooks cannot gate. A callback that returns async: true (async_: True in Python) cannot block the tool or change its input. A hook that gates an action must return its decision before execution.
The hook is not the last line. The hook does not own the business record. The tool service must repeat authorization and business validation, and use an idempotency key so a retry cannot create a duplicate. Passing the hook shows the request looked acceptable, not that the action is correct.
The event names overlap, but the owners differ. Agent SDK hooks are registered by the application running the agent, through ClaudeAgentOptions in Python or the SDK options in TypeScript. Claude Code hooks are configured in the Claude Code environment. Exam questions and real incidents both turn on this difference.
Review the blocked paths as carefully as the successful one.