cc-hooks-ts 2.0.70 → 2.1.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/README.md CHANGED
@@ -16,6 +16,7 @@ See [examples](./examples) for more usage examples.
16
16
  - [Advanced Usage](#advanced-usage)
17
17
  - [Conditional Hook Execution](#conditional-hook-execution)
18
18
  - [Advanced JSON Output](#advanced-json-output)
19
+ - [Async JSON Output (Experimental)](#async-json-output-experimental)
19
20
  - [Documentation](#documentation)
20
21
  - [Development](#development)
21
22
  - [How to follow the upstream changes](#how-to-follow-the-upstream-changes)
@@ -210,6 +211,47 @@ Use `context.json()` to return structured JSON output with advanced control over
210
211
 
211
212
  For detailed information about available JSON fields and their behavior, see the [official documentation](https://docs.anthropic.com/en/docs/claude-code/hooks#advanced:-json-output).
212
213
 
214
+ ### Async JSON Output (Experimental)
215
+
216
+ > [!WARNING]
217
+ > This behavior is undocumented by Anthropic and may change.
218
+
219
+ > [!CAUTION]
220
+ > You must enable verbose output if you want to see async hook outputs like `systemMessage` or `hookSpecificOutput.additionalContext`.
221
+ >
222
+ > You can enable it in Claude Code by going to `/config` and setting "verbose" to true.
223
+
224
+ Async JSON output allows hooks to perform longer computations without blocking the Claude Code TUI.
225
+
226
+ You can use `context.defer()` to respond Claude Code immediately while performing longer computations in the background.
227
+
228
+ You should complete the async operation within a reasonable time (e.g. 15 seconds).
229
+
230
+ ```ts
231
+ import { defineHook } from "cc-hooks-ts";
232
+
233
+ const hook = defineHook({
234
+ trigger: { PostToolUse: { Read: true } },
235
+ run: (context) =>
236
+ context.defer(
237
+ async () => {
238
+ // Simulate long-running computation
239
+ await new Promise((resolve) => setTimeout(resolve, 2000));
240
+
241
+ return {
242
+ event: "PostToolUse",
243
+ output: {
244
+ systemMessage: "Read tool used successfully after async processing!"
245
+ }
246
+ };
247
+ },
248
+ {
249
+ timeoutMs: 5000 // Optional timeout for the async operation.
250
+ }
251
+ )
252
+ });
253
+ ```
254
+
213
255
  ## Documentation
214
256
 
215
257
  For more detailed information about Claude Code hooks, visit the [official documentation](https://docs.anthropic.com/en/docs/claude-code/hooks).
@@ -243,7 +285,7 @@ pnpm typecheck
243
285
  ```bash
244
286
  npm diff --diff=@anthropic-ai/claude-agent-sdk@0.1.69 --diff=@anthropic-ai/claude-agent-sdk@0.1.70 '**/*.d.ts'
245
287
 
246
- # You can use dandavison/delta for better diff visualization
288
+ # Only for humans, You can use dandavison/delta for better diff visualization
247
289
  npm diff --diff=@anthropic-ai/claude-agent-sdk@0.1.69 --diff=@anthropic-ai/claude-agent-sdk@0.1.70 '**/*.d.ts' | delta --side-by-side
248
290
  ```
249
291
 
package/dist/index.d.mts CHANGED
@@ -104,7 +104,7 @@ declare const HookInputSchemas: {
104
104
  readonly hook_event_name: v.LiteralSchema<"PreCompact", undefined>;
105
105
  } & {
106
106
  custom_instructions: v.NullableSchema<v.StringSchema<undefined>, undefined>;
107
- trigger: v.UnionSchema<[v.LiteralSchema<"manual", undefined>, v.LiteralSchema<"auto", undefined>], undefined>;
107
+ trigger: v.PicklistSchema<["manual", "auto"], undefined>;
108
108
  }, undefined>;
109
109
  readonly SessionStart: v.ObjectSchema<{
110
110
  readonly cwd: v.StringSchema<undefined>;
@@ -113,7 +113,7 @@ declare const HookInputSchemas: {
113
113
  readonly transcript_path: v.StringSchema<undefined>;
114
114
  readonly hook_event_name: v.LiteralSchema<"SessionStart", undefined>;
115
115
  } & {
116
- source: v.UnionSchema<[v.LiteralSchema<"startup", undefined>, v.LiteralSchema<"resume", undefined>, v.LiteralSchema<"clear", undefined>, v.LiteralSchema<"compact", undefined>], undefined>;
116
+ source: v.PicklistSchema<["startup", "resume", "clear", "compact"], undefined>;
117
117
  }, undefined>;
118
118
  readonly SessionEnd: v.ObjectSchema<{
119
119
  readonly cwd: v.StringSchema<undefined>;
@@ -122,7 +122,7 @@ declare const HookInputSchemas: {
122
122
  readonly transcript_path: v.StringSchema<undefined>;
123
123
  readonly hook_event_name: v.LiteralSchema<"SessionEnd", undefined>;
124
124
  } & {
125
- reason: v.StringSchema<undefined>;
125
+ reason: v.PicklistSchema<["clear", "logout", "prompt_input_exit", "other", "bypass_permissions_disabled"], undefined>;
126
126
  }, undefined>;
127
127
  readonly PermissionRequest: v.ObjectSchema<{
128
128
  readonly cwd: v.StringSchema<undefined>;
@@ -132,39 +132,39 @@ declare const HookInputSchemas: {
132
132
  readonly hook_event_name: v.LiteralSchema<"PermissionRequest", undefined>;
133
133
  } & {
134
134
  permission_suggestions: v.ExactOptionalSchema<v.ArraySchema<v.VariantSchema<"type", [v.ObjectSchema<{
135
- readonly behavior: v.UnionSchema<[v.LiteralSchema<"allow", undefined>, v.LiteralSchema<"deny", undefined>, v.LiteralSchema<"ask", undefined>], undefined>;
136
- readonly destination: v.UnionSchema<[v.LiteralSchema<"userSettings", undefined>, v.LiteralSchema<"projectSettings", undefined>, v.LiteralSchema<"localSettings", undefined>, v.LiteralSchema<"session", undefined>, v.LiteralSchema<"cliArg", undefined>], undefined>;
135
+ readonly behavior: v.PicklistSchema<["allow", "deny", "ask"], undefined>;
136
+ readonly destination: v.PicklistSchema<["userSettings", "projectSettings", "localSettings", "session", "cliArg"], undefined>;
137
137
  readonly rules: v.ArraySchema<v.ObjectSchema<{
138
138
  readonly ruleContent: v.ExactOptionalSchema<v.StringSchema<undefined>, undefined>;
139
139
  readonly toolName: v.StringSchema<undefined>;
140
140
  }, undefined>, undefined>;
141
141
  readonly type: v.LiteralSchema<"addRules", undefined>;
142
142
  }, undefined>, v.ObjectSchema<{
143
- readonly behavior: v.UnionSchema<[v.LiteralSchema<"allow", undefined>, v.LiteralSchema<"deny", undefined>, v.LiteralSchema<"ask", undefined>], undefined>;
144
- readonly destination: v.UnionSchema<[v.LiteralSchema<"userSettings", undefined>, v.LiteralSchema<"projectSettings", undefined>, v.LiteralSchema<"localSettings", undefined>, v.LiteralSchema<"session", undefined>, v.LiteralSchema<"cliArg", undefined>], undefined>;
143
+ readonly behavior: v.PicklistSchema<["allow", "deny", "ask"], undefined>;
144
+ readonly destination: v.PicklistSchema<["userSettings", "projectSettings", "localSettings", "session", "cliArg"], undefined>;
145
145
  readonly rules: v.ArraySchema<v.ObjectSchema<{
146
146
  readonly ruleContent: v.ExactOptionalSchema<v.StringSchema<undefined>, undefined>;
147
147
  readonly toolName: v.StringSchema<undefined>;
148
148
  }, undefined>, undefined>;
149
149
  readonly type: v.LiteralSchema<"replaceRules", undefined>;
150
150
  }, undefined>, v.ObjectSchema<{
151
- readonly behavior: v.UnionSchema<[v.LiteralSchema<"allow", undefined>, v.LiteralSchema<"deny", undefined>, v.LiteralSchema<"ask", undefined>], undefined>;
152
- readonly destination: v.UnionSchema<[v.LiteralSchema<"userSettings", undefined>, v.LiteralSchema<"projectSettings", undefined>, v.LiteralSchema<"localSettings", undefined>, v.LiteralSchema<"session", undefined>, v.LiteralSchema<"cliArg", undefined>], undefined>;
151
+ readonly behavior: v.PicklistSchema<["allow", "deny", "ask"], undefined>;
152
+ readonly destination: v.PicklistSchema<["userSettings", "projectSettings", "localSettings", "session", "cliArg"], undefined>;
153
153
  readonly rules: v.ArraySchema<v.ObjectSchema<{
154
154
  readonly ruleContent: v.ExactOptionalSchema<v.StringSchema<undefined>, undefined>;
155
155
  readonly toolName: v.StringSchema<undefined>;
156
156
  }, undefined>, undefined>;
157
157
  readonly type: v.LiteralSchema<"removeRules", undefined>;
158
158
  }, undefined>, v.ObjectSchema<{
159
- readonly destination: v.UnionSchema<[v.LiteralSchema<"userSettings", undefined>, v.LiteralSchema<"projectSettings", undefined>, v.LiteralSchema<"localSettings", undefined>, v.LiteralSchema<"session", undefined>, v.LiteralSchema<"cliArg", undefined>], undefined>;
160
- readonly mode: v.UnionSchema<[v.LiteralSchema<"acceptEdits", undefined>, v.LiteralSchema<"bypassPermissions", undefined>, v.LiteralSchema<"default", undefined>, v.LiteralSchema<"dontAsk", undefined>, v.LiteralSchema<"delegate", undefined>, v.LiteralSchema<"plan", undefined>], undefined>;
159
+ readonly destination: v.PicklistSchema<["userSettings", "projectSettings", "localSettings", "session", "cliArg"], undefined>;
160
+ readonly mode: v.PicklistSchema<["acceptEdits", "bypassPermissions", "default", "dontAsk", "delegate", "plan"], undefined>;
161
161
  readonly type: v.LiteralSchema<"setMode", undefined>;
162
162
  }, undefined>, v.ObjectSchema<{
163
- readonly destination: v.UnionSchema<[v.LiteralSchema<"userSettings", undefined>, v.LiteralSchema<"projectSettings", undefined>, v.LiteralSchema<"localSettings", undefined>, v.LiteralSchema<"session", undefined>, v.LiteralSchema<"cliArg", undefined>], undefined>;
163
+ readonly destination: v.PicklistSchema<["userSettings", "projectSettings", "localSettings", "session", "cliArg"], undefined>;
164
164
  readonly directories: v.ArraySchema<v.StringSchema<undefined>, undefined>;
165
165
  readonly type: v.LiteralSchema<"addDirectories", undefined>;
166
166
  }, undefined>, v.ObjectSchema<{
167
- readonly destination: v.UnionSchema<[v.LiteralSchema<"userSettings", undefined>, v.LiteralSchema<"projectSettings", undefined>, v.LiteralSchema<"localSettings", undefined>, v.LiteralSchema<"session", undefined>, v.LiteralSchema<"cliArg", undefined>], undefined>;
167
+ readonly destination: v.PicklistSchema<["userSettings", "projectSettings", "localSettings", "session", "cliArg"], undefined>;
168
168
  readonly directories: v.ArraySchema<v.StringSchema<undefined>, undefined>;
169
169
  readonly type: v.LiteralSchema<"removeDirectories", undefined>;
170
170
  }, undefined>], undefined>, undefined>, undefined>;
@@ -265,39 +265,39 @@ type ToolSpecificPostToolUseFailureInput = { [K in keyof ToolSchema]: Omit<BaseH
265
265
  * @package
266
266
  */
267
267
  declare const permissionUpdateSchema: v.VariantSchema<"type", [v.ObjectSchema<{
268
- readonly behavior: v.UnionSchema<[v.LiteralSchema<"allow", undefined>, v.LiteralSchema<"deny", undefined>, v.LiteralSchema<"ask", undefined>], undefined>;
269
- readonly destination: v.UnionSchema<[v.LiteralSchema<"userSettings", undefined>, v.LiteralSchema<"projectSettings", undefined>, v.LiteralSchema<"localSettings", undefined>, v.LiteralSchema<"session", undefined>, v.LiteralSchema<"cliArg", undefined>], undefined>;
268
+ readonly behavior: v.PicklistSchema<["allow", "deny", "ask"], undefined>;
269
+ readonly destination: v.PicklistSchema<["userSettings", "projectSettings", "localSettings", "session", "cliArg"], undefined>;
270
270
  readonly rules: v.ArraySchema<v.ObjectSchema<{
271
271
  readonly ruleContent: v.ExactOptionalSchema<v.StringSchema<undefined>, undefined>;
272
272
  readonly toolName: v.StringSchema<undefined>;
273
273
  }, undefined>, undefined>;
274
274
  readonly type: v.LiteralSchema<"addRules", undefined>;
275
275
  }, undefined>, v.ObjectSchema<{
276
- readonly behavior: v.UnionSchema<[v.LiteralSchema<"allow", undefined>, v.LiteralSchema<"deny", undefined>, v.LiteralSchema<"ask", undefined>], undefined>;
277
- readonly destination: v.UnionSchema<[v.LiteralSchema<"userSettings", undefined>, v.LiteralSchema<"projectSettings", undefined>, v.LiteralSchema<"localSettings", undefined>, v.LiteralSchema<"session", undefined>, v.LiteralSchema<"cliArg", undefined>], undefined>;
276
+ readonly behavior: v.PicklistSchema<["allow", "deny", "ask"], undefined>;
277
+ readonly destination: v.PicklistSchema<["userSettings", "projectSettings", "localSettings", "session", "cliArg"], undefined>;
278
278
  readonly rules: v.ArraySchema<v.ObjectSchema<{
279
279
  readonly ruleContent: v.ExactOptionalSchema<v.StringSchema<undefined>, undefined>;
280
280
  readonly toolName: v.StringSchema<undefined>;
281
281
  }, undefined>, undefined>;
282
282
  readonly type: v.LiteralSchema<"replaceRules", undefined>;
283
283
  }, undefined>, v.ObjectSchema<{
284
- readonly behavior: v.UnionSchema<[v.LiteralSchema<"allow", undefined>, v.LiteralSchema<"deny", undefined>, v.LiteralSchema<"ask", undefined>], undefined>;
285
- readonly destination: v.UnionSchema<[v.LiteralSchema<"userSettings", undefined>, v.LiteralSchema<"projectSettings", undefined>, v.LiteralSchema<"localSettings", undefined>, v.LiteralSchema<"session", undefined>, v.LiteralSchema<"cliArg", undefined>], undefined>;
284
+ readonly behavior: v.PicklistSchema<["allow", "deny", "ask"], undefined>;
285
+ readonly destination: v.PicklistSchema<["userSettings", "projectSettings", "localSettings", "session", "cliArg"], undefined>;
286
286
  readonly rules: v.ArraySchema<v.ObjectSchema<{
287
287
  readonly ruleContent: v.ExactOptionalSchema<v.StringSchema<undefined>, undefined>;
288
288
  readonly toolName: v.StringSchema<undefined>;
289
289
  }, undefined>, undefined>;
290
290
  readonly type: v.LiteralSchema<"removeRules", undefined>;
291
291
  }, undefined>, v.ObjectSchema<{
292
- readonly destination: v.UnionSchema<[v.LiteralSchema<"userSettings", undefined>, v.LiteralSchema<"projectSettings", undefined>, v.LiteralSchema<"localSettings", undefined>, v.LiteralSchema<"session", undefined>, v.LiteralSchema<"cliArg", undefined>], undefined>;
293
- readonly mode: v.UnionSchema<[v.LiteralSchema<"acceptEdits", undefined>, v.LiteralSchema<"bypassPermissions", undefined>, v.LiteralSchema<"default", undefined>, v.LiteralSchema<"dontAsk", undefined>, v.LiteralSchema<"delegate", undefined>, v.LiteralSchema<"plan", undefined>], undefined>;
292
+ readonly destination: v.PicklistSchema<["userSettings", "projectSettings", "localSettings", "session", "cliArg"], undefined>;
293
+ readonly mode: v.PicklistSchema<["acceptEdits", "bypassPermissions", "default", "dontAsk", "delegate", "plan"], undefined>;
294
294
  readonly type: v.LiteralSchema<"setMode", undefined>;
295
295
  }, undefined>, v.ObjectSchema<{
296
- readonly destination: v.UnionSchema<[v.LiteralSchema<"userSettings", undefined>, v.LiteralSchema<"projectSettings", undefined>, v.LiteralSchema<"localSettings", undefined>, v.LiteralSchema<"session", undefined>, v.LiteralSchema<"cliArg", undefined>], undefined>;
296
+ readonly destination: v.PicklistSchema<["userSettings", "projectSettings", "localSettings", "session", "cliArg"], undefined>;
297
297
  readonly directories: v.ArraySchema<v.StringSchema<undefined>, undefined>;
298
298
  readonly type: v.LiteralSchema<"addDirectories", undefined>;
299
299
  }, undefined>, v.ObjectSchema<{
300
- readonly destination: v.UnionSchema<[v.LiteralSchema<"userSettings", undefined>, v.LiteralSchema<"projectSettings", undefined>, v.LiteralSchema<"localSettings", undefined>, v.LiteralSchema<"session", undefined>, v.LiteralSchema<"cliArg", undefined>], undefined>;
300
+ readonly destination: v.PicklistSchema<["userSettings", "projectSettings", "localSettings", "session", "cliArg"], undefined>;
301
301
  readonly directories: v.ArraySchema<v.StringSchema<undefined>, undefined>;
302
302
  readonly type: v.LiteralSchema<"removeDirectories", undefined>;
303
303
  }, undefined>], undefined>;
@@ -324,7 +324,28 @@ type HookOutput = {
324
324
  /**
325
325
  * @package
326
326
  */
327
- type ExtractHookOutput<TEvent$1 extends SupportedHookEvent> = HookOutput extends Record<SupportedHookEvent, unknown> ? HookOutput[TEvent$1] : never;
327
+ type ExtractSyncHookOutput<TEvent$1 extends SupportedHookEvent> = HookOutput extends Record<SupportedHookEvent, unknown> ? HookOutput[TEvent$1] : never;
328
+ /**
329
+ * @package
330
+ */
331
+ type ExtractAsyncHookOutput<TEvent$1 extends SupportedHookEvent> = _InternalExtractAsyncHookOutput<ExtractSyncHookOutput<TEvent$1>>;
332
+ /**
333
+ * Compute ExtractSyncHookOutput<TEvent> only once for better performance.
334
+ *
335
+ * Only `systemMessage` and `hookSpecificOutput.additionalContext` are read by Claude Code if we are using async hook.
336
+ * (This feature is not documented)
337
+ *
338
+ * @internal
339
+ */
340
+ type _InternalExtractAsyncHookOutput<Output extends CommonHookOutputs> = Output extends {
341
+ hookSpecificOutput?: {
342
+ additionalContext?: infer TAdditionalContext;
343
+ };
344
+ } ? Pick<Output, "systemMessage"> & {
345
+ hookSpecificOutput?: {
346
+ additionalContext?: TAdditionalContext;
347
+ };
348
+ } : Pick<Output, "systemMessage">;
328
349
  /**
329
350
  * Common fields of hook outputs
330
351
  *
@@ -507,28 +528,142 @@ interface HookContext<THookTrigger extends HookTrigger> {
507
528
  /**
508
529
  * Cause a blocking error.
509
530
  *
510
- * @param error `error` is fed back to Claude to process automatically
531
+ * When called, Claude Code stops processing and reports the error to Claude.
532
+ * The hook exits with code 2, and the error message is fed back to Claude for automatic processing.
533
+ *
534
+ * @example
535
+ * // Block access to sensitive files
536
+ * const hook = defineHook({
537
+ * trigger: { PreToolUse: { Read: true } },
538
+ * run: (context) => {
539
+ * const { file_path } = context.input.tool_input;
540
+ *
541
+ * if (file_path.includes('.env') || file_path.includes('secrets')) {
542
+ * return context.blockingError('Access to sensitive files is not allowed');
543
+ * }
544
+ *
545
+ * return context.success();
546
+ * }
547
+ * });
511
548
  */
512
549
  blockingError: (error: string) => HookResponseBlockingError;
550
+ /**
551
+ * Defer processing and produce JSON output after long-running computation.
552
+ *
553
+ * @experimental This behavior is undocumented by Anthropic and may change in future versions
554
+ *
555
+ * @example
556
+ * // Compute analysis with timeout protection
557
+ * const hook = defineHook({
558
+ * trigger: { PostToolUse: { Grep: true } },
559
+ * run: (context) => {
560
+ * return context.defer(
561
+ * async () => {
562
+ * const analysis = await analyzeGrepResults(context.input.tool_response);
563
+ *
564
+ * return {
565
+ * event: "PostToolUse",
566
+ * output: {
567
+ * systemMessage: `Found ${analysis.matchCount} matches`,
568
+ * hookSpecificOutput: {
569
+ * additionalContext: analysis.summary
570
+ * }
571
+ * }
572
+ * };
573
+ * },
574
+ * { timeoutMs: 5000 } // Fail fast if analysis takes too long
575
+ * );
576
+ * }
577
+ * });
578
+ */
579
+ defer: (handler: () => Awaitable<AsyncHookResultJSON<THookTrigger>>, options?: {
580
+ /**
581
+ * Optional timeout in milliseconds.
582
+ *
583
+ * Claude Code has its own internal timeouts; this setting allows you to specify a shorter timeout
584
+ * and cc-hooks-ts will abort the operation as circuit breaker.
585
+ *
586
+ * @default
587
+ * Claude Code has its own internal timeouts.
588
+ */
589
+ timeoutMs?: number | undefined;
590
+ }) => HookResponseAsyncJSON<THookTrigger>;
513
591
  input: ExtractTriggeredHookInput<THookTrigger>;
514
592
  /**
515
593
  * Direct access to advanced JSON output.
516
594
  *
595
+ * Provides fine-grained control over hook behavior through structured JSON output.
596
+ * This is the most powerful way to control Claude Code's behavior, including
597
+ * permission decisions, input modifications, and additional context.
598
+ *
517
599
  * @see {@link https://docs.anthropic.com/en/docs/claude-code/hooks#advanced%3A-json-output}
600
+ *
601
+ * @example
602
+ * // PreToolUse: Deny access with reason
603
+ * const hook = defineHook({
604
+ * trigger: { PreToolUse: { Read: true } },
605
+ * run: (context) => {
606
+ * const { file_path } = context.input.tool_input;
607
+ *
608
+ * if (file_path.includes('.env')) {
609
+ * return context.json({
610
+ * event: "PreToolUse",
611
+ * output: {
612
+ * hookSpecificOutput: {
613
+ * permissionDecision: "deny",
614
+ * permissionDecisionReason: "Access to .env files is restricted for security."
615
+ * }
616
+ * }
617
+ * });
618
+ * }
619
+ *
620
+ * return context.success();
621
+ * }
622
+ * });
518
623
  */
519
- json: (payload: HookResultJSON<THookTrigger>) => HookResponseJSON<THookTrigger>;
624
+ json: (payload: SyncHookResultJSON<THookTrigger>) => HookResponseSyncJSON<THookTrigger>;
520
625
  /**
521
626
  * Cause a non-blocking error.
522
627
  *
523
- * @param message `message` is shown to the user and execution continues.
628
+ * The message is shown to the user and execution continues.
629
+ *
630
+ * The hook exits with code 1, but Claude Code proceeds with normal operation.
631
+ *
632
+ * @example
633
+ * // Optional notification without message
634
+ * const hook = defineHook({
635
+ * trigger: { Notification: true },
636
+ * run: (context) => {
637
+ * try {
638
+ * // Attempt to send notification
639
+ * sendNotification(context.input.message);
640
+ * return context.success();
641
+ * } catch (error) {
642
+ * // Fail silently - notification is optional
643
+ * return context.nonBlockingError();
644
+ * }
645
+ * }
646
+ * });
524
647
  */
525
648
  nonBlockingError: (message?: string) => HookResponseNonBlockingError;
526
649
  /**
527
650
  * Indicate successful handling of the hook.
651
+ *
652
+ * The hook exits with code 0. Optionally provides a message for the user or additional context for Claude.
653
+ *
654
+ * @example
655
+ * // Basic success without payload
656
+ * const hook = defineHook({
657
+ * trigger: { PreToolUse: { Read: true } },
658
+ * run: (context) => {
659
+ * // Validation passed
660
+ * return context.success();
661
+ * }
662
+ * });
528
663
  */
529
664
  success: (payload?: HookSuccessPayload) => HookResponseSuccess;
530
665
  }
531
- type HookResponse<THookTrigger extends HookTrigger> = HookResponseBlockingError | HookResponseJSON<THookTrigger> | HookResponseNonBlockingError | HookResponseSuccess;
666
+ type HookResponse<THookTrigger extends HookTrigger> = HookResponseBlockingError | HookResponseSyncJSON<THookTrigger> | HookResponseAsyncJSON<THookTrigger> | HookResponseNonBlockingError | HookResponseSuccess;
532
667
  type HookResponseNonBlockingError = {
533
668
  kind: "non-blocking-error";
534
669
  payload?: string;
@@ -553,11 +688,28 @@ type HookSuccessPayload = {
553
688
  */
554
689
  additionalClaudeContext?: string | undefined;
555
690
  };
556
- type HookResponseJSON<TTrigger extends HookTrigger> = {
557
- kind: "json";
558
- payload: HookResultJSON<TTrigger>;
691
+ type HookResponseSyncJSON<TTrigger extends HookTrigger> = {
692
+ kind: "json-sync";
693
+ payload: SyncHookResultJSON<TTrigger>;
559
694
  };
560
- type HookResultJSON<TTrigger extends HookTrigger> = { [EventKey in keyof TTrigger]: EventKey extends SupportedHookEvent ? TTrigger[EventKey] extends true | Record<PropertyKey, true> ? {
695
+ type HookResponseAsyncJSON<TTrigger extends HookTrigger> = {
696
+ kind: "json-async";
697
+ timeoutMs?: number | undefined;
698
+ run: () => Awaitable<AsyncHookResultJSON<TTrigger>>;
699
+ };
700
+ type SyncHookResultJSON<TTrigger extends HookTrigger> = { [EventKey in keyof TTrigger]: EventKey extends SupportedHookEvent ? TTrigger[EventKey] extends true | Record<PropertyKey, true> ? {
701
+ /**
702
+ * The name of the event being triggered.
703
+ *
704
+ * Required for proper TypeScript inference.
705
+ */
706
+ event: EventKey;
707
+ /**
708
+ * The output data for the event.
709
+ */
710
+ output: ExtractSyncHookOutput<EventKey>;
711
+ } : never : never }[keyof TTrigger];
712
+ type AsyncHookResultJSON<TTrigger extends HookTrigger> = { [EventKey in keyof TTrigger]: EventKey extends SupportedHookEvent ? TTrigger[EventKey] extends true | Record<PropertyKey, true> ? {
561
713
  /**
562
714
  * The name of the event being triggered.
563
715
  *
@@ -567,7 +719,7 @@ type HookResultJSON<TTrigger extends HookTrigger> = { [EventKey in keyof TTrigge
567
719
  /**
568
720
  * The output data for the event.
569
721
  */
570
- output: ExtractHookOutput<EventKey>;
722
+ output: ExtractAsyncHookOutput<EventKey>;
571
723
  } : never : never }[keyof TTrigger];
572
724
  //#endregion
573
725
  //#region src/define.d.ts
package/dist/index.mjs CHANGED
@@ -6,6 +6,11 @@ function defineHook(definition) {
6
6
  }
7
7
  function createContext(input) {
8
8
  return {
9
+ defer: (handler, options) => ({
10
+ kind: "json-async",
11
+ run: handler,
12
+ timeoutMs: options?.timeoutMs
13
+ }),
9
14
  blockingError: (error) => ({
10
15
  kind: "blocking-error",
11
16
  payload: error
@@ -23,22 +28,22 @@ function createContext(input) {
23
28
  }
24
29
  }),
25
30
  json: (payload) => ({
26
- kind: "json",
31
+ kind: "json-sync",
27
32
  payload
28
33
  })
29
34
  };
30
35
  }
31
- const permissionBehaviorSchema = v.union([
32
- v.literal("allow"),
33
- v.literal("deny"),
34
- v.literal("ask")
36
+ const permissionBehaviorSchema = v.picklist([
37
+ "allow",
38
+ "deny",
39
+ "ask"
35
40
  ]);
36
- const permissionUpdateDestinationSchema = v.union([
37
- v.literal("userSettings"),
38
- v.literal("projectSettings"),
39
- v.literal("localSettings"),
40
- v.literal("session"),
41
- v.literal("cliArg")
41
+ const permissionUpdateDestinationSchema = v.picklist([
42
+ "userSettings",
43
+ "projectSettings",
44
+ "localSettings",
45
+ "session",
46
+ "cliArg"
42
47
  ]);
43
48
  const permissionRuleValueSchema = v.object({
44
49
  ruleContent: v.exactOptional(v.string()),
@@ -65,13 +70,13 @@ const permissionUpdateSchema = v.variant("type", [
65
70
  }),
66
71
  v.object({
67
72
  destination: permissionUpdateDestinationSchema,
68
- mode: v.union([
69
- v.literal("acceptEdits"),
70
- v.literal("bypassPermissions"),
71
- v.literal("default"),
72
- v.literal("dontAsk"),
73
- v.literal("delegate"),
74
- v.literal("plan")
73
+ mode: v.picklist([
74
+ "acceptEdits",
75
+ "bypassPermissions",
76
+ "default",
77
+ "dontAsk",
78
+ "delegate",
79
+ "plan"
75
80
  ]),
76
81
  type: v.literal("setMode")
77
82
  }),
@@ -136,15 +141,21 @@ const HookInputSchemas = {
136
141
  }),
137
142
  PreCompact: buildHookInputSchema("PreCompact", {
138
143
  custom_instructions: v.nullable(v.string()),
139
- trigger: v.union([v.literal("manual"), v.literal("auto")])
144
+ trigger: v.picklist(["manual", "auto"])
140
145
  }),
141
- SessionStart: buildHookInputSchema("SessionStart", { source: v.union([
142
- v.literal("startup"),
143
- v.literal("resume"),
144
- v.literal("clear"),
145
- v.literal("compact")
146
+ SessionStart: buildHookInputSchema("SessionStart", { source: v.picklist([
147
+ "startup",
148
+ "resume",
149
+ "clear",
150
+ "compact"
151
+ ]) }),
152
+ SessionEnd: buildHookInputSchema("SessionEnd", { reason: v.picklist([
153
+ "clear",
154
+ "logout",
155
+ "prompt_input_exit",
156
+ "other",
157
+ "bypass_permissions_disabled"
146
158
  ]) }),
147
- SessionEnd: buildHookInputSchema("SessionEnd", { reason: v.string() }),
148
159
  PermissionRequest: buildHookInputSchema("PermissionRequest", {
149
160
  permission_suggestions: v.exactOptional(v.array(permissionUpdateSchema)),
150
161
  tool_input: v.unknown(),
@@ -167,32 +178,65 @@ async function runHook(def) {
167
178
  const parsed = v.parse(inputSchema, JSON.parse(rawInput));
168
179
  eventName = parsed.hook_event_name;
169
180
  const result = await run(createContext(parsed));
170
- handleHookResult(eventName, result);
181
+ await handleHookResult(eventName, result);
171
182
  } catch (error) {
172
- handleHookResult(eventName, {
183
+ await handleHookResult(eventName, {
173
184
  kind: "non-blocking-error",
174
185
  payload: `Error in hook: ${error instanceof Error ? error.message : String(error)}`
175
186
  });
176
187
  }
177
188
  }
178
- function handleHookResult(eventName, hookResult) {
189
+ async function handleHookResult(eventName, hookResult) {
179
190
  switch (hookResult.kind) {
180
- case "success":
181
- if (eventName === "UserPromptSubmit" || eventName === "SessionStart") {
182
- if (isNonEmptyString(hookResult.payload.additionalClaudeContext)) console.log(hookResult.payload.additionalClaudeContext);
183
- return process.exit(0);
184
- }
185
- if (isNonEmptyString(hookResult.payload.messageForUser)) console.log(hookResult.payload.messageForUser);
186
- return process.exit(0);
187
191
  case "blocking-error":
188
192
  if (hookResult.payload) console.error(hookResult.payload);
189
193
  return process.exit(2);
194
+ case "json-async": {
195
+ const userTimeout = hookResult.timeoutMs;
196
+ console.log(JSON.stringify({
197
+ async: true,
198
+ asyncTimeout: userTimeout ?? void 0
199
+ }));
200
+ const safeInvokeDeferredHook = async () => {
201
+ try {
202
+ return {
203
+ isError: false,
204
+ payload: await hookResult.run()
205
+ };
206
+ } catch (error) {
207
+ return {
208
+ isError: true,
209
+ reason: error instanceof Error ? error.message : String(error)
210
+ };
211
+ }
212
+ };
213
+ let deferredResult;
214
+ if (userTimeout == null) deferredResult = await safeInvokeDeferredHook();
215
+ else deferredResult = await Promise.race([safeInvokeDeferredHook(), new Promise((resolve) => setTimeout(() => resolve({
216
+ isError: true,
217
+ reason: `Exceeded user specified timeout: ${userTimeout}ms`
218
+ }), userTimeout + 5e3))]);
219
+ if (deferredResult.isError) {
220
+ if (isNonEmptyString(deferredResult.reason)) console.error(`Async hook execution failed: ${deferredResult.reason}`);
221
+ return process.exit(1);
222
+ }
223
+ console.log(JSON.stringify(deferredResult.payload.output));
224
+ return process.exit(0);
225
+ }
226
+ case "json-sync":
227
+ console.log(JSON.stringify(hookResult.payload.output));
228
+ return process.exit(0);
190
229
  case "non-blocking-error":
191
230
  if (isNonEmptyString(hookResult.payload)) console.error(hookResult.payload);
192
231
  return process.exit(1);
193
- case "json":
194
- console.log(JSON.stringify(hookResult.payload.output));
232
+ case "success":
233
+ if (eventName === "UserPromptSubmit" || eventName === "SessionStart") {
234
+ if (isNonEmptyString(hookResult.payload.additionalClaudeContext)) console.log(hookResult.payload.additionalClaudeContext);
235
+ return process.exit(0);
236
+ }
237
+ if (isNonEmptyString(hookResult.payload.messageForUser)) console.log(hookResult.payload.messageForUser);
195
238
  return process.exit(0);
239
+ default: throw new Error(`Unknown hook result kind: ${JSON.stringify(hookResult)}`);
196
240
  }
197
241
  }
198
242
  function extractInputSchemaFromTrigger(trigger) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-hooks-ts",
3
- "version": "2.0.70",
3
+ "version": "2.1.1",
4
4
  "type": "module",
5
5
  "description": "Write claude code hooks with type safety",
6
6
  "sideEffects": false,
@@ -43,26 +43,26 @@
43
43
  },
44
44
  "devDependencies": {
45
45
  "@arethetypeswrong/core": "0.18.2",
46
- "@types/node": "25.0.1",
46
+ "@types/node": "25.0.3",
47
47
  "@typescript/native-preview": "^7.0.0-dev.20251108.1",
48
48
  "@virtual-live-lab/eslint-config": "2.3.1",
49
49
  "@virtual-live-lab/tsconfig": "2.1.21",
50
50
  "eslint": "9.39.2",
51
51
  "eslint-plugin-import-access": "3.1.0",
52
- "oxfmt": "0.17.0",
52
+ "oxfmt": "0.21.0",
53
53
  "pkg-pr-new": "0.0.62",
54
54
  "publint": "0.3.16",
55
- "release-it": "19.1.0",
55
+ "release-it": "19.2.2",
56
56
  "release-it-pnpm": "4.6.6",
57
- "tsdown": "0.17.2",
57
+ "tsdown": "0.18.4",
58
58
  "type-fest": "5.3.1",
59
59
  "typescript": "5.9.3",
60
- "typescript-eslint": "8.49.0",
60
+ "typescript-eslint": "8.51.0",
61
61
  "unplugin-unused": "0.5.6",
62
- "vitest": "4.0.15"
62
+ "vitest": "4.0.16"
63
63
  },
64
64
  "dependencies": {
65
- "@anthropic-ai/claude-agent-sdk": "0.1.70",
65
+ "@anthropic-ai/claude-agent-sdk": "0.2.1",
66
66
  "valibot": "^1.1.0"
67
67
  },
68
68
  "scripts": {