Define Claude Code hooks with full type safety using TypeScript.
See examples for more usage examples.
- cc-hooks-ts
Note
Starting with versions 2.0.42, we will raise our version number to match Claude Code whenever Hook-related changes occur.
This ensures we can adopt newer type definitions while maintaining compatibility.
# npm
npm i cc-hooks-ts
# yarn
yarn add cc-hooks-ts
# pnpm
pnpm add cc-hooks-ts
# Bun
bun add cc-hooks-ts
# Deno
deno add npm:cc-hooks-tsimport { defineHook } from "cc-hooks-ts";
const hook = defineHook({
// Specify the event(s) that trigger this hook.
trigger: {
SessionStart: true,
},
// Implement what you want to do.
run: (context) => {
// Do something great here
return context.success({
messageForUser: "Welcome to your coding session!",
});
},
});
// import.meta.main is available in Node.js 24.2+ and Bun and Deno
if (import.meta.main) {
const { runHook } = await import("cc-hooks-ts");
await runHook(hook);
}Then, load defined hooks in your Claude Code settings at ~/.claude/settings.json.
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "bun run -i --silent path/to/your/sessionHook.ts"
}
]
}
]
}
}In PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, and PermissionDenied events, you can define hooks specific to tools by specifying tool names in the trigger configuration.
For example, you can create a hook that only runs before the Read tool is used:
const preReadHook = defineHook({
trigger: { PreToolUse: { Read: true } },
run: (context) => {
// context.input.tool_input is typed as { file_path: string; limit?: number; offset?: number; }
const { file_path } = context.input.tool_input;
if (file_path.includes(".env")) {
return context.blockingError("Cannot read environment files");
}
return context.success();
},
});
if (import.meta.main) {
const { runHook } = await import("cc-hooks-ts");
await runHook(preReadHook);
}Then configure it in Claude Code settings:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "bun run -i --silent path/to/your/preReadHook.ts"
}
]
}
]
}
}The same trigger shape also works for permission hooks:
const permissionRequestHook = defineHook({
trigger: { PermissionRequest: { Bash: true } },
run: (context) => {
// context.input.tool_input is typed as BashInput
const { command } = context.input.tool_input;
if (command.includes("rm -rf")) {
return context.blockingError("Refusing destructive command");
}
return context.success();
},
});You can add support for custom tools by extending the tool type definitions.
This is useful when you want to your MCP-defined tools to have type-safe hook inputs.
import { defineHook } from "cc-hooks-ts";
// Example: type-safe hooks for DeepWiki MCP Server tools
declare module "cc-hooks-ts" {
interface ToolSchema {
mcp__deepwiki__ask_question: {
input: {
question: string;
repoName: string;
};
response: unknown;
};
}
}
const deepWikiHook = defineHook({
trigger: { PreToolUse: { mcp__deepwiki__ask_question: true } },
run: (context) => {
// context.input.tool_input is typed as { question: string; repoName: string; }
const { question, repoName } = context.input.tool_input;
if (question.length > 500) {
return context.blockingError("Question is too long");
}
return context.success();
},
});You can conditionally execute hooks based on runtime logic using the shouldRun function.
If shouldRun returns false, the hook will be skipped.
import { defineHook } from "cc-hooks-ts";
const hook = defineHook({
trigger: {
Notification: true,
},
// Only run this hook on macOS
shouldRun: () => process.platform === "darwin",
run: (context) => {
// Some macOS-specific logic like sending a notification using AppleScript
return context.success();
},
});Use context.json() to return structured JSON output with advanced control over hook behavior.
For detailed information about available JSON fields and their behavior, see the official documentation.
Warning
This behavior is undocumented by Anthropic and may change.
Caution
You must enable verbose output if you want to see async hook outputs like systemMessage or hookSpecificOutput.additionalContext.
You can enable it in Claude Code by going to /config and setting "verbose" to true.
Async JSON output allows hooks to perform longer computations without blocking the Claude Code TUI.
You can use context.defer() to respond Claude Code immediately while performing longer computations in the background.
You should complete the async operation within a reasonable time (e.g. 15 seconds).
import { defineHook } from "cc-hooks-ts";
const hook = defineHook({
trigger: { PostToolUse: { Read: true } },
run: (context) =>
context.defer(
async () => {
// Simulate long-running computation
await new Promise((resolve) => setTimeout(resolve, 2000));
return {
event: "PostToolUse",
output: {
systemMessage: "Read tool used successfully after async processing!",
},
};
},
{
timeoutMs: 5000, // Optional timeout for the async operation.
},
),
});For more detailed information about Claude Code hooks, visit the official documentation.
# Run tests
pnpm test
# Build
pnpm build
# Lint
pnpm lint
# Format
pnpm format
# Type check
pnpm typecheckDependabot automatically creates PRs to bump @anthropic-ai/claude-agent-sdk. The CI bot posts a type diff comment on each PR.
-
Find the Dependabot PR that bumps
@anthropic-ai/claude-agent-sdkand check out its branch. -
Read the type diff posted as a PR comment, then reflect the changes.
- Edit
src/hooks/for changed hook input / output types.- No need for adding tests in most cases since we are testing the whole type definitions in these files:
src/hooks/input/schemas.test-d.tssrc/hooks/output/index.test-d.tssrc/hooks/event.test-d.tssrc/hooks/permission.test-d.ts
- No need for adding tests in most cases since we are testing the whole type definitions in these files:
- Edit
src/index.tsfor changed tool input / output types. - YOU SHOULD NOT MODIFY
versioninpackage.jsonmanually.
- Edit
-
Push to the Dependabot PR branch.
-
Update the PR title to:
fix: update to parity with Claude Code v$(npm info @anthropic-ai/claude-agent-sdk claudeCodeVersion)
-
Create a new branch and bump
@anthropic-ai/claude-agent-sdkto the latest version:git switch -c bump-claude-agent-sdk pnpm add @anthropic-ai/claude-agent-sdk@latest
-
Get the type diff between the old and new versions:
npm diff --diff=@anthropic-ai/claude-agent-sdk@<old_version> --diff=@anthropic-ai/claude-agent-sdk@<new_version> '**/*.d.ts'
-
Reflect the changes (same as step 2 above).
-
Commit, push, and create a PR with the title:
fix: update to parity with Claude Code v$(npm info @anthropic-ai/claude-agent-sdk claudeCodeVersion)
MIT
We welcome contributions! Feel free to open issues or submit pull requests.
Made with ❤️ for hackers using Claude Code