cc-hooks-ts 2.0.70 → 2.0.76

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,45 @@ 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
+ Claude Code also accepts async hook responses.
225
+
226
+ Use `context.defer()` when you need extra time to compute hook output.
227
+
228
+ ```ts
229
+ import { defineHook } from "cc-hooks-ts";
230
+
231
+ const hook = defineHook({
232
+ trigger: { PostToolUse: { Read: true } },
233
+ run: (context) =>
234
+ context.defer(
235
+ async () => {
236
+ // Simulate long-running computation
237
+ await new Promise((resolve) => setTimeout(resolve, 2000));
238
+
239
+ return {
240
+ event: "PostToolUse",
241
+ output: {
242
+ systemMessage: "Read tool used successfully after async processing!"
243
+ }
244
+ };
245
+ },
246
+ {
247
+ timeoutMs: 5000 // Optional timeout for the async operation.
248
+ }
249
+ )
250
+ });
251
+ ```
252
+
213
253
  ## Documentation
214
254
 
215
255
  For more detailed information about Claude Code hooks, visit the [official documentation](https://docs.anthropic.com/en/docs/claude-code/hooks).
@@ -243,7 +283,7 @@ pnpm typecheck
243
283
  ```bash
244
284
  npm diff --diff=@anthropic-ai/claude-agent-sdk@0.1.69 --diff=@anthropic-ai/claude-agent-sdk@0.1.70 '**/*.d.ts'
245
285
 
246
- # You can use dandavison/delta for better diff visualization
286
+ # Only for humans, You can use dandavison/delta for better diff visualization
247
287
  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
288
  ```
249
289
 
package/dist/index.d.mts CHANGED
@@ -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,7 +28,7 @@ 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
  };
@@ -167,32 +172,66 @@ async function runHook(def) {
167
172
  const parsed = v.parse(inputSchema, JSON.parse(rawInput));
168
173
  eventName = parsed.hook_event_name;
169
174
  const result = await run(createContext(parsed));
170
- handleHookResult(eventName, result);
175
+ await handleHookResult(eventName, result);
171
176
  } catch (error) {
172
- handleHookResult(eventName, {
177
+ await handleHookResult(eventName, {
173
178
  kind: "non-blocking-error",
174
179
  payload: `Error in hook: ${error instanceof Error ? error.message : String(error)}`
175
180
  });
176
181
  }
177
182
  }
178
- function handleHookResult(eventName, hookResult) {
183
+ async function handleHookResult(eventName, hookResult) {
179
184
  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
185
  case "blocking-error":
188
186
  if (hookResult.payload) console.error(hookResult.payload);
189
187
  return process.exit(2);
188
+ case "json-async": {
189
+ const userTimeout = hookResult.timeoutMs;
190
+ const startAsync = {
191
+ async: true,
192
+ asyncTimeout: userTimeout ?? void 0
193
+ };
194
+ console.log(JSON.stringify(startAsync));
195
+ const safeInvokeDeferredHook = async () => {
196
+ try {
197
+ return {
198
+ isError: false,
199
+ payload: await hookResult.run()
200
+ };
201
+ } catch (error) {
202
+ return {
203
+ isError: true,
204
+ reason: error instanceof Error ? error.message : String(error)
205
+ };
206
+ }
207
+ };
208
+ let deferredResult;
209
+ if (userTimeout == null) deferredResult = await safeInvokeDeferredHook();
210
+ else deferredResult = await Promise.race([safeInvokeDeferredHook(), new Promise((resolve) => setTimeout(() => resolve({
211
+ isError: true,
212
+ reason: `Exceeded user specified timeout: ${userTimeout}ms`
213
+ }), userTimeout + 5e3))]);
214
+ if (deferredResult.isError) {
215
+ if (isNonEmptyString(deferredResult.reason)) console.error(`Async hook execution failed: ${deferredResult.reason}`);
216
+ return process.exit(1);
217
+ }
218
+ console.log(JSON.stringify(deferredResult.payload.output));
219
+ return process.exit(0);
220
+ }
221
+ case "json-sync":
222
+ console.log(JSON.stringify(hookResult.payload.output));
223
+ return process.exit(0);
190
224
  case "non-blocking-error":
191
225
  if (isNonEmptyString(hookResult.payload)) console.error(hookResult.payload);
192
226
  return process.exit(1);
193
- case "json":
194
- console.log(JSON.stringify(hookResult.payload.output));
227
+ case "success":
228
+ if (eventName === "UserPromptSubmit" || eventName === "SessionStart") {
229
+ if (isNonEmptyString(hookResult.payload.additionalClaudeContext)) console.log(hookResult.payload.additionalClaudeContext);
230
+ return process.exit(0);
231
+ }
232
+ if (isNonEmptyString(hookResult.payload.messageForUser)) console.log(hookResult.payload.messageForUser);
195
233
  return process.exit(0);
234
+ default: throw new Error(`Unknown hook result kind: ${JSON.stringify(hookResult)}`);
196
235
  }
197
236
  }
198
237
  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.0.76",
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.19.0",
53
53
  "pkg-pr-new": "0.0.62",
54
54
  "publint": "0.3.16",
55
55
  "release-it": "19.1.0",
56
56
  "release-it-pnpm": "4.6.6",
57
- "tsdown": "0.17.2",
57
+ "tsdown": "0.18.1",
58
58
  "type-fest": "5.3.1",
59
59
  "typescript": "5.9.3",
60
- "typescript-eslint": "8.49.0",
60
+ "typescript-eslint": "8.50.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.1.76",
66
66
  "valibot": "^1.1.0"
67
67
  },
68
68
  "scripts": {