cc-hooks-ts 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 sushichan044
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,217 @@
1
+ # cc-hooks-ts
2
+
3
+ Define Claude Code hooks with full type safety using TypeScript and Valibot validation.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npx npym add cc-hooks-ts
9
+ ```
10
+
11
+ ## Basic Usage
12
+
13
+ > [!NOTE]
14
+ > We highly recommend using Bun or Deno for automatic dependency downloading at runtime.
15
+ >
16
+ > - Deno: <https://docs.deno.com/runtime/fundamentals/modules/#managing-third-party-modules-and-libraries>
17
+ > - Bun: <https://bun.com/docs/runtime/autoimport>
18
+
19
+ ### Define a Hook
20
+
21
+ With Deno:
22
+
23
+ ```typescript
24
+ #!/usr/bin/env -S deno run --quiet --allow-env --allow-read
25
+ import { defineHook, runHook } from "cc-hooks-ts";
26
+
27
+ // Session start hook
28
+ const sessionHook = defineHook({
29
+ trigger: { SessionStart: true },
30
+ run: (context) => {
31
+ console.log(`Session started: ${context.input.session_id}`);
32
+ return context.success({
33
+ messageForUser: "Welcome to your coding session!"
34
+ });
35
+ }
36
+ });
37
+
38
+ await runHook(sessionHook);
39
+ ```
40
+
41
+ Or with Bun:
42
+
43
+ ```typescript
44
+ #!/usr/bin/env -S bun run --silent
45
+ import { defineHook, runHook } from "cc-hooks-ts";
46
+
47
+ const sessionHook = defineHook({
48
+ trigger: { SessionStart: true },
49
+ run: (context) => {
50
+ return context.success({
51
+ messageForUser: "Session started!"
52
+ });
53
+ }
54
+ });
55
+
56
+ await runHook(sessionHook);
57
+ ```
58
+
59
+ ### Call from Claude Code
60
+
61
+ Then, load defined hooks in your Claude Code settings at `~/.claude/settings.json`.
62
+
63
+ ```json
64
+ {
65
+ "hooks": {
66
+ "SessionStart": [
67
+ {
68
+ "hooks": [
69
+ {
70
+ "type": "command",
71
+ "command": "$HOME/.claude/<name-of-your-hooks>.ts"
72
+ }
73
+ ]
74
+ }
75
+ ]
76
+ }
77
+ }
78
+ ```
79
+
80
+ ## Custom Tool Type Support
81
+
82
+ For better type inference in PreToolUse and PostToolUse hooks, you can extend the `ToolSchema` interface to define your own tool types:
83
+
84
+ ### Adding Custom Tool Definitions
85
+
86
+ Extend the `ToolSchema` interface to add custom tool definitions:
87
+
88
+ ```typescript
89
+ declare module "cc-hooks-ts" {
90
+ interface ToolSchema {
91
+ MyCustomTool: {
92
+ input: {
93
+ customParam: string;
94
+ optionalParam?: number;
95
+ };
96
+ response: {
97
+ result: string;
98
+ };
99
+ };
100
+ }
101
+ }
102
+ ```
103
+
104
+ Now you can use your custom tool with full type safety:
105
+
106
+ ```typescript
107
+ const customToolHook = defineHook({
108
+ trigger: { PreToolUse: { MyCustomTool: true } },
109
+ run: (context) => {
110
+ // context.input.tool_input is typed as { customParam: string; optionalParam?: number; }
111
+ const { customParam, optionalParam } = context.input.tool_input;
112
+ return context.success();
113
+ }
114
+ });
115
+ ```
116
+
117
+ ## API Reference
118
+
119
+ ### defineHook Function
120
+
121
+ Creates type-safe hook definitions with full TypeScript inference:
122
+
123
+ ```typescript
124
+ // Tool-specific PreToolUse hook
125
+ const readHook = defineHook({
126
+ trigger: { PreToolUse: { Read: true } },
127
+ run: (context) => {
128
+ // context.input.tool_input is typed as { file_path: string }
129
+ const { file_path } = context.input.tool_input;
130
+
131
+ if (file_path.includes('.env')) {
132
+ return context.blockingError('Cannot read environment files');
133
+ }
134
+
135
+ return context.success();
136
+ }
137
+ });
138
+
139
+ // Multiple event triggers
140
+ const multiEventHook = defineHook({
141
+ trigger: {
142
+ PreToolUse: { Read: true, WebFetch: true },
143
+ PostToolUse: { Read: true }
144
+ },
145
+ shouldRun: () => process.env.NODE_ENV === 'development',
146
+ run: (context) => {
147
+ // Handle different events and tools based on context.input
148
+ return context.success();
149
+ }
150
+ });
151
+ ```
152
+
153
+ ### runHook Function
154
+
155
+ Executes hooks with complete lifecycle management:
156
+
157
+ - Reads input from stdin
158
+ - Validates input using Valibot schemas
159
+ - Creates typed context
160
+ - Executes hook handler
161
+ - Formats and outputs results
162
+
163
+ ```typescript
164
+ await runHook(hook);
165
+ ```
166
+
167
+ ### Hook Context
168
+
169
+ The context provides strongly typed input access and response helpers:
170
+
171
+ ```typescript
172
+ run: (context) => {
173
+ // Typed input based on trigger configuration
174
+ const input = context.input;
175
+
176
+ // Response helpers
177
+ return context.success({ messageForUser: "Success!" });
178
+ // or context.blockingError("Error occurred");
179
+ // or context.nonBlockingError("Warning message");
180
+ // or context.json({ event: "EventName", output: {...} });
181
+ }
182
+ ```
183
+
184
+ ## Development
185
+
186
+ ```bash
187
+ # Run tests
188
+ pnpm test
189
+
190
+ # Build
191
+ pnpm build
192
+
193
+ # Lint
194
+ pnpm lint
195
+
196
+ # Format
197
+ pnpm format
198
+
199
+ # Type check
200
+ pnpm typecheck
201
+ ```
202
+
203
+ ## Documentation
204
+
205
+ For more detailed information about Claude Code hooks, visit the [official documentation](https://docs.anthropic.com/en/docs/claude-code/hooks).
206
+
207
+ ## License
208
+
209
+ MIT
210
+
211
+ ## Contributing
212
+
213
+ We welcome contributions! Feel free to open issues or submit pull requests.
214
+
215
+ ---
216
+
217
+ **Made with ❤️ for hackers using Claude Code**
@@ -0,0 +1,562 @@
1
+ import * as v from "valibot";
2
+
3
+ //#region src/event.d.ts
4
+ declare const SUPPORTED_HOOK_EVENTS: readonly ["PreToolUse", "PostToolUse", "Notification", "UserPromptSubmit", "Stop", "SubagentStop", "PreCompact", "SessionStart", "SessionEnd"];
5
+ /**
6
+ * @see {@link https://docs.anthropic.com/en/docs/claude-code/hooks#hook-events}
7
+ */
8
+ type SupportedHookEvent = (typeof SUPPORTED_HOOK_EVENTS)[number];
9
+ //#endregion
10
+ //#region src/output.d.ts
11
+ type HookOutput = {
12
+ PreToolUse: PreToolUseHookOutput;
13
+ PostToolUse: PostToolUseHookOutput;
14
+ UserPromptSubmit: UserPromptSubmitHookOutput;
15
+ Stop: StopHookOutput;
16
+ SubagentStop: SubagentStopHookOutput;
17
+ SessionStart: SessionStartHookOutput;
18
+ Notification: CommonHookOutputs;
19
+ PreCompact: CommonHookOutputs;
20
+ SessionEnd: CommonHookOutputs;
21
+ };
22
+ type ExtractHookOutput<TEvent extends SupportedHookEvent> = HookOutput extends Record<SupportedHookEvent, unknown> ? HookOutput[TEvent] : never;
23
+ /**
24
+ * Common fields of hook outputs
25
+ *
26
+ * @see {@link https://docs.anthropic.com/en/docs/claude-code/hooks#common-json-fields}
27
+ */
28
+ type CommonHookOutputs = {
29
+ /**
30
+ * Whether Claude should continue after hook execution
31
+ *
32
+ * If `continue` is false, Claude stops processing after the hooks run.
33
+ * @default true
34
+ */
35
+ continue?: boolean;
36
+ /**
37
+ * Accompanies `continue` with a `reason` shown to the user, not shown to Claude.
38
+ *
39
+ * NOT FOR CLAUDE
40
+ */
41
+ stopReason?: string;
42
+ /**
43
+ * If `true`, user cannot see the stdout of this hook.
44
+ *
45
+ * @default false
46
+ */
47
+ suppressOutput?: boolean;
48
+ /**
49
+ * Optional warning message shown to the user
50
+ */
51
+ systemMessage?: string;
52
+ };
53
+ /**
54
+ * @see {@link https://docs.anthropic.com/en/docs/claude-code/hooks#pretooluse-decision-control}
55
+ */
56
+ interface PreToolUseHookOutput extends CommonHookOutputs {
57
+ /**
58
+ * - `block` automatically prompts Claude with `reason`.
59
+ * - `undefined` does nothing. `reason` is ignored.
60
+ */
61
+ decision: "block" | undefined;
62
+ hookSpecificOutput?: {
63
+ hookEventName: "PreToolUse";
64
+ /**
65
+ * Adds context for Claude to consider.
66
+ */
67
+ additionalContext?: string;
68
+ };
69
+ reason?: string;
70
+ }
71
+ /**
72
+ * @see {@link https://docs.anthropic.com/en/docs/claude-code/hooks#posttooluse-decision-control}
73
+ */
74
+ interface PostToolUseHookOutput extends CommonHookOutputs {
75
+ /**
76
+ * - `block` automatically prompts Claude with `reason`.
77
+ * - `undefined` does nothing. `reason` is ignored.
78
+ */
79
+ decision: "block" | undefined;
80
+ hookSpecificOutput?: {
81
+ hookEventName: "PostToolUse";
82
+ /**
83
+ * Adds context for Claude to consider.
84
+ */
85
+ additionalContext?: string;
86
+ };
87
+ reason?: string;
88
+ }
89
+ /**
90
+ * @see {@link https://docs.anthropic.com/en/docs/claude-code/hooks#userpromptsubmit-decision-control}
91
+ */
92
+ interface UserPromptSubmitHookOutput extends CommonHookOutputs {
93
+ /**
94
+ * - `block` prevents the prompt from being processed.
95
+ * The submitted prompt is erased from context. `reason` is shown to the user but not added to context.
96
+ *
97
+ * - `undefined` allows the prompt to proceed normally. `reason` is ignored.
98
+ */
99
+ decision?: "block" | undefined;
100
+ hookSpecificOutput?: {
101
+ hookEventName: "UserPromptSubmit";
102
+ /**
103
+ * Adds the string to the context if not blocked.
104
+ */
105
+ additionalContext?: string;
106
+ };
107
+ reason?: string;
108
+ }
109
+ /**
110
+ * @see {@link https://docs.anthropic.com/en/docs/claude-code/hooks#stop%2Fsubagentstop-decision-control}
111
+ */
112
+ interface StopHookOutput extends CommonHookOutputs {
113
+ /**
114
+ * - `block` prevents Claude from stopping. You must populate `reason` for Claude to know how to proceed.
115
+ *
116
+ * - `undefined` allows Claude to stop. `reason` is ignored.
117
+ */
118
+ decision: "block" | undefined;
119
+ /**
120
+ * Reason for the decision.
121
+ */
122
+ reason?: string;
123
+ }
124
+ /**
125
+ * @see {@link https://docs.anthropic.com/en/docs/claude-code/hooks#stop%2Fsubagentstop-decision-control}
126
+ */
127
+ interface SubagentStopHookOutput extends CommonHookOutputs {
128
+ /**
129
+ * - `block` prevents Claude from stopping. You must populate `reason` for Claude to know how to proceed.
130
+ *
131
+ * - `undefined` allows Claude to stop. `reason` is ignored.
132
+ */
133
+ decision: "block" | undefined;
134
+ /**
135
+ * Reason for the decision.
136
+ */
137
+ reason?: string;
138
+ }
139
+ interface SessionStartHookOutput extends CommonHookOutputs {
140
+ hookSpecificOutput?: {
141
+ hookEventName: "SessionStart";
142
+ /**
143
+ * Adds the string to the context.
144
+ */
145
+ additionalContext?: string;
146
+ };
147
+ }
148
+ //#endregion
149
+ //#region src/input/schemas.d.ts
150
+ /**
151
+ * @package
152
+ */
153
+ declare const HookInputSchemas: {
154
+ readonly PreToolUse: v.ObjectSchema<{
155
+ readonly hook_event_name: v.LiteralSchema<"PreToolUse", undefined>;
156
+ readonly cwd: v.StringSchema<undefined>;
157
+ readonly session_id: v.StringSchema<undefined>;
158
+ readonly transcript_path: v.StringSchema<undefined>;
159
+ } & {
160
+ tool_input: v.UnknownSchema;
161
+ tool_name: v.IntersectSchema<[v.StringSchema<undefined>, v.RecordSchema<v.StringSchema<undefined>, v.NeverSchema<undefined>, undefined>], undefined>;
162
+ }, undefined>;
163
+ readonly PostToolUse: v.ObjectSchema<{
164
+ readonly hook_event_name: v.LiteralSchema<"PostToolUse", undefined>;
165
+ readonly cwd: v.StringSchema<undefined>;
166
+ readonly session_id: v.StringSchema<undefined>;
167
+ readonly transcript_path: v.StringSchema<undefined>;
168
+ } & {
169
+ tool_input: v.UnknownSchema;
170
+ tool_name: v.StringSchema<undefined>;
171
+ tool_response: v.UnknownSchema;
172
+ }, undefined>;
173
+ readonly Notification: v.ObjectSchema<{
174
+ readonly hook_event_name: v.LiteralSchema<"Notification", undefined>;
175
+ readonly cwd: v.StringSchema<undefined>;
176
+ readonly session_id: v.StringSchema<undefined>;
177
+ readonly transcript_path: v.StringSchema<undefined>;
178
+ } & {
179
+ message: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
180
+ }, undefined>;
181
+ readonly UserPromptSubmit: v.ObjectSchema<{
182
+ readonly hook_event_name: v.LiteralSchema<"UserPromptSubmit", undefined>;
183
+ readonly cwd: v.StringSchema<undefined>;
184
+ readonly session_id: v.StringSchema<undefined>;
185
+ readonly transcript_path: v.StringSchema<undefined>;
186
+ } & {
187
+ prompt: v.StringSchema<undefined>;
188
+ }, undefined>;
189
+ readonly Stop: v.ObjectSchema<{
190
+ readonly hook_event_name: v.LiteralSchema<"Stop", undefined>;
191
+ readonly cwd: v.StringSchema<undefined>;
192
+ readonly session_id: v.StringSchema<undefined>;
193
+ readonly transcript_path: v.StringSchema<undefined>;
194
+ } & {
195
+ stop_hook_active: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
196
+ }, undefined>;
197
+ readonly SubagentStop: v.ObjectSchema<{
198
+ readonly hook_event_name: v.LiteralSchema<"SubagentStop", undefined>;
199
+ readonly cwd: v.StringSchema<undefined>;
200
+ readonly session_id: v.StringSchema<undefined>;
201
+ readonly transcript_path: v.StringSchema<undefined>;
202
+ } & {
203
+ stop_hook_active: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
204
+ }, undefined>;
205
+ readonly PreCompact: v.ObjectSchema<{
206
+ readonly hook_event_name: v.LiteralSchema<"PreCompact", undefined>;
207
+ readonly cwd: v.StringSchema<undefined>;
208
+ readonly session_id: v.StringSchema<undefined>;
209
+ readonly transcript_path: v.StringSchema<undefined>;
210
+ } & {
211
+ custom_instructions: v.StringSchema<undefined>;
212
+ trigger: v.UnionSchema<[v.LiteralSchema<"manual", undefined>, v.LiteralSchema<"auto", undefined>], undefined>;
213
+ }, undefined>;
214
+ readonly SessionStart: v.ObjectSchema<{
215
+ readonly hook_event_name: v.LiteralSchema<"SessionStart", undefined>;
216
+ readonly cwd: v.StringSchema<undefined>;
217
+ readonly session_id: v.StringSchema<undefined>;
218
+ readonly transcript_path: v.StringSchema<undefined>;
219
+ } & {
220
+ source: v.StringSchema<undefined>;
221
+ }, undefined>;
222
+ readonly SessionEnd: v.ObjectSchema<{
223
+ readonly hook_event_name: v.LiteralSchema<"SessionEnd", undefined>;
224
+ readonly cwd: v.StringSchema<undefined>;
225
+ readonly session_id: v.StringSchema<undefined>;
226
+ readonly transcript_path: v.StringSchema<undefined>;
227
+ } & {
228
+ reason: v.StringSchema<undefined>;
229
+ }, undefined>;
230
+ };
231
+ //#endregion
232
+ //#region src/input/types.d.ts
233
+ /**
234
+ * Internal type that combines base hook inputs with tool-specific inputs for PreToolUse events.
235
+ * For non-PreToolUse events, this is equivalent to BaseHookInputs.
236
+ *
237
+ * @example
238
+ * ```ts
239
+ * type PreToolUseInputs = HookInputs["PreToolUse"]
240
+ * // Result: Base inputs + { Read: SpecifiedToolUseInput<"Read">, WebFetch: SpecifiedToolUseInput<"WebFetch"> }
241
+ * ```
242
+ * @package
243
+ */
244
+ type HookInputs = { [EventKey in SupportedHookEvent]: EventKey extends "PreToolUse" ? ToolSpecificPreToolUseInput & {
245
+ default: BaseHookInputs["PreToolUse"];
246
+ } : EventKey extends "PostToolUse" ? ToolSpecificPostToolUseInput & {
247
+ default: BaseHookInputs["PostToolUse"];
248
+ } : {
249
+ default: BaseHookInputs[EventKey];
250
+ } };
251
+ /**
252
+ * Extracts all possible hook input types for a specific event type.
253
+ * For non-tool-specific events, this returns only the default input type.
254
+ * For tool-specific events (PreToolUse/PostToolUse), this returns a union of all possible inputs including default and tool-specific variants.
255
+ *
256
+ * @example
257
+ * ```ts
258
+ * // For non-tool-specific events
259
+ * type SessionStartInputs = ExtractAllHookInputsForEvent<"SessionStart">
260
+ * // Result: { cwd: string; hook_event_name: "SessionStart"; session_id: string; transcript_path: string }
261
+ *
262
+ * // For tool-specific events
263
+ * type PreToolUseInputs = ExtractAllHookInputsForEvent<"PreToolUse">
264
+ * // Result: Union of default input + all tool-specific inputs (Read, WebFetch, etc.)
265
+ * ```
266
+ * @package
267
+ */
268
+ type ExtractAllHookInputsForEvent<TEvent extends SupportedHookEvent> = { [K in keyof HookInputs[TEvent]]: HookInputs[TEvent][K] }[keyof HookInputs[TEvent]];
269
+ /**
270
+ * Extracts the hook input type for a specific tool within a given event type.
271
+ * This type utility is used to get strongly-typed inputs for tool-specific hook handlers.
272
+ * The second parameter is constrained to valid tool names for the given event type.
273
+ *
274
+ * @example
275
+ * ```ts
276
+ * // Extract Read tool input for PreToolUse event
277
+ * type ReadPreInput = ExtractSpecificHookInputForEvent<"PreToolUse", "Read">
278
+ * // Result: { cwd: string; hook_event_name: "PreToolUse"; session_id: string; transcript_path: string; tool_input: ReadInput; tool_name: "Read" }
279
+ *
280
+ * // Extract WebFetch tool input for PostToolUse event
281
+ * type WebFetchPostInput = ExtractSpecificHookInputForEvent<"PostToolUse", "WebFetch">
282
+ * // Result: { cwd: string; hook_event_name: "PostToolUse"; session_id: string; transcript_path: string; tool_input: WebFetchInput; tool_name: "WebFetch"; tool_response: WebFetchResponse }
283
+ *
284
+ * // Type error: "Read" is not valid for non-tool-specific events
285
+ * type Invalid = ExtractSpecificHookInputForEvent<"SessionStart", "Read"> // ❌ Compile error
286
+ * ```
287
+ * @package
288
+ */
289
+ type ExtractSpecificHookInputForEvent<TEvent extends SupportedHookEvent, TSpecificKey extends ExtractExtendedSpecificKeys<TEvent>> = { [K in keyof HookInputs[TEvent]]: K extends TSpecificKey ? HookInputs[TEvent][K] : never }[keyof HookInputs[TEvent]];
290
+ /**
291
+ * @package
292
+ */
293
+ type ExtractExtendedSpecificKeys<TEvent extends SupportedHookEvent> = Exclude<keyof HookInputs[TEvent], "default">;
294
+ type BaseHookInputs = { [EventKey in SupportedHookEvent]: v.InferOutput<(typeof HookInputSchemas)[EventKey]> };
295
+ type ToolSpecificPreToolUseInput = { [K in keyof ToolSchema]: Omit<BaseHookInputs["PreToolUse"], "tool_input" | "tool_name"> & {
296
+ tool_input: ToolSchema[K]["input"];
297
+ tool_name: K;
298
+ } };
299
+ type ToolSpecificPostToolUseInput = { [K in keyof ToolSchema]: Omit<BaseHookInputs["PostToolUse"], "tool_input" | "tool_name" | "tool_response"> & {
300
+ tool_input: ToolSchema[K]["input"];
301
+ tool_name: K;
302
+ tool_response: ToolSchema[K]["response"];
303
+ } };
304
+ //#endregion
305
+ //#region src/types.d.ts
306
+ type HookTrigger = Partial<{ [TEvent in SupportedHookEvent]: true | Partial<{ [SchemaKey in ExtractExtendedSpecificKeys<TEvent>]?: true }> }>;
307
+ type ExtractTriggeredHookInput<TTrigger extends HookTrigger> = { [EventKey in keyof TTrigger]: EventKey extends SupportedHookEvent ? TTrigger[EventKey] extends true ? ExtractAllHookInputsForEvent<EventKey> : TTrigger[EventKey] extends Record<PropertyKey, true> ? { [SpecificKey in keyof TTrigger[EventKey]]: SpecificKey extends ExtractExtendedSpecificKeys<EventKey> ? ExtractSpecificHookInputForEvent<EventKey, SpecificKey> : never }[keyof TTrigger[EventKey]] : never : never }[keyof TTrigger];
308
+ //#endregion
309
+ //#region src/context.d.ts
310
+ interface HookContext<THookTrigger extends HookTrigger> {
311
+ /**
312
+ * Cause a blocking error.
313
+ *
314
+ * @param error `error` is fed back to Claude to process automatically
315
+ */
316
+ blockingError: (error: string) => HookResponseBlockingError;
317
+ input: ExtractTriggeredHookInput<THookTrigger>;
318
+ /**
319
+ * Direct access to advanced JSON output.
320
+ *
321
+ * @see {@link https://docs.anthropic.com/en/docs/claude-code/hooks#advanced%3A-json-output}
322
+ */
323
+ json: (payload: HookResultJSON<THookTrigger>) => HookResponseJSON<THookTrigger>;
324
+ /**
325
+ * Cause a non-blocking error.
326
+ *
327
+ * @param message `message` is shown to the user and execution continues.
328
+ */
329
+ nonBlockingError: (message?: string) => HookResponseNonBlockingError;
330
+ /**
331
+ * Indicate successful handling of the hook.
332
+ */
333
+ success: (payload?: HookSuccessPayload) => HookResponseSuccess;
334
+ }
335
+ type HookResponse<THookTrigger extends HookTrigger> = HookResponseBlockingError | HookResponseJSON<THookTrigger> | HookResponseNonBlockingError | HookResponseSuccess;
336
+ type HookResponseNonBlockingError = {
337
+ kind: "non-blocking-error";
338
+ payload?: string;
339
+ };
340
+ type HookResponseBlockingError = {
341
+ kind: "blocking-error";
342
+ payload: string;
343
+ };
344
+ type HookResponseSuccess = {
345
+ kind: "success";
346
+ payload: HookSuccessPayload;
347
+ };
348
+ type HookSuccessPayload = {
349
+ /**
350
+ * Message shown to the user in transcript mode.
351
+ */
352
+ messageForUser?: string | undefined;
353
+ /**
354
+ * Additional context for Claude.
355
+ *
356
+ * Only works for `UserPromptSubmit` and `SessionStart` hooks.
357
+ */
358
+ additionalClaudeContext?: string | undefined;
359
+ };
360
+ type HookResponseJSON<TTrigger extends HookTrigger> = {
361
+ kind: "json";
362
+ payload: HookResultJSON<TTrigger>;
363
+ };
364
+ type HookResultJSON<TTrigger extends HookTrigger> = { [EventKey in keyof TTrigger]: EventKey extends SupportedHookEvent ? TTrigger[EventKey] extends true | Record<PropertyKey, true> ? {
365
+ /**
366
+ * The name of the event being triggered.
367
+ *
368
+ * Required for proper TypeScript inference.
369
+ */
370
+ event: EventKey;
371
+ /**
372
+ * The output data for the event.
373
+ */
374
+ output: ExtractHookOutput<EventKey>;
375
+ } : never : never }[keyof TTrigger];
376
+ //#endregion
377
+ //#region src/utils/types.d.ts
378
+ type Awaitable<T> = Promise<T> | T;
379
+ //#endregion
380
+ //#region src/define.d.ts
381
+ /**
382
+ * Creates a type-safe Claude Code hook definition.
383
+ *
384
+ * This function provides a way to define hooks with full TypeScript type safety,
385
+ * including input validation using Valibot schemas and strongly typed context.
386
+ *
387
+ * @param definition - The hook definition object containing trigger events and handler function
388
+ * @returns The same hook definition, but with enhanced type safety
389
+ *
390
+ * @example
391
+ * ```ts
392
+ * // Basic session start hook
393
+ * const sessionHook = defineHook({
394
+ * trigger: { SessionStart: true },
395
+ * run: (context) => {
396
+ * console.log(`Session started: ${context.input.session_id}`);
397
+ * return context.success({
398
+ * messageForUser: "Welcome to your coding session!"
399
+ * });
400
+ * }
401
+ * });
402
+ *
403
+ * // Tool-specific PreToolUse hook
404
+ * const readHook = defineHook({
405
+ * trigger: { PreToolUse: { Read: true } },
406
+ * run: (context) => {
407
+ * // context.input.tool_input is typed as { file_path: string }
408
+ * const { file_path } = context.input.tool_input;
409
+ *
410
+ * if (file_path.includes('.env')) {
411
+ * return context.blockingError('Cannot read environment files');
412
+ * }
413
+ *
414
+ * return context.success();
415
+ * }
416
+ * });
417
+ *
418
+ * // Multiple event triggers with conditional logic
419
+ * const multiEventHook = defineHook({
420
+ * trigger: {
421
+ * PreToolUse: { Read: true, WebFetch: true },
422
+ * PostToolUse: { Read: true }
423
+ * },
424
+ * shouldRun: () => process.env.NODE_ENV === 'development',
425
+ * run: (context) => {
426
+ * // Handle different events and tools based on context.input
427
+ * return context.success();
428
+ * }
429
+ * });
430
+ * ```
431
+ */
432
+ declare function defineHook<THookTrigger extends HookTrigger = HookTrigger>(definition: HookDefinition<THookTrigger>): HookDefinition<THookTrigger>;
433
+ type HookDefinition<THookTrigger extends HookTrigger = HookTrigger> = {
434
+ /**
435
+ * The event that triggers the hook.
436
+ */
437
+ trigger: THookTrigger;
438
+ /**
439
+ * The function to run when the hook is triggered.
440
+ *
441
+ * @example
442
+ * ```ts
443
+ * // Example usage
444
+ * (context) => {
445
+ * const input = context.input;
446
+ *
447
+ * // Your hook logic here
448
+ *
449
+ * return context.success();
450
+ * }
451
+ */
452
+ run: HookHandler<THookTrigger>;
453
+ /**
454
+ * Determines whether the hook should run.
455
+ *
456
+ * @default true
457
+ *
458
+ * @returns Whether the hook should run.
459
+ */
460
+ shouldRun?: (() => Awaitable<boolean>) | boolean;
461
+ };
462
+ type HookHandler<THookTrigger extends HookTrigger> = (context: HookContext<THookTrigger>) => Awaitable<HookResponse<THookTrigger>>;
463
+ //#endregion
464
+ //#region src/run.d.ts
465
+ /**
466
+ * Executes a Claude Code hook with runtime input validation and error handling.
467
+ *
468
+ * This function handles the complete lifecycle of hook execution including:
469
+ * - Reading input from stdin
470
+ * - Validating input against Valibot schemas
471
+ * - Creating typed context
472
+ * - Executing the hook handler
473
+ * - Formatting and outputting results
474
+ *
475
+ * @param definition - The hook definition to execute
476
+ *
477
+ * @example
478
+ * ```ts
479
+ * // CLI usage: echo '{"hook_event_name":"SessionStart",...}' | node hook.js
480
+ * const hook = defineHook({
481
+ * trigger: { SessionStart: true },
482
+ * run: (context) => context.success()
483
+ * });
484
+ *
485
+ * // Execute the hook (typically called from CLI)
486
+ * await runHook(hook);
487
+ *
488
+ * // Hook with error handling
489
+ * const validationHook = defineHook({
490
+ * trigger: { PreToolUse: { Read: true } },
491
+ * run: (context) => {
492
+ * try {
493
+ * const { file_path } = context.input.tool_input;
494
+ *
495
+ * if (!file_path.endsWith('.ts')) {
496
+ * return context.nonBlockingError('Warning: Non-TypeScript file detected');
497
+ * }
498
+ *
499
+ * return context.success();
500
+ * } catch (error) {
501
+ * return context.blockingError(`Validation failed: ${error.message}`);
502
+ * }
503
+ * }
504
+ * });
505
+ *
506
+ * await runHook(validationHook);
507
+ * ```
508
+ */
509
+ declare function runHook<THookTrigger extends HookTrigger = HookTrigger>(def: HookDefinition<THookTrigger>): Promise<void>;
510
+ //#endregion
511
+ //#region src/index.d.ts
512
+ /**
513
+ * Represents the input schema for each tool in the `PreToolUse` and `PostToolUse` hooks.
514
+ *
515
+ * This interface enables type-safe hook handling with tool-specific inputs and responses.
516
+ * Users can extend this interface with declaration merging to add custom tool definitions.
517
+ *
518
+ * @example
519
+ * ```ts
520
+ * // Extend with custom tools
521
+ * declare module "cc-hooks-ts" {
522
+ * interface ToolSchema {
523
+ * MyCustomTool: {
524
+ * input: {
525
+ * customParam: string;
526
+ * optionalParam?: number;
527
+ * };
528
+ * response: {
529
+ * result: string;
530
+ * };
531
+ * };
532
+ * }
533
+ * }
534
+ *
535
+ * // Use in hook definition
536
+ * const hook = defineHook({
537
+ * trigger: { PreToolUse: { MyCustomTool: true } },
538
+ * run: (context) => {
539
+ * // context.input.tool_input is now typed as { customParam: string; optionalParam?: number; }
540
+ * const { customParam, optionalParam } = context.input.tool_input;
541
+ * return context.success();
542
+ * }
543
+ * });
544
+ * ```
545
+ */
546
+ interface ToolSchema {
547
+ Read: {
548
+ input: {
549
+ file_path: string;
550
+ };
551
+ response: unknown;
552
+ };
553
+ WebFetch: {
554
+ input: {
555
+ prompt: string;
556
+ url: string;
557
+ };
558
+ response: unknown;
559
+ };
560
+ }
561
+ //#endregion
562
+ export { type ExtractAllHookInputsForEvent, type ExtractSpecificHookInputForEvent, ToolSchema, defineHook, runHook };
package/dist/index.mjs ADDED
@@ -0,0 +1,130 @@
1
+ import { readFileSync } from "node:fs";
2
+ import process from "node:process";
3
+ import * as v from "valibot";
4
+ function defineHook(definition) {
5
+ return definition;
6
+ }
7
+ function createContext(input) {
8
+ return {
9
+ blockingError: (error) => ({
10
+ kind: "blocking-error",
11
+ payload: error
12
+ }),
13
+ input,
14
+ nonBlockingError: (message) => ({
15
+ kind: "non-blocking-error",
16
+ payload: message
17
+ }),
18
+ success: (result) => ({
19
+ kind: "success",
20
+ payload: {
21
+ additionalClaudeContext: result?.additionalClaudeContext,
22
+ messageForUser: result?.messageForUser
23
+ }
24
+ }),
25
+ json: (payload) => ({
26
+ kind: "json",
27
+ payload
28
+ })
29
+ };
30
+ }
31
+ const SUPPORTED_HOOK_EVENTS = [
32
+ "PreToolUse",
33
+ "PostToolUse",
34
+ "Notification",
35
+ "UserPromptSubmit",
36
+ "Stop",
37
+ "SubagentStop",
38
+ "PreCompact",
39
+ "SessionStart",
40
+ "SessionEnd"
41
+ ];
42
+ const baseHookInputSchema = v.object({
43
+ cwd: v.string(),
44
+ hook_event_name: v.union(SUPPORTED_HOOK_EVENTS.map((e) => v.literal(e))),
45
+ session_id: v.string(),
46
+ transcript_path: v.string()
47
+ });
48
+ function buildHookInputSchema(hook_event_name, entries) {
49
+ return v.object({
50
+ ...baseHookInputSchema.entries,
51
+ hook_event_name: v.literal(hook_event_name),
52
+ ...entries
53
+ });
54
+ }
55
+ const HookInputSchemas = {
56
+ PreToolUse: buildHookInputSchema("PreToolUse", {
57
+ tool_input: v.unknown(),
58
+ tool_name: v.intersect([v.string(), v.record(v.string(), v.never())])
59
+ }),
60
+ PostToolUse: buildHookInputSchema("PostToolUse", {
61
+ tool_input: v.unknown(),
62
+ tool_name: v.string(),
63
+ tool_response: v.unknown()
64
+ }),
65
+ Notification: buildHookInputSchema("Notification", { message: v.optional(v.string()) }),
66
+ UserPromptSubmit: buildHookInputSchema("UserPromptSubmit", { prompt: v.string() }),
67
+ Stop: buildHookInputSchema("Stop", { stop_hook_active: v.optional(v.boolean()) }),
68
+ SubagentStop: buildHookInputSchema("SubagentStop", { stop_hook_active: v.optional(v.boolean()) }),
69
+ PreCompact: buildHookInputSchema("PreCompact", {
70
+ custom_instructions: v.string(),
71
+ trigger: v.union([v.literal("manual"), v.literal("auto")])
72
+ }),
73
+ SessionStart: buildHookInputSchema("SessionStart", { source: v.string() }),
74
+ SessionEnd: buildHookInputSchema("SessionEnd", { reason: v.string() })
75
+ };
76
+ function isNonEmptyString(value) {
77
+ return typeof value === "string" && value.length > 0;
78
+ }
79
+ async function runHook(def) {
80
+ const { run, shouldRun = true, trigger } = def;
81
+ let eventName = null;
82
+ try {
83
+ const proceed = typeof shouldRun === "function" ? await shouldRun() : shouldRun;
84
+ if (!proceed) return handleHookResult(eventName, {
85
+ kind: "success",
86
+ payload: {}
87
+ });
88
+ const inputSchema = extractInputSchemaFromTrigger(trigger);
89
+ const rawInput = readFileSync(process.stdin.fd, "utf-8");
90
+ const parsed = v.parse(inputSchema, JSON.parse(rawInput));
91
+ eventName = parsed.hook_event_name;
92
+ const context = createContext(parsed);
93
+ const result = await run(context);
94
+ handleHookResult(eventName, result);
95
+ } catch (error) {
96
+ handleHookResult(eventName, {
97
+ kind: "non-blocking-error",
98
+ payload: `Error in hook: ${error instanceof Error ? error.message : String(error)}`
99
+ });
100
+ }
101
+ }
102
+ function handleHookResult(eventName, hookResult) {
103
+ switch (hookResult.kind) {
104
+ case "success":
105
+ if (eventName === "UserPromptSubmit" || eventName === "SessionStart") {
106
+ if (isNonEmptyString(hookResult.payload.additionalClaudeContext)) {
107
+ console.log(hookResult.payload.additionalClaudeContext);
108
+ return process.exit(0);
109
+ }
110
+ }
111
+ if (isNonEmptyString(hookResult.payload.messageForUser)) console.log(hookResult.payload.messageForUser);
112
+ return process.exit(0);
113
+ case "blocking-error":
114
+ if (hookResult.payload) console.error(hookResult.payload);
115
+ return process.exit(2);
116
+ case "non-blocking-error":
117
+ if (isNonEmptyString(hookResult.payload)) console.error(hookResult.payload);
118
+ return process.exit(1);
119
+ case "json":
120
+ console.log(JSON.stringify(hookResult.payload));
121
+ return process.exit(0);
122
+ }
123
+ }
124
+ function extractInputSchemaFromTrigger(trigger) {
125
+ const schemas = Object.keys(trigger).map((hookEvent) => {
126
+ return HookInputSchemas[hookEvent];
127
+ });
128
+ return v.union(schemas);
129
+ }
130
+ export { defineHook, runHook };
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "cc-hooks-ts",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "Write claude code hooks with type safety",
6
+ "sideEffects": false,
7
+ "license": "MIT",
8
+ "author": {
9
+ "name": "sushichan044",
10
+ "url": "https://github.com/sushichan044"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/sushichan044/cc-hooks-ts.git"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "registry": "https://registry.npmjs.org/",
19
+ "provenance": true
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/sushichan044/cc-hooks-ts/issues"
23
+ },
24
+ "engines": {
25
+ "node": "^20.12.0 || ^22.0.0 || >=24.0.0"
26
+ },
27
+ "keywords": [
28
+ "claude",
29
+ "claude-code",
30
+ "hooks",
31
+ "typescript",
32
+ "type-safety",
33
+ "anthropic"
34
+ ],
35
+ "files": [
36
+ "dist"
37
+ ],
38
+ "exports": {
39
+ ".": {
40
+ "types": "./dist/index.d.mts",
41
+ "import": "./dist/index.mjs"
42
+ }
43
+ },
44
+ "devDependencies": {
45
+ "@arethetypeswrong/core": "0.18.2",
46
+ "@biomejs/biome": "2.1.2",
47
+ "@types/node": "24.3.0",
48
+ "@virtual-live-lab/eslint-config": "2.2.24",
49
+ "@virtual-live-lab/tsconfig": "2.1.21",
50
+ "eslint": "9.31.0",
51
+ "eslint-plugin-import-access": "3.0.0",
52
+ "pkg-pr-new": "0.0.54",
53
+ "publint": "0.3.12",
54
+ "release-it": "19.0.4",
55
+ "release-it-pnpm": "4.6.6",
56
+ "tsdown": "0.14.1",
57
+ "typescript": "5.9.2",
58
+ "typescript-eslint": "8.39.0",
59
+ "unplugin-unused": "0.5.2",
60
+ "vitest": "3.2.4"
61
+ },
62
+ "dependencies": {
63
+ "valibot": "^1.1.0"
64
+ },
65
+ "scripts": {
66
+ "lint": "eslint --max-warnings 0 --fix .",
67
+ "format": "biome format --write .",
68
+ "format:check": "biome format . --reporter=github",
69
+ "typecheck": "tsc --noEmit",
70
+ "test": "vitest --run",
71
+ "build": "tsdown",
72
+ "pkg-pr-new": "pkg-pr-new publish --compact --comment=update --pnpm"
73
+ }
74
+ }