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 +43 -1
- package/dist/index.d.mts +185 -33
- package/dist/index.mjs +81 -37
- package/package.json +8 -8
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.
|
|
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.
|
|
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.
|
|
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.
|
|
136
|
-
readonly destination: v.
|
|
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.
|
|
144
|
-
readonly destination: v.
|
|
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.
|
|
152
|
-
readonly destination: v.
|
|
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.
|
|
160
|
-
readonly mode: v.
|
|
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.
|
|
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.
|
|
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.
|
|
269
|
-
readonly destination: v.
|
|
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.
|
|
277
|
-
readonly destination: v.
|
|
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.
|
|
285
|
-
readonly destination: v.
|
|
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.
|
|
293
|
-
readonly mode: v.
|
|
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.
|
|
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.
|
|
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
|
|
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,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.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
const permissionBehaviorSchema = v.picklist([
|
|
37
|
+
"allow",
|
|
38
|
+
"deny",
|
|
39
|
+
"ask"
|
|
35
40
|
]);
|
|
36
|
-
const permissionUpdateDestinationSchema = v.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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.
|
|
144
|
+
trigger: v.picklist(["manual", "auto"])
|
|
140
145
|
}),
|
|
141
|
-
SessionStart: buildHookInputSchema("SessionStart", { source: v.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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 "
|
|
194
|
-
|
|
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.
|
|
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.
|
|
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.21.0",
|
|
53
53
|
"pkg-pr-new": "0.0.62",
|
|
54
54
|
"publint": "0.3.16",
|
|
55
|
-
"release-it": "19.
|
|
55
|
+
"release-it": "19.2.2",
|
|
56
56
|
"release-it-pnpm": "4.6.6",
|
|
57
|
-
"tsdown": "0.
|
|
57
|
+
"tsdown": "0.18.4",
|
|
58
58
|
"type-fest": "5.3.1",
|
|
59
59
|
"typescript": "5.9.3",
|
|
60
|
-
"typescript-eslint": "8.
|
|
60
|
+
"typescript-eslint": "8.51.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.2.1",
|
|
66
66
|
"valibot": "^1.1.0"
|
|
67
67
|
},
|
|
68
68
|
"scripts": {
|