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 +41 -1
- package/dist/index.d.mts +162 -10
- package/dist/index.mjs +52 -13
- package/package.json +7 -7
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
|
|
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
|
-
*
|
|
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:
|
|
624
|
+
json: (payload: SyncHookResultJSON<THookTrigger>) => HookResponseSyncJSON<THookTrigger>;
|
|
520
625
|
/**
|
|
521
626
|
* Cause a non-blocking error.
|
|
522
627
|
*
|
|
523
|
-
*
|
|
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 |
|
|
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
|
|
557
|
-
kind: "json";
|
|
558
|
-
payload:
|
|
691
|
+
type HookResponseSyncJSON<TTrigger extends HookTrigger> = {
|
|
692
|
+
kind: "json-sync";
|
|
693
|
+
payload: SyncHookResultJSON<TTrigger>;
|
|
559
694
|
};
|
|
560
|
-
type
|
|
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:
|
|
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 "
|
|
194
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
57
|
+
"tsdown": "0.18.1",
|
|
58
58
|
"type-fest": "5.3.1",
|
|
59
59
|
"typescript": "5.9.3",
|
|
60
|
-
"typescript-eslint": "8.
|
|
60
|
+
"typescript-eslint": "8.50.0",
|
|
61
61
|
"unplugin-unused": "0.5.6",
|
|
62
|
-
"vitest": "4.0.
|
|
62
|
+
"vitest": "4.0.16"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@anthropic-ai/claude-agent-sdk": "0.1.
|
|
65
|
+
"@anthropic-ai/claude-agent-sdk": "0.1.76",
|
|
66
66
|
"valibot": "^1.1.0"
|
|
67
67
|
},
|
|
68
68
|
"scripts": {
|