From agent-sdk-pro
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.
npx claudepluginhub itamarzand88/claude-code-agentic-engineering --plugin agent-sdk-proThis skill uses the workspace's default tool permissions.
**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.
examples/auto-format-hook.tsexamples/env-protection-hook.tsexamples/file-restriction-hook.tsexamples/input-redirect-hook.tsexamples/security-blocker-hook.tsexamples/smart-dispatch-hook.tsreferences/hook-events-reference.mdreferences/posttooluse-patterns.mdreferences/pretooluse-patterns.mdreferences/smart-dispatch-pattern.mdreferences/testing-hooks.mdSearches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Guides MCP server integration in Claude Code plugins via .mcp.json or plugin.json configs for stdio, SSE, HTTP types, enabling external services as tools.
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.
// 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;
};
};
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
};
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`,
},
};
};
}
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.",
},
};
};
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)}`,
},
};
}
};
}
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] }] : []),
],
},
},
});
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();
}
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";
}
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
},
},
};
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 (default true)stopReason: string — message shown when continue is falsesuppressOutput: boolean — hide hook stdout from transcriptsystemMessage: string — inject a message directly into Claude's conversationHandle 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.`,
};
};
signal.aborted first — prevents work on cancelled operationsHookInput, guard to the specific type{} for non-applicable cases — empty output = allow/no-opnull when unavailabletool_input types — always cast safely via helperssignal to fetch() — so HTTP requests cancel properly on hook timeoutinput.hook_event_name — not hardcoded strings in hookEventName fieldFor 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, mocking execSyncExamples:
examples/env-protection-hook.ts — .env file 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 — updatedInput patterns: sandbox redirect, strip dangerous flags, inject env vars