| name | cc-hooks-ts:cc-hooks-ts |
| description | This skill is the mandatory reference for all Claude Code hook creation. Use cc-hooks-ts for every hook. This skill should be used when the user asks to "create a hook", "add a hook", "write a hook", "implement a hook", "rewrite hooks in TypeScript", "use cc-hooks-ts", or needs to build any Claude Code hook — cc-hooks-ts is always required regardless of whether explicitly mentioned. |
Creating Claude Code Hooks with cc-hooks-ts
cc-hooks-ts is mandatory for all hook development. Never write raw hook scripts without it.
Install
bun add cc-hooks-ts
Basic Hook Structure
Every hook follows this pattern:
#!/usr/bin/env bun
import { defineHook } from "cc-hooks-ts";
const hook = defineHook({
trigger: {
PostToolUse: {
Write: true,
Edit: true,
},
},
shouldRun: () => process.platform === "darwin",
run: (context) => {
return context.success({
messageForUser: "Hook executed successfully",
});
},
});
if (import.meta.main) {
const { runHook } = await import("cc-hooks-ts");
await runHook(hook);
}
Key elements:
- Shebang
#!/usr/bin/env bun — hooks run as standalone executables
defineHook() — provides full type inference for trigger, input, and response
runHook() via dynamic import — handles stdin parsing, validation, context creation, and output formatting
if (import.meta.main) guard — allows importing the hook definition in tests without side effects
Supported Events
SessionStart — Session begins
- Use Case: Environment checks, dependency install
SessionEnd — Session ends
- Use Case: Cleanup, state persistence
PreToolUse — Before tool execution
- Use Case: Block operations, validate input, modify input
PostToolUse — After tool execution
- Use Case: Logging, inject additional context
PostToolUseFailure — After tool failure
- Use Case: Error handling, retry guidance
UserPromptSubmit — User submits prompt
- Use Case: Prompt augmentation, blocking
Stop — Claude stops processing
- Use Case: Cleanup, notifications
SubagentStart — Subagent spawns
- Use Case: Subagent initialization
SubagentStop — Subagent stops
- Use Case: Subagent result processing
Notification — System notification
PermissionRequest — Permission requested
- Use Case: Auto-approve/deny rules
PreCompact — Before context compaction
- Use Case: State preservation
PostCompact — After context compaction
- Use Case: Post-compaction processing
Setup — Setup/maintenance trigger
- Use Case: One-time setup tasks
Tool-Specific Triggers
Filter hooks to specific tools by listing them in the trigger config:
const hook = defineHook({
trigger: {
PreToolUse: {
Read: true,
Bash: true,
},
},
run: (context) => {
return context.success({});
},
});
Context API
Response Methods
context.success(payload?) — 0
- Behavior: Continue normally. Optional
messageForUser and additionalClaudeContext.
context.blockingError(message) — 2
- Behavior: Stop execution. Error message fed to Claude.
context.nonBlockingError(message?) — 1
- Behavior: Show warning to user, continue execution.
context.json(payload) — 0
- Behavior: Full control over hook output — set
permissionDecision, additionalContext, suppressOutput, etc.
context.defer(handler, opts?) — 0
- Behavior: Deferred async processing with optional
timeoutMs.
context.input
Strongly typed based on trigger configuration:
context.input.cwd;
context.input.session_id;
context.input.transcript_path;
context.input.hook_event_name;
context.input.tool_name;
context.input.tool_input;
context.input.tool_response;
shouldRun
Optional conditional execution — boolean or function (sync/async):
shouldRun: () => process.env.HOOKS_ENABLED === "true";
shouldRun: false;
If it returns false, the hook exits with code 0 (skipped silently).
context.json() Response Structure
Use context.json() for advanced control. The structure varies by event — see references/response-patterns.md for full details.
PreToolUse — control permission and modify input:
return context.json({
event: "PreToolUse",
output: {
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "allow" | "ask" | "deny",
permissionDecisionReason: "Auto-approved by policy",
updatedInput: { file_path: "/corrected/path" },
additionalContext: "Message for Claude",
},
},
});
PostToolUse — inject context after tool runs:
return context.json({
event: "PostToolUse",
output: {
hookSpecificOutput: {
hookEventName: "PostToolUse",
additionalContext: "Additional context for Claude",
},
suppressOutput: true,
},
});
plugin.json Configuration
Register hooks in plugin.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "../../hooks/my-hook.ts"
}
]
}
]
}
}
- Use
../.. for plugin-relative paths
- The
matcher field filters at event dispatch level; the trigger config in defineHook filters at code level
- Hook files must have executable permission (
chmod +x)
Custom Tool Types
Extend type definitions for MCP or custom tools via declaration merging:
declare module "cc-hooks-ts" {
interface ToolSchema {
my_custom_tool: {
input: { query: string };
response: { result: string };
};
}
}
Then context.input.tool_input is typed as { query: string } when triggered for my_custom_tool.
Stop Hook Recursion Guard (stop_hook_active)
When a Stop hook returns additionalContext or blocks the stop, Claude may continue processing and eventually stop again — triggering the same Stop hook recursively. To prevent this infinite loop, Claude Code sets stop_hook_active: true in the input on the second invocation.
Always check this flag in Stop hooks:
const hook = defineHook({
trigger: { Stop: true },
run: (context) => {
if (context.input.stop_hook_active) {
return context.success({});
}
return context.success({
additionalClaudeContext: "Remember to push before ending",
});
},
});
Without this guard, a Stop hook that injects context will cause Claude to resume, stop again, trigger the hook again, and loop indefinitely.
Best Practices
- Use
console.error() for user-visible output — stdout is reserved for hook JSON responses
- Implement cooldown for frequently triggered hooks to avoid performance impact
- Keep hooks lightweight — hook execution blocks Claude's processing
- Design for idempotency — same input should produce same result
- Wrap with try-catch — unhandled errors cause hook failures
- Guard Stop hooks against recursion — always check
stop_hook_active (see above)
References
Bundled Resources
References
references/response-patterns.md — Complete context.json() response structures for all hook events — PreToolUse, PostToolUse, UserPromptSubmit, PermissionRequest, Stop, and more