cc-hooks-ts 2.0.65 → 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,8 +16,10 @@ 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)
22
+ - [How to follow the upstream changes](#how-to-follow-the-upstream-changes)
21
23
  - [License](#license)
22
24
  - [Contributing](#contributing)
23
25
 
@@ -209,6 +211,45 @@ Use `context.json()` to return structured JSON output with advanced control over
209
211
 
210
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).
211
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
+
212
253
  ## Documentation
213
254
 
214
255
  For more detailed information about Claude Code hooks, visit the [official documentation](https://docs.anthropic.com/en/docs/claude-code/hooks).
@@ -232,6 +273,29 @@ pnpm format
232
273
  pnpm typecheck
233
274
  ```
234
275
 
276
+ ### How to follow the upstream changes
277
+
278
+ 1. Install the latest version of `@anthropic-ai/claude-agent-sdk` and run `pnpm run check`.
279
+ - If the command passes without errors, there are no type changes.
280
+
281
+ 2. Get diff of the types. This example gets the diff between Claude Code 2.0.69 and 2.0.70:
282
+
283
+ ```bash
284
+ npm diff --diff=@anthropic-ai/claude-agent-sdk@0.1.69 --diff=@anthropic-ai/claude-agent-sdk@0.1.70 '**/*.d.ts'
285
+
286
+ # Only for humans, You can use dandavison/delta for better diff visualization
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
288
+ ```
289
+
290
+ 3. Reflect the changes.
291
+ - Edit `src/hooks/` for changed hook input / output types.
292
+ - No need for adding tests in most cases since we are testing the whole type definitions in these files:
293
+ - `src/hooks/input/schemas.test-d.ts`
294
+ - `src/hooks/output/index.test-d.ts`
295
+ - `src/hooks/event.test-d.ts`
296
+ - `src/hooks/permission.test-d.ts`
297
+ - Edit `src/index.ts` for changed tool input / output types.
298
+
235
299
  ## License
236
300
 
237
301
  MIT
package/dist/index.d.mts CHANGED
@@ -157,7 +157,7 @@ declare const HookInputSchemas: {
157
157
  readonly type: v.LiteralSchema<"removeRules", undefined>;
158
158
  }, undefined>, v.ObjectSchema<{
159
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<"plan", 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>;
161
161
  readonly type: v.LiteralSchema<"setMode", undefined>;
162
162
  }, undefined>, v.ObjectSchema<{
163
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>;
@@ -290,7 +290,7 @@ declare const permissionUpdateSchema: v.VariantSchema<"type", [v.ObjectSchema<{
290
290
  readonly type: v.LiteralSchema<"removeRules", undefined>;
291
291
  }, undefined>, v.ObjectSchema<{
292
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<"plan", 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>;
294
294
  readonly type: v.LiteralSchema<"setMode", undefined>;
295
295
  }, undefined>, v.ObjectSchema<{
296
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>;
@@ -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
  };
@@ -70,6 +75,7 @@ const permissionUpdateSchema = v.variant("type", [
70
75
  v.literal("bypassPermissions"),
71
76
  v.literal("default"),
72
77
  v.literal("dontAsk"),
78
+ v.literal("delegate"),
73
79
  v.literal("plan")
74
80
  ]),
75
81
  type: v.literal("setMode")
@@ -166,32 +172,66 @@ async function runHook(def) {
166
172
  const parsed = v.parse(inputSchema, JSON.parse(rawInput));
167
173
  eventName = parsed.hook_event_name;
168
174
  const result = await run(createContext(parsed));
169
- handleHookResult(eventName, result);
175
+ await handleHookResult(eventName, result);
170
176
  } catch (error) {
171
- handleHookResult(eventName, {
177
+ await handleHookResult(eventName, {
172
178
  kind: "non-blocking-error",
173
179
  payload: `Error in hook: ${error instanceof Error ? error.message : String(error)}`
174
180
  });
175
181
  }
176
182
  }
177
- function handleHookResult(eventName, hookResult) {
183
+ async function handleHookResult(eventName, hookResult) {
178
184
  switch (hookResult.kind) {
179
- case "success":
180
- if (eventName === "UserPromptSubmit" || eventName === "SessionStart") {
181
- if (isNonEmptyString(hookResult.payload.additionalClaudeContext)) console.log(hookResult.payload.additionalClaudeContext);
182
- return process.exit(0);
183
- }
184
- if (isNonEmptyString(hookResult.payload.messageForUser)) console.log(hookResult.payload.messageForUser);
185
- return process.exit(0);
186
185
  case "blocking-error":
187
186
  if (hookResult.payload) console.error(hookResult.payload);
188
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);
189
224
  case "non-blocking-error":
190
225
  if (isNonEmptyString(hookResult.payload)) console.error(hookResult.payload);
191
226
  return process.exit(1);
192
- case "json":
193
- 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);
194
233
  return process.exit(0);
234
+ default: throw new Error(`Unknown hook result kind: ${JSON.stringify(hookResult)}`);
195
235
  }
196
236
  }
197
237
  function extractInputSchemaFromTrigger(trigger) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-hooks-ts",
3
- "version": "2.0.65",
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": "24.10.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
- "eslint": "9.39.1",
50
+ "eslint": "9.39.2",
51
51
  "eslint-plugin-import-access": "3.1.0",
52
- "oxfmt": "0.16.0",
52
+ "oxfmt": "0.19.0",
53
53
  "pkg-pr-new": "0.0.62",
54
- "publint": "0.3.15",
55
- "release-it": "19.0.6",
54
+ "publint": "0.3.16",
55
+ "release-it": "19.1.0",
56
56
  "release-it-pnpm": "4.6.6",
57
- "tsdown": "0.17.0",
57
+ "tsdown": "0.18.1",
58
58
  "type-fest": "5.3.1",
59
59
  "typescript": "5.9.3",
60
- "typescript-eslint": "8.48.1",
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.65",
65
+ "@anthropic-ai/claude-agent-sdk": "0.1.76",
66
66
  "valibot": "^1.1.0"
67
67
  },
68
68
  "scripts": {