Analysis
If you run AI coding agents inside a business, the scary part isn't the writing of code. It's everything around it: an agent that quietly burns through your API budget, ships output in the wrong format, or touches a file it shouldn't. Hooks are the answer most teams reach for. They let you sit in the middle of what the agent does and say yes, no, or "do it differently."
The idea is simple. At set moments in the agent's run, your own code gets to step in. Before it acts, you can check the request and block it. After it acts, you can reshape the result. When something breaks, you can retry or raise an alarm. When it finishes, you can log what happened for the audit trail.
One caveat worth stating plainly, because it affects every code sample here. The hook names, the YAML config file, and the SDK import used throughout this guide don't line up with how Claude Code actually ships hooks today. The real product uses tool-lifecycle events such as PreToolUse and PostToolUse, configured in JSON settings files rather than a .claude/hooks.yaml, and the handlers receive JSON on stdin and respond with exit codes (Claude Code Docs, Hooks reference). Treat the patterns below as a mental model for the kinds of guardrails you want, then build them against the supported events. The snippets as written will not run.
With that said, here's the lifecycle and the four control points the rest of the guide is built around.
Analysis
Prerequisites
- Claude Code >= 0.38 (reportedly the version that added hooks, unconfirmed; the current product is on the 2.x line and hook features shipped across several 1.x releases, per the claude-code changelog)
- TypeScript 5.3+
- A working grasp of middleware patterns
Step-by-Step Framework
Step 1: Understand the Hook Lifecycle
User Request
↓
[Global preExecute hooks] → can modify input, add context, block execution
↓
[Skill-specific preExecute hooks]
↓
Skill Execution (the actual work)
↓
[Skill-specific postExecute hooks] → can modify/transform output
↓
[Global postExecute hooks]
↓
[onComplete hooks] → logging, cleanup, notifications
↓
Response to User
If error at any point:
↓
[onError hooks] → retry, fallback, alertRead top to bottom, the flow is intuitive: requests pass through pre-execution checks, do the work, pass through post-execution shaping, then finish. Errors get diverted to their own handler. The real product groups these around tool calls rather than a generic request, and the official docs confirm that a pre-tool hook can approve or deny an action before it runs while a post-tool hook fires once it succeeds (Claude Code Docs, Hooks reference). So the shape of the diagram is sound even though the names below are not the supported ones.
Step 2: Create a Global Pre-Execution Hook
This first example caps spend per session. The agent estimates the cost of an operation before it runs, and if running it would push the session over budget, the hook stops it.
// .claude/hooks/cost-limiter.ts
import { HookContext, PreExecuteHook } from '@anthropic/claude-sdk';
// Track spending per session
const sessionSpend = new Map<string, number>();
const BUDGET_LIMIT = 10.00; // $10 per session
export const costLimiterHook: PreExecuteHook = {
name: 'cost-limiter',
priority: 100, // Higher = runs first
async beforeExecute(context: HookContext): Promise<HookContext> {
const sessionId = context.sessionId;
const estimatedCost = context.estimatedTokens * context.modelPricing.output / 1000;
const currentSpend = sessionSpend.get(sessionId) || 0;
if (currentSpend + estimatedCost > BUDGET_LIMIT) {
throw new BudgetExceededError(
`Session budget exceeded: $${currentSpend.toFixed(2)} / $${BUDGET_LIMIT}.
Estimated cost of this operation: $${estimatedCost.toFixed(2)}.
Request admin approval to continue.`
);
}
// Add cost metadata for post-execution tracking
context.metadata.estimatedCost = estimatedCost;
context.metadata.budgetRemaining = BUDGET_LIMIT - currentSpend;
return context;
}
};A few things to flag. The import is from @anthropic/claude-sdk, which isn't a real package, Anthropic ships `@anthropic-ai/claude-agent-sdk`, @anthropic-ai/claude-code, and @anthropic-ai/sdk instead. And the pattern of returning a mutated HookContext from a beforeExecute method is not how supported hooks work; they signal a block with exit code 2 or a deny decision rather than throwing inside a returned context object (Claude Code Docs, Hooks reference). The budgeting logic is still a good template to port: track spend, estimate the next operation, refuse when the total crosses your limit.
Step 3: Create a Post-Execution Hook
Once the work is done, a post-execution hook can reshape what comes back. Here it tidies code output and appends a cost footer.
// .claude/hooks/output-formatter.ts
import { PostExecuteHook, HookContext } from '@anthropic/claude-sdk';
export const outputFormatterHook: PostExecuteHook = {
name: 'output-formatter',
priority: 50,
async afterExecute(context: HookContext): Promise<HookContext> {
const output = context.result;
// Auto-format code blocks if the output contains code
if (context.skillName === 'code-writer' || context.skillName === 'refactor') {
context.result = await formatCodeOutput(output, context.metadata.language);
}
// Add metadata footer to responses
if (context.metadata.estimatedCost) {
context.result +=

