Skip to main content
Field note · anthropic agent sdkPublished May 23, 2026 · 19 min read

Anthropic Agent SDK Hooks: TypeScript Reference

Programmatic Anthropic Agent SDK hooks reference. The core events, full TypeScript signatures, PreToolUse, PostToolUse, Stop, SubagentStop, UserPromptSubmit with worked examples.

anthropic agent sdkclaude agent sdktypescript hooksclaude code hookspretooluseposttooluse

What Anthropic Agent SDK Hooks Are (And Why You Need the TypeScript Reference)

Anthropic Agent SDK hooks are typed async TypeScript callbacks registered inside query() that intercept 12 lifecycle events in the @anthropic-ai/claude-agent-sdk package (official overview). They are the programmatic counterpart to the declarative settings.json hooks documented for Claude Code, sharing the same decision shape but executing as functions in your own Node.js process.

The official Anthropic docs publish individual event definitions. What is missing is a single reference that lists every event, its TypeScript input shape, every output channel, and a runnable example for each. This post is that reference.

20+
hook events in the Agent SDK as of October 2026; this guide covers the twelve core ones
Source: Anthropic Agent SDK Docs, October 2026

How Anthropic Agent SDK Hooks Work in TypeScript

Every hook is an async callback registered against an event name inside the options.hooks object passed to query(). Each event has its own typed input that extends a shared BaseHookInput and carries event-specific fields. The callback returns a HookJSONOutput object that controls the agent's next move. The canonical specification for the hook surface lives in the official hooks guide and the TypeScript reference.

The three concepts you need:

  1. HookEvent is a TypeScript union of every event name (more than 20 as of October 2026). It drives the keys of options.hooks.
  2. HookCallback is the async function signature shared by every hook: (input, toolUseID, { signal }) => Promise<HookJSONOutput>.
  3. HookJSONOutput is the return shape with four channels: hookSpecificOutput (event-specific decisions like permissionDecision), systemMessage (a message shown to the user, not the model), additionalContext (context Claude sees, for certain events), and decision: 'block' (block a prompt, or keep the agent working past Stop and SubagentStop).

A minimal registration looks like this:

import { query, HookCallback } from '@anthropic-ai/claude-agent-sdk'

const audit: HookCallback = async (input, toolUseID, { signal }) => {
  console.log(`[${input.hook_event_name}]`, toolUseID)
  return {}
}

for await (const message of query({
  prompt: 'Refactor the auth middleware',
  options: {
    hooks: {
      PreToolUse: [{ matcher: 'Edit|Write', hooks: [audit] }],
      PostToolUse: [{ matcher: 'Edit|Write', hooks: [audit] }],
    },
  },
})) {
  console.log(message)
}

Each matcher carries an optional matcher regex that filters by tool name, and a hooks array of callbacks that run in order when the matcher matches. Returning {} from a callback is the explicit "allow" signal.

The Core Hook Events at a Glance

The HookEvent union published by the SDK enumerates every supported event. Memorize this table once and the rest of the reference is just typing.

EventWhen it firesCan permission-gate?Primary use
PreToolUseBefore a tool call executesYesSecurity gates, deny dangerous actions, rewrite tool input
PostToolUseAfter a tool call returns successfullyNoAudit, post-process results, inject context
PostToolUseFailureAfter a tool call throws or failsNoError logging, retry instrumentation, alerting
UserPromptSubmitWhen the user submits a promptYesValidate input, append system guidance, inject context
SessionStartAt the start of an agent sessionNoLogging, telemetry init, conversation context priming
SessionEndWhen the agent session endsNoTelemetry flush, summary generation, cleanup
StopWhen the main agent finishes respondingBlock-style onlyFinal quality checks, mandatory verification, completion gating
SubagentStartWhen a subagent is invokedNoSubagent context priming, telemetry
SubagentStopWhen a subagent finishesBlock-style onlySubagent output validation, coordination
PreCompactBefore message compaction runsNoPreserve specific messages, log compaction events
NotificationWhen the agent sends a notificationNoSlack alerts, email triggers, observability
PermissionRequestWhen the agent requests permissionNoCustom permission UX, audit trail

Read this table as the API surface. Every event name is a literal key you can put in options.hooks. Every cell maps to a TypeScript shape you can import from the SDK.

“The hooks option is a dictionary where keys are hook event names and values are arrays of matchers, each containing an optional filter pattern and your callback functions.”

Anthropic Agent SDK Docs

PreToolUse: The One That Can Say No

PreToolUse is the most powerful event in the system because it is the only one that can permission-gate the tool call before it executes. Set permissionDecision to 'deny' and the tool never runs.

The input shape:

type PreToolUseHookInput = BaseHookInput & {
  hook_event_name: 'PreToolUse'
  tool_name: string
  tool_input: unknown
}

The output channels you care about:

  • hookSpecificOutput.permissionDecision: 'allow' | 'deny' | 'ask'
  • hookSpecificOutput.permissionDecisionReason: string shown to Claude
  • hookSpecificOutput.updatedInput: replacement tool_input (only valid when permissionDecision is 'allow')
  • hookSpecificOutput.hookEventName: must echo input.hook_event_name so the decision binds to the right event

Worked example: deny edits to .env

import {
  query,
  HookCallback,
  PreToolUseHookInput,
} from '@anthropic-ai/claude-agent-sdk'

const protectEnvFiles: HookCallback = async (input, toolUseID, { signal }) => {
  const preInput = input as PreToolUseHookInput
  const filePath = preInput.tool_input?.file_path as string
  const fileName = filePath?.split('/').pop()

  if (fileName === '.env') {
    return {
      hookSpecificOutput: {
        hookEventName: input.hook_event_name,
        permissionDecision: 'deny',
        permissionDecisionReason: 'Cannot modify .env files',
      },
    }
  }

  return {}
}

for await (const message of query({
  prompt: 'Update the database configuration',
  options: {
    hooks: {
      PreToolUse: [{ matcher: 'Write|Edit', hooks: [protectEnvFiles] }],
    },
  },
})) {
  console.log(message)
}

Worked example: rewrite tool input instead of denying

You do not have to block. You can rewrite. Return permissionDecision: 'allow' with an updatedInput to modify the tool call before execution. This pattern is useful for force-applying conventions, sanitizing arguments, or routing tool calls through a wrapper:

const enforcePrettierWrite: HookCallback = async input => {
  const preInput = input as PreToolUseHookInput
  if (preInput.tool_name !== 'Write') return {}

  const original = preInput.tool_input as { file_path: string; content: string }
  const formatted = await runPrettier(original.content, original.file_path)

  return {
    hookSpecificOutput: {
      hookEventName: input.hook_event_name,
      permissionDecision: 'allow',
      updatedInput: { ...original, content: formatted },
    },
  }
}

The agent never sees the unformatted content. The diff is applied silently.

PostToolUse: Audit and Annotate After the Fact

PostToolUse runs after a tool call returns. It cannot stop or rewind the action, but it can inject context for Claude's next turn through additionalContext.

type PostToolUseHookInput = BaseHookInput & {
  hook_event_name: 'PostToolUse'
  tool_name: string
  tool_input: unknown
  tool_response: unknown
}

Worked example: type-check after every Edit

import { exec } from 'node:child_process'
import { promisify } from 'node:util'
const execAsync = promisify(exec)

const typeCheckAfterEdit: HookCallback = async input => {
  if (input.hook_event_name !== 'PostToolUse') return {}
  if ((input as any).tool_name !== 'Edit') return {}

  try {
    await execAsync('npx tsc --noEmit')
    return {}
  } catch (err) {
    return {
      hookSpecificOutput: {
        hookEventName: input.hook_event_name,
        additionalContext: `TypeScript errors introduced by the last edit:\n${(err as Error).message}`,
      },
    }
  }
}

When tsc fails, the error message is fed back into the conversation as additionalContext so Claude sees it on the next turn and can fix the regression itself. This is the SDK equivalent of running a type-check in a CI pipeline, except the agent self-corrects in-loop.

4
HookJSONOutput channels: hookSpecificOutput, systemMessage, additionalContext, decision
Source: Anthropic Agent SDK Docs, 2026

UserPromptSubmit: Guard or Steer the User's Input

UserPromptSubmit fires when the user submits a prompt, before Claude starts planning. The input carries the raw prompt:

type UserPromptSubmitHookInput = BaseHookInput & {
  hook_event_name: 'UserPromptSubmit'
  prompt: string
}

You can block submissions outright with decision: 'block' and a reason, or steer the agent quietly by returning additionalContext (information Claude can use). A systemMessage shows a note to the user, not the model.

Worked example: block prompts that ask for production deletes

const blockProdDeletes: HookCallback = async input => {
  if (input.hook_event_name !== 'UserPromptSubmit') return {}

  const prompt = (input as { prompt: string }).prompt.toLowerCase()
  const looksDangerous =
    prompt.includes('drop table') ||
    prompt.includes('delete from production') ||
    prompt.includes('truncate')

  if (looksDangerous) {
    return {
      decision: 'block',
      reason:
        'Destructive database operations require a separate approval flow.',
    }
  }

  return {}
}

Worked example: inject relevant context based on the prompt

const injectRoadmapContext: HookCallback = async input => {
  if (input.hook_event_name !== 'UserPromptSubmit') return {}

  const prompt = (input as { prompt: string }).prompt.toLowerCase()
  if (!prompt.includes('roadmap') && !prompt.includes('what should we ship')) {
    return {}
  }

  const roadmap = await loadRoadmapFromDb()

  return {
    hookSpecificOutput: {
      hookEventName: input.hook_event_name,
      additionalContext: `Current Q3 priorities:\n${roadmap}`,
    },
  }
}

The user does not see the injection. Claude just knows more than the prompt suggests.

Stop and SubagentStop: The Loop-Safe Final Guard

Stop fires when the main agent has finished its turn. SubagentStop fires when a subagent finishes. Both share a critical safety field: stop_hook_active.

type StopHookInput = BaseHookInput & {
  hook_event_name: 'Stop'
  stop_hook_active: boolean
}

type SubagentStopHookInput = BaseHookInput & {
  hook_event_name: 'SubagentStop'
  stop_hook_active: boolean
}

When stop_hook_active is true, the Stop hook has already fired once during this turn. Re-blocking from inside the hook can create an infinite loop where the agent tries to stop, your hook blocks, the agent tries again, your hook blocks again. Always early-return when stop_hook_active is true.

The Stop family supports a special output: top-level decision: 'block' with a reason string. This tells the agent it is not allowed to stop yet and must keep working.

Worked example: require coverage report before stopping

const requireCoverageReport: HookCallback = async input => {
  if (input.hook_event_name !== 'Stop') return {}

  const stopInput = input as { stop_hook_active: boolean }
  if (stopInput.stop_hook_active) return {}

  const reportExists = await fileExists('coverage/coverage-summary.json')
  if (reportExists) return {}

  return {
    decision: 'block',
    reason:
      'Coverage report missing at coverage/coverage-summary.json. Run npm run test:coverage before finishing.',
  }
}

This hook is safe because of the stop_hook_active early-return. The first stop attempt triggers the block; the second stop (after Claude has generated the report) skips the check and lets the agent finish.

“Fields within the hookSpecificOutput object provide granular control over specific hook events. The hookEventName must match input.hook_event_name to ensure the decision is applied to the correct event.”

Anthropic Agent SDK Docs

Session, Subagent, and Compaction Hooks

The remaining six events do not gate tool execution but they are essential for observability, telemetry, and lifecycle bookkeeping.

SessionStart and SessionEnd

SessionStart is the natural place to bootstrap telemetry, prime conversation context, or log a run. SessionEnd is where you flush metrics, save transcripts, or post a summary to Slack.

const sessionTelemetry: HookCallback = async input => {
  if (input.hook_event_name === 'SessionStart') {
    await metrics.increment('agent.session.started')
    return {
      hookSpecificOutput: {
        hookEventName: input.hook_event_name,
        additionalContext: `Session started at ${new Date().toISOString()}.`,
      },
    }
  }

  if (input.hook_event_name === 'SessionEnd') {
    await metrics.increment('agent.session.ended')
    return {}
  }

  return {}
}

SubagentStart and SubagentStop

SubagentStart fires when the agent invokes a subagent. Use it to pass context the parent agent has but the subagent does not. SubagentStop (covered above) is the subagent counterpart to Stop, including stop_hook_active.

PreCompact

PreCompact fires before the SDK runs message compaction. The hook itself cannot prevent compaction, but it can log which messages are about to be compacted so you can debug context-loss regressions later. This is the cheapest observability hook in the SDK: register it once, log the input, and you have a record every time history collapses.

Notification and PermissionRequest

Notification fires when the agent emits a notification (long-running tool completion, idle-state message, etc.). Wire it to your team's alerting layer. PermissionRequest fires when the agent asks for permission through the SDK's permission flow; use it to build custom approval UIs or write an audit trail.

Anthropic Agent SDK Hooks vs Claude Code settings.json Hooks

Both surfaces share an engine. Both use the same permissionDecision vocabulary. They differ in where the code lives and who runs it.

Dimensionsettings.json hooks (Claude Code)Agent SDK hooks (TypeScript)
Where defined.claude/settings.json declarative configoptions.hooks inside query() at runtime
Handler typescommand (shell), prompt (LLM), agent (subagent)Async TypeScript HookCallback
Type safetyJSON validationFull TS types: HookCallback, PreToolUseHookInput, etc.
Execution modelSpawns a subprocess per hookRuns in the same Node.js process
Latency floorShell startup + JSON serialize/deserializeMicroseconds for in-process logic
DistributionChecked into the repo, shared across teamBundled with your application code
Best forOperational governance, team-wide enforcementProgrammatic agents in production apps
CompositionChain via && in command stringsStandard TypeScript composition

The two surfaces are complementary, not competing. A real team often uses both: settings.json for team-wide guardrails (block .env edits, format on save) and SDK hooks for application-specific logic inside the product itself. The Claude Code hooks production guide covers the settings.json surface end-to-end; this post is the SDK counterpart.

How Anthropic Agent SDK Hooks Compare to Cursor Hooks

Cursor hooks (introduced in v1.7) target the Cursor editor agent specifically. They are configured in Cursor's settings, run inside the Cursor process, and have their own event names and decision shape.

The two systems do not interoperate at runtime, but the design patterns transfer cleanly:

  • Cursor beforeShellExecution ↔ SDK PreToolUse with a Bash matcher
  • Cursor afterFileEdit ↔ SDK PostToolUse with Edit|Write matcher
  • Cursor stop ↔ SDK Stop

If your team uses both Cursor and the Anthropic Agent SDK, keep the policy logic (allowlists, denylists, format rules) in shared TypeScript modules and call them from both environments. This avoids the drift problem where Cursor enforces one set of rules and the SDK enforces another, leaving an inconsistency window.

3
AI coding surfaces with hook systems: Anthropic Agent SDK, Claude Code settings.json, Cursor
Source: Anthropic Agent SDK Docs and Cursor Docs, 2026

Hook Ordering, Chaining, and Return-Value Semantics

When an event fires, all matching hooks run in parallel. For permission decisions the most restrictive result applies: a single deny blocks the call whatever the other hooks return. Because completion order is not deterministic, write each hook to act on its own rather than relying on another hook having run first.

Within the same event but across different matchers, each matcher group is evaluated independently against the tool_name. Multiple matchers can fire on the same call, and their callbacks run in parallel:

options: {
  hooks: {
    PreToolUse: [
      { matcher: 'Edit|Write', hooks: [protectEnvFiles] },
      { matcher: 'Bash', hooks: [blockDangerousCommands] },
      { matcher: '.*', hooks: [auditAll] },
    ]
  }
}

If the tool is Edit, protectEnvFiles and auditAll both run. If protectEnvFiles denies, auditAll still runs in this matcher group's tail position (the deny is recorded but does not prevent observability hooks from firing).

The return value contract:

  • Returning {} is the explicit allow signal.
  • Returning a hookSpecificOutput with permissionDecision: 'deny' blocks a tool call (PreToolUse). A UserPromptSubmit hook blocks with decision: 'block' instead.
  • Returning a hookSpecificOutput with permissionDecision: 'allow' and updatedInput modifies the tool input.
  • Returning decision: 'block' with reason keeps the agent running past a Stop or SubagentStop event.
  • Returning systemMessage shows a message to the user. To reach Claude, return additionalContext.
  • Returning additionalContext injects context for PostToolUse, UserPromptSubmit, SessionStart, and SubagentStart events.

Production Patterns: Three Ready-to-Paste Templates

Adapt these to your own agents and ship.

Pattern 1: Permission audit trail to Supabase

import { supabase } from './supabase'

const auditPermissionRequests: HookCallback = async (input, toolUseID) => {
  if (input.hook_event_name !== 'PreToolUse') return {}

  const preInput = input as PreToolUseHookInput
  await supabase.from('agent_audit_log').insert({
    tool_use_id: toolUseID,
    tool_name: preInput.tool_name,
    tool_input: preInput.tool_input,
    session_id: preInput.session_id,
    created_at: new Date().toISOString(),
  })

  return {}
}

Every tool call is logged with the session ID. Now you can answer "what did the agent try to do on Tuesday?" without grepping logs.

Pattern 2: Cost guard with maxBudgetUsd shadow

const costGuard: HookCallback = async input => {
  if (input.hook_event_name !== 'PreToolUse') return {}

  const spentSoFar = await getSpentForSession((input as any).session_id)
  if (spentSoFar > 5.0) {
    return {
      hookSpecificOutput: {
        hookEventName: input.hook_event_name,
        permissionDecision: 'deny',
        permissionDecisionReason: `Session budget exceeded ($5). Stopping.`,
      },
    }
  }

  return {}
}

The SDK already supports maxBudgetUsd in query options, but a hook gives you tighter, per-tool granularity and the ability to log overruns to your own telemetry.

Pattern 3: Slack notification on long-running operations

const notifySlackOnLongOps: HookCallback = async input => {
  if (input.hook_event_name !== 'Notification') return {}

  await fetch(process.env.SLACK_WEBHOOK_URL!, {
    method: 'POST',
    body: JSON.stringify({
      text: `Agent notification: ${JSON.stringify(input)}`,
    }),
  })

  return {}
}

Pair this with a long-running tool like a deep-research subagent and your team gets pinged when manual review is needed.

How Hooks Connect to the Rest of the SDK

Hooks are one of several control surfaces in the Anthropic Agent SDK. They compose with:

  • canUseTool in query() options: a permission gate that fires before every tool call, distinct from PreToolUse because it can ask the user interactively rather than autonomously denying. See the official permissions guide.
  • allowedTools / disallowedTools: static allowlists/denylists evaluated before hooks fire. Same permissions guide.
  • maxBudgetUsd / maxTurns: hard ceilings on session resources, independent of hook logic.
  • Subagents (via agents option): hooks fire inside subagent contexts too, so a PreToolUse hook registered at the top level applies to subagent tool calls.
  • Sessions and MCP servers: hooks observe and gate work happening across both surfaces.

A complete production setup typically uses disallowedTools for the hardest restrictions, canUseTool for interactive prompts, and SDK hooks for everything that needs custom logic or telemetry. For deeper context engineering patterns alongside hooks, see Context Engineering Beyond CLAUDE.md.

How Do You Debug a Hook That Is Not Firing?

The most common failure mode is the matcher regex not matching the tool name you expected. Tool names are case-sensitive and exact, so 'edit' will never match the Edit tool. When a hook seems silent, instrument it before you debug deeper:

const debugAll: HookCallback = async (input, toolUseID) => {
  console.error(
    `[hook] event=${input.hook_event_name} toolUseID=${toolUseID}`,
    JSON.stringify(input, null, 2)
  )
  return {}
}

// Register on every event with a permissive matcher
options: {
  hooks: {
    PreToolUse: [{ matcher: '.*', hooks: [debugAll] }],
    PostToolUse: [{ matcher: '.*', hooks: [debugAll] }],
    UserPromptSubmit: [{ hooks: [debugAll] }],
    Stop: [{ hooks: [debugAll] }],
  }
}

Run the query and watch stderr. You will see every event the SDK fires, exactly what tool_name Claude is passing, and the full tool_input payload. From there, narrow the matcher.

Three debugging gotchas worth knowing:

  1. Matchers are regex, not glob. 'Edit|Write' matches both tools because it is a regex alternation. 'Edit,Write' matches neither because the comma is a literal character.
  2. hookEventName mismatches silently fail. If you return hookSpecificOutput.hookEventName: 'PreToolUse' from a PostToolUse callback, the decision is ignored. Always echo input.hook_event_name.
  3. return {} versus return with no value. TypeScript requires the explicit {} return for the allow signal. A bare return returns undefined which the SDK does not treat as a valid HookJSONOutput and may surface as a runtime error in strict mode.

Unit-testing hooks is straightforward because they are plain async functions. Pass a constructed input object, await the return, assert on the shape. No SDK harness required:

import { describe, it, expect } from 'vitest'

describe('protectEnvFiles', () => {
  it('denies .env edits', async () => {
    const result = await protectEnvFiles(
      {
        hook_event_name: 'PreToolUse',
        tool_name: 'Edit',
        tool_input: { file_path: 'apps/api/.env' },
        session_id: 'test',
      } as any,
      'test-tool-use-id',
      { signal: new AbortController().signal }
    )

    expect(result.hookSpecificOutput?.permissionDecision).toBe('deny')
  })
})

Treat hooks like any other piece of your codebase: type-check, test, and review. They are policy code that runs against a Claude agent, which means a bad hook can either allow what it should block or block what it should allow. Both failure modes are silent unless you are actively logging.

Anthropic Agent SDK Hooks: Questions Developers Ask

Common questions about this topic, answered.

What is the Anthropic Agent SDK hooks API?

The Anthropic Agent SDK hooks API is a programmatic event system in the @anthropic-ai/claude-agent-sdk TypeScript package. It exposes 12 lifecycle events (PreToolUse, PostToolUse, UserPromptSubmit, Stop, SubagentStop, PreCompact, SessionStart, SessionEnd, Notification, PermissionRequest, SubagentStart, PostToolUseFailure) that you register as async TypeScript callbacks inside the query() options.hooks object. Each callback receives a typed input matching its event shape and returns a HookJSONOutput object that can allow, deny, modify, or annotate the agent operation.

How do I install the Anthropic Agent SDK?

Install the SDK with npm install @anthropic-ai/claude-agent-sdk. Import the query function plus the typed inputs you need: import { query, HookCallback, PreToolUseHookInput } from "@anthropic-ai/claude-agent-sdk". The package exports both the runtime query() function and the TypeScript types for every hook event so your callbacks stay fully type-checked.

What is the difference between Anthropic Agent SDK hooks and Claude Code settings.json hooks?

Claude Code settings.json hooks are configured declaratively in .claude/settings.json and execute as shell commands, single-turn prompts, or subagents. Anthropic Agent SDK hooks are TypeScript async callback functions registered programmatically in query() options.hooks. The decision shape (permissionDecision allow/deny/ask, hookSpecificOutput, systemMessage, additionalContext) is identical because both surfaces share the same underlying engine. Settings.json suits operational governance shared by a team; SDK hooks suit programmatic agents embedded in your application code.

How does PreToolUse block a tool call?

Return an object whose hookSpecificOutput contains permissionDecision: "deny" plus a permissionDecisionReason string. The reason is shown to Claude so the agent knows why the action was rejected and can plan an alternative. You can also return permissionDecision: "ask" to require human confirmation, or permissionDecision: "allow" with an updatedInput object to modify the tool arguments before execution.

How do I prevent an infinite Stop hook loop?

Check the stop_hook_active boolean on the StopHookInput. When true, the Stop hook has already fired once during this turn. Always early-return without setting decision: "block" if stop_hook_active is true, otherwise the hook can re-trigger itself and produce a non-terminating loop. The same boolean exists on SubagentStopHookInput for subagent stop hooks.

Can SDK hooks modify the user prompt before Claude sees it?

Yes. The UserPromptSubmit hook receives the prompt string on UserPromptSubmitHookInput.prompt and can return additionalContext (inside hookSpecificOutput) to give Claude more information before it processes the input; systemMessage shows a note to the user, not the model. To block a prompt outright, return decision: "block" with a reason.

What is the cost of running async TypeScript hooks?

Hooks run in the same Node.js process as your query() call, so the latency floor is microseconds for synchronous logic and dominated by any awaited network calls inside the callback. There is no shell-out overhead and no JSON serialization cost on the way in (the input is passed as a typed JavaScript object). Hooks that perform LLM calls or external API requests will dominate end-to-end latency, so cache or short-circuit when possible.

Do Anthropic Agent SDK hooks work with Cursor hooks?

Cursor hooks (v1.7+) are configured separately in Cursor settings and target the Cursor agent specifically. Anthropic Agent SDK hooks target any Claude agent invoked through @anthropic-ai/claude-agent-sdk. They do not overlap at runtime, but the design patterns transfer: PreToolUse for security gates, PostToolUse for automation, Stop for completion checks. Teams using both should keep the policy logic in shared TypeScript modules and call them from each environment to stay consistent.

Where to Go Next

Official references (always check these for the latest API surface):

Pixelmojo posts that pair with this one:

If you are evaluating whether to use SDK hooks or settings.json hooks (or both), read the Claude Code hooks production guide for the declarative side. The two posts together give you the complete hook surface.

If you are designing the AI governance layer around your hooks, the Claude Code Technical Debt Mitigation Guide covers the framework hooks plug into: thread-based engineering checkpoints, quality gates, and review thresholds.

Ready to ship AI products with hooks done right?

Next insightGemini, YouTube and AI Visibility: A Dated Note on What Changed and When
Lloyd Pilapil
About the author
Lloyd PilapilFounder & AI Product Architect · Pixelmojo

Lloyd Pilapil is the founder of Pixelmojo and a senior UI/UX and growth designer with 20+ years of digital product experience and more than 30 years across visual and graphic design. His work includes projects for Salesforce, Parsons, Egis, and other public- and private-sector organizations. He builds production AI systems for B2B companies and writes about agentic AI, multi-agent orchestration, AX (Agentic Experience) design, GEO, and Thread-Based Engineering, focused on shipping AI products that generate revenue, not prototypes.

Agentic AI SystemsMulti-Agent OrchestrationAX DesignGEO & AI SearchThread-Based EngineeringAI Product DevelopmentGrowth MarketingUI/UX Design

Related reading

All field notes
context engineering · 15 min

Context Engineering Beyond CLAUDE.md: The 5-Layer Hierarchy

CLAUDE.md is just layer one. The five-layer context hierarchy, memory patterns, and subagent strategies that separate productive AI coding from prompt guessing. With working examples.

Building AIRead article →
thread based engineering · 17 min

Thread-Based Engineering: The Framework for Scaling AI Development

The definitive guide to Thread-Based Engineering: 7 thread types, governance alignment, a production case study (Lakbay AI), and the research on AI code quality, including the Veracode finding that models failed to write XSS-safe code 86% of the time.

Building AIRead article →
thread based engineering · 17 min

Thread-Based Agentic Experience Engineering [TBE + AXD]

The unified framework connecting Thread-Based Engineering to Agentic Experience Design. How thread autonomy levels map to trust patterns and supervision models.

Building AIRead article →
Read enough? Check your own
Does AI get your business?

Run Radar's six free technical checks on your domain, then follow the evidence behind each finding.