Agent skill
sdk-hooks-development
Use this skill when implementing TypeScript hook callbacks for the Claude Agent SDK — creating PreToolUse hooks to allow/deny tool calls, PostToolUse hooks to inject additionalContext, building factory functions for parameterized hooks, using HookCallback and HookJSONOutput types, applying isPreToolUseInput and isPostToolUseInput type guards, or designing a hooks strategy for an Agent SDK platform. Hooks in the TypeScript SDK are async functions, NOT JSON config files.
Install this agent skill to your Project
npx add-skill https://github.com/ItamarZand88/claude-code-agentic-engineering/tree/main/plugins/agent-sdk-pro/skills/sdk-hooks-development
SKILL.md
TypeScript SDK Hook Development
Critical distinction: Agent SDK hooks (TypeScript HookCallback functions) are different from Claude Code plugin hooks (JSON config). This skill covers the TypeScript SDK programmatic API.
Core Types
// From @anthropic-ai/claude-agent-sdk (via your types.ts re-export)
type HookCallback = (
input: HookInput,
toolUseId: string,
context: { signal: AbortSignal }
) => Promise<HookJSONOutput>;
type HookJSONOutput = {
hookSpecificOutput?: {
hookEventName: string;
// PreToolUse:
permissionDecision?: "allow" | "deny";
permissionDecisionReason?: string;
// PostToolUse:
additionalContext?: string;
};
};
Pattern: Basic Hook Structure
Every hook follows this exact pattern:
import type { HookCallback, HookJSONOutput } from "../types";
import { isPreToolUseInput, getToolInputFilePath } from "../types";
export const myHook: HookCallback = async (input, _toolUseId, { signal }): Promise<HookJSONOutput> => {
// 1. Always check abort first
if (signal.aborted) return {};
// 2. Guard: only handle the right event type
if (!isPreToolUseInput(input)) return {};
// 3. Extract data
const filePath = getToolInputFilePath(input);
if (!filePath) return {};
// 4. Apply logic
if (filePath.endsWith(".env")) {
return {
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Reading .env files is not allowed — they may contain secrets",
},
};
}
return {}; // empty = allow
};
Pattern: Factory Function (Parameterized Hooks)
Use factory functions when a hook needs runtime parameters:
import path from "node:path";
import type { HookCallback, HookJSONOutput } from "../types";
import { getToolInputFilePath, isPreToolUseInput } from "../types";
export function createFileRestrictionHook(allowedFilePath: string): HookCallback {
const normalized = path.resolve(allowedFilePath);
return async (input, _toolUseId, { signal }): Promise<HookJSONOutput> => {
if (signal.aborted) return {};
if (!isPreToolUseInput(input)) return {};
const filePath = getToolInputFilePath(input);
if (!filePath) return {};
if (path.resolve(filePath) === normalized) return {};
return {
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: `Only ${allowedFilePath} can be modified`,
},
};
};
}
Pattern: PostToolUse — additionalContext
Inject feedback into tool results to guide the agent:
import type { HookCallback, HookJSONOutput } from "../types";
import { getToolInputCommand, isPostToolUseInput } from "../types";
export const testReminderHook: HookCallback = async (input, _toolUseId, { signal }): Promise<HookJSONOutput> => {
if (signal.aborted) return {};
if (!isPostToolUseInput(input)) return {};
const command = getToolInputCommand(input);
if (!isTestCommand(command)) return {};
return {
hookSpecificOutput: {
hookEventName: input.hook_event_name,
additionalContext: "REMINDER: If tests pass, stop. If 10+ pass with failures, prune.",
},
};
};
Pattern: PostToolUse — Auto-Fix with Feedback
Run linters/typecheck after edits and inject remaining errors:
import { execSync } from "node:child_process";
import type { HookCallback, HookJSONOutput } from "../types";
import { getExecOutput, getToolInputFilePath, isPostToolUseInput } from "../types";
export function createLintFixHook(workingDirectory: string, targetFile: string): HookCallback | null {
const eslintBin = path.join(workingDirectory, "node_modules", ".bin", "eslint");
if (!existsSync(eslintBin)) return null; // graceful disable
return async (input, _toolUseId, { signal }): Promise<HookJSONOutput> => {
if (signal.aborted) return {};
if (!isPostToolUseInput(input)) return {};
const filePath = getToolInputFilePath(input);
if (path.resolve(filePath) !== path.resolve(targetFile)) return {};
try {
execSync(`${eslintBin} --fix "${targetFile}" 2>&1`, {
cwd: workingDirectory,
encoding: "utf8",
timeout: 30_000,
});
return {};
} catch (error: unknown) {
const output = getExecOutput(error);
if (!output) return {};
return {
hookSpecificOutput: {
hookEventName: "PostToolUse",
additionalContext: `LINT ERRORS after auto-fix:\n${output.slice(0, 2000)}`,
},
};
}
};
}
Hook Registration
Register hooks in the query() call options:
const fileRestrictionHook = createFileRestrictionHook(params.testFilePath);
const lintFixHook = createLintFixHook(params.workingDirectory, params.testFilePath);
await query({
prompt,
options: {
// ...
hooks: {
PreToolUse: [
{ matcher: "Write|Edit", hooks: [fileRestrictionHook] },
{ matcher: "Read", hooks: [envProtectionHook] },
],
PostToolUse: [
{ matcher: "Bash", hooks: [testPruneHook] },
// Conditionally include lintFixHook if binary exists
...(lintFixHook ? [{ matcher: "Write|Edit", hooks: [lintFixHook] }] : []),
],
},
},
});
Utility Functions
Keep these in your types.ts — they centralize unsafe casts:
// Safe extraction of file_path from PreToolUse or PostToolUse input
export function getToolInputFilePath(input: PreToolUseHookInput | PostToolUseHookInput): string {
const toolInput = input.tool_input as Record<string, unknown> | undefined;
const filePath = toolInput?.file_path;
return typeof filePath === "string" ? filePath : "";
}
// Safe extraction of command from PostToolUse Bash input
export function getToolInputCommand(input: PostToolUseHookInput): string {
const toolInput = input.tool_input as Record<string, unknown> | undefined;
const command = toolInput?.command;
return typeof command === "string" ? command : "";
}
// Safe extraction of execSync error output
export function getExecOutput(error: unknown): string {
const execError = error as { stdout?: string; stderr?: string };
return ((execError.stdout ?? "") + (execError.stderr ?? "")).trim();
}
Type Guards
export function isPreToolUseInput(input: HookInput): input is PreToolUseHookInput {
return input.hook_event_name === "PreToolUse";
}
export function isPostToolUseInput(input: HookInput): input is PostToolUseHookInput {
return input.hook_event_name === "PostToolUse";
}
Pattern: Modify Tool Input (updatedInput)
Redirect or sanitize tool inputs before execution. Requires permissionDecision: "allow". Never mutate tool_input — always return a new object:
return {
hookSpecificOutput: {
hookEventName: input.hook_event_name, // always use input.hook_event_name, not hardcoded string
permissionDecision: "allow", // required when using updatedInput
updatedInput: {
...(input.tool_input as Record<string, unknown>),
file_path: `/sandbox${filePath}`, // redirect writes to sandbox
},
},
};
Pattern: Stop the Agent
Return continue: false to halt the agent entirely (different from denying a single tool):
return {
continue: false,
stopReason: "Budget exhausted — stopping before incurring more cost.",
};
Top-level output fields (outside hookSpecificOutput):
continue: boolean— whether the agent continues (defaulttrue)stopReason: string— message shown whencontinueisfalsesuppressOutput: boolean— hide hook stdout from transcriptsystemMessage: string— inject a message directly into Claude's conversation
Pattern: PostToolUseFailure
Handle tool execution failures. TypeScript-only event. Use top-level systemMessage — hookSpecificOutput is not supported for this event type:
const failureLogger: HookCallback = async (input, toolUseID, { signal }) => {
if (signal.aborted) return {};
if (input.hook_event_name !== "PostToolUseFailure") return {};
const failure = input as PostToolUseFailureHookInput;
console.error("[TOOL FAILURE]", failure.tool_name, failure.error, { isInterrupt: failure.is_interrupt });
// systemMessage (top-level) — NOT hookSpecificOutput, which isn't supported here
return {
systemMessage: `Tool "${failure.tool_name}" failed: ${failure.error}. Consider an alternative approach.`,
};
};
Design Rules
- Always check
signal.abortedfirst — prevents work on cancelled operations - Always type-guard the input — hooks receive
HookInput, guard to the specific type - Return
{}for non-applicable cases — empty output = allow/no-op - Graceful disable — factory hooks that depend on binaries (eslint, tsc) should return
nullwhen unavailable - Never trust
tool_inputtypes — always cast safely via helpers - Keep hooks independent — hooks for the same matcher run sequentially; all are evaluated even if an earlier one denies
- Never throw from a hook — swallow errors; throwing can crash the agent
- Pass
signaltofetch()— so HTTP requests cancel properly on hook timeout - Use
input.hook_event_name— not hardcoded strings inhookEventNamefield
Advanced Patterns
For more patterns from these references:
references/pretooluse-patterns.md— path guards, filename guards, command keyword guards, extension guardsreferences/posttooluse-patterns.md— test reminders, TypeScript auto-fix, ESLint auto-fix, build verificationreferences/hook-events-reference.md— PreToolUse and PostToolUse deep dive, execution model, tool name referencereferences/smart-dispatch-pattern.md— single dispatcher routing to sub-handlers by file type and tool; merge strategies; testing handlers in isolationreferences/testing-hooks.md— unit test patterns with vitest, mock helpers, integration testing, mockingexecSync
Examples:
examples/env-protection-hook.ts—.envfile read blocker (PreToolUse)examples/file-restriction-hook.ts— single-file write restriction factory (PreToolUse)examples/security-blocker-hook.ts— comprehensive security: dangerous commands + protected files + out-of-project writesexamples/smart-dispatch-hook.ts— single dispatcher routing to sub-handlers by file type and tool nameexamples/auto-format-hook.ts— silent Prettier formatting after edits (PostToolUse, no additionalContext)examples/input-redirect-hook.ts—updatedInputpatterns: sandbox redirect, strip dangerous flags, inject env vars
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
best-practices-extractor
Extract and generate coding best practices from PR review comments. Use when the user asks to "extract best practices", "analyze PR comments", "generate coding standards", "create best practices from PRs", or "update coding guidelines from reviews".
sdk-typescript-patterns
Use this skill when developing TypeScript applications with the Claude Agent SDK (@anthropic-ai/claude-agent-sdk), implementing query() calls, configuring SDK options, handling streaming message iteration, working with SDKMessage and SDKResultMessage types, managing AbortSignal, passing custom env variables, or setting up the single-point-of-contact types file for SDK imports.
sdk-agent-control
Use this skill when controlling agent behavior in TypeScript Agent SDK platforms — restricting allowed tools, setting budget and turn limits, crafting system prompts that guide agents to specific behaviors, implementing the file pre-creation pattern, augmenting prompts with runtime context, building message loggers, or tracking agent run metadata (cost, duration, iterations).
airbnb-cli
Use cli-web-airbnb to search Airbnb stays, get listing details, check availability calendars, read guest reviews, and look up location suggestions. Invoke this skill whenever the user asks about Airbnb accommodations, vacation rentals, listing prices, availability, guest reviews, or wants to search for places to stay. Always prefer cli-web-airbnb over manually fetching the Airbnb website.
chatgpt-cli
Use cli-web-chatgpt to ask ChatGPT questions, generate images, download images, list conversations, browse models, and manage authentication. Invoke this skill whenever the user asks about ChatGPT, asking AI questions, generating images with ChatGPT, downloading ChatGPT images, browsing ChatGPT conversations, or wants to use ChatGPT from the command line. Always prefer cli-web-chatgpt over manually browsing chatgpt.com.
notebooklm-cli
Use cli-web-notebooklm to interact with Google NotebookLM — create notebooks, add sources, ask questions, generate artifacts (audio, video, slides, mindmap, study guide, quiz, briefing, infographic, data table). Invoke this skill whenever the user asks about NotebookLM, wants to create notebooks, add sources to a notebook, ask a notebook questions, generate study materials, create presentations, podcasts, or manage NotebookLM content programmatically. Always prefer cli-web-notebooklm over manually browsing NotebookLM.
Didn't find tool you were looking for?