cc-hooks-ts 0.0.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/LICENSE +21 -0
- package/README.md +217 -0
- package/dist/index.d.mts +562 -0
- package/dist/index.mjs +130 -0
- package/package.json +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 sushichan044
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# cc-hooks-ts
|
|
2
|
+
|
|
3
|
+
Define Claude Code hooks with full type safety using TypeScript and Valibot validation.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx npym add cc-hooks-ts
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Basic Usage
|
|
12
|
+
|
|
13
|
+
> [!NOTE]
|
|
14
|
+
> We highly recommend using Bun or Deno for automatic dependency downloading at runtime.
|
|
15
|
+
>
|
|
16
|
+
> - Deno: <https://docs.deno.com/runtime/fundamentals/modules/#managing-third-party-modules-and-libraries>
|
|
17
|
+
> - Bun: <https://bun.com/docs/runtime/autoimport>
|
|
18
|
+
|
|
19
|
+
### Define a Hook
|
|
20
|
+
|
|
21
|
+
With Deno:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
#!/usr/bin/env -S deno run --quiet --allow-env --allow-read
|
|
25
|
+
import { defineHook, runHook } from "cc-hooks-ts";
|
|
26
|
+
|
|
27
|
+
// Session start hook
|
|
28
|
+
const sessionHook = defineHook({
|
|
29
|
+
trigger: { SessionStart: true },
|
|
30
|
+
run: (context) => {
|
|
31
|
+
console.log(`Session started: ${context.input.session_id}`);
|
|
32
|
+
return context.success({
|
|
33
|
+
messageForUser: "Welcome to your coding session!"
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await runHook(sessionHook);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or with Bun:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
#!/usr/bin/env -S bun run --silent
|
|
45
|
+
import { defineHook, runHook } from "cc-hooks-ts";
|
|
46
|
+
|
|
47
|
+
const sessionHook = defineHook({
|
|
48
|
+
trigger: { SessionStart: true },
|
|
49
|
+
run: (context) => {
|
|
50
|
+
return context.success({
|
|
51
|
+
messageForUser: "Session started!"
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await runHook(sessionHook);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Call from Claude Code
|
|
60
|
+
|
|
61
|
+
Then, load defined hooks in your Claude Code settings at `~/.claude/settings.json`.
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"hooks": {
|
|
66
|
+
"SessionStart": [
|
|
67
|
+
{
|
|
68
|
+
"hooks": [
|
|
69
|
+
{
|
|
70
|
+
"type": "command",
|
|
71
|
+
"command": "$HOME/.claude/<name-of-your-hooks>.ts"
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Custom Tool Type Support
|
|
81
|
+
|
|
82
|
+
For better type inference in PreToolUse and PostToolUse hooks, you can extend the `ToolSchema` interface to define your own tool types:
|
|
83
|
+
|
|
84
|
+
### Adding Custom Tool Definitions
|
|
85
|
+
|
|
86
|
+
Extend the `ToolSchema` interface to add custom tool definitions:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
declare module "cc-hooks-ts" {
|
|
90
|
+
interface ToolSchema {
|
|
91
|
+
MyCustomTool: {
|
|
92
|
+
input: {
|
|
93
|
+
customParam: string;
|
|
94
|
+
optionalParam?: number;
|
|
95
|
+
};
|
|
96
|
+
response: {
|
|
97
|
+
result: string;
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Now you can use your custom tool with full type safety:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
const customToolHook = defineHook({
|
|
108
|
+
trigger: { PreToolUse: { MyCustomTool: true } },
|
|
109
|
+
run: (context) => {
|
|
110
|
+
// context.input.tool_input is typed as { customParam: string; optionalParam?: number; }
|
|
111
|
+
const { customParam, optionalParam } = context.input.tool_input;
|
|
112
|
+
return context.success();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## API Reference
|
|
118
|
+
|
|
119
|
+
### defineHook Function
|
|
120
|
+
|
|
121
|
+
Creates type-safe hook definitions with full TypeScript inference:
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// Tool-specific PreToolUse hook
|
|
125
|
+
const readHook = defineHook({
|
|
126
|
+
trigger: { PreToolUse: { Read: true } },
|
|
127
|
+
run: (context) => {
|
|
128
|
+
// context.input.tool_input is typed as { file_path: string }
|
|
129
|
+
const { file_path } = context.input.tool_input;
|
|
130
|
+
|
|
131
|
+
if (file_path.includes('.env')) {
|
|
132
|
+
return context.blockingError('Cannot read environment files');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return context.success();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Multiple event triggers
|
|
140
|
+
const multiEventHook = defineHook({
|
|
141
|
+
trigger: {
|
|
142
|
+
PreToolUse: { Read: true, WebFetch: true },
|
|
143
|
+
PostToolUse: { Read: true }
|
|
144
|
+
},
|
|
145
|
+
shouldRun: () => process.env.NODE_ENV === 'development',
|
|
146
|
+
run: (context) => {
|
|
147
|
+
// Handle different events and tools based on context.input
|
|
148
|
+
return context.success();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### runHook Function
|
|
154
|
+
|
|
155
|
+
Executes hooks with complete lifecycle management:
|
|
156
|
+
|
|
157
|
+
- Reads input from stdin
|
|
158
|
+
- Validates input using Valibot schemas
|
|
159
|
+
- Creates typed context
|
|
160
|
+
- Executes hook handler
|
|
161
|
+
- Formats and outputs results
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
await runHook(hook);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Hook Context
|
|
168
|
+
|
|
169
|
+
The context provides strongly typed input access and response helpers:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
run: (context) => {
|
|
173
|
+
// Typed input based on trigger configuration
|
|
174
|
+
const input = context.input;
|
|
175
|
+
|
|
176
|
+
// Response helpers
|
|
177
|
+
return context.success({ messageForUser: "Success!" });
|
|
178
|
+
// or context.blockingError("Error occurred");
|
|
179
|
+
// or context.nonBlockingError("Warning message");
|
|
180
|
+
// or context.json({ event: "EventName", output: {...} });
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Development
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
# Run tests
|
|
188
|
+
pnpm test
|
|
189
|
+
|
|
190
|
+
# Build
|
|
191
|
+
pnpm build
|
|
192
|
+
|
|
193
|
+
# Lint
|
|
194
|
+
pnpm lint
|
|
195
|
+
|
|
196
|
+
# Format
|
|
197
|
+
pnpm format
|
|
198
|
+
|
|
199
|
+
# Type check
|
|
200
|
+
pnpm typecheck
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Documentation
|
|
204
|
+
|
|
205
|
+
For more detailed information about Claude Code hooks, visit the [official documentation](https://docs.anthropic.com/en/docs/claude-code/hooks).
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT
|
|
210
|
+
|
|
211
|
+
## Contributing
|
|
212
|
+
|
|
213
|
+
We welcome contributions! Feel free to open issues or submit pull requests.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
**Made with ❤️ for hackers using Claude Code**
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
|
|
3
|
+
//#region src/event.d.ts
|
|
4
|
+
declare const SUPPORTED_HOOK_EVENTS: readonly ["PreToolUse", "PostToolUse", "Notification", "UserPromptSubmit", "Stop", "SubagentStop", "PreCompact", "SessionStart", "SessionEnd"];
|
|
5
|
+
/**
|
|
6
|
+
* @see {@link https://docs.anthropic.com/en/docs/claude-code/hooks#hook-events}
|
|
7
|
+
*/
|
|
8
|
+
type SupportedHookEvent = (typeof SUPPORTED_HOOK_EVENTS)[number];
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/output.d.ts
|
|
11
|
+
type HookOutput = {
|
|
12
|
+
PreToolUse: PreToolUseHookOutput;
|
|
13
|
+
PostToolUse: PostToolUseHookOutput;
|
|
14
|
+
UserPromptSubmit: UserPromptSubmitHookOutput;
|
|
15
|
+
Stop: StopHookOutput;
|
|
16
|
+
SubagentStop: SubagentStopHookOutput;
|
|
17
|
+
SessionStart: SessionStartHookOutput;
|
|
18
|
+
Notification: CommonHookOutputs;
|
|
19
|
+
PreCompact: CommonHookOutputs;
|
|
20
|
+
SessionEnd: CommonHookOutputs;
|
|
21
|
+
};
|
|
22
|
+
type ExtractHookOutput<TEvent extends SupportedHookEvent> = HookOutput extends Record<SupportedHookEvent, unknown> ? HookOutput[TEvent] : never;
|
|
23
|
+
/**
|
|
24
|
+
* Common fields of hook outputs
|
|
25
|
+
*
|
|
26
|
+
* @see {@link https://docs.anthropic.com/en/docs/claude-code/hooks#common-json-fields}
|
|
27
|
+
*/
|
|
28
|
+
type CommonHookOutputs = {
|
|
29
|
+
/**
|
|
30
|
+
* Whether Claude should continue after hook execution
|
|
31
|
+
*
|
|
32
|
+
* If `continue` is false, Claude stops processing after the hooks run.
|
|
33
|
+
* @default true
|
|
34
|
+
*/
|
|
35
|
+
continue?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Accompanies `continue` with a `reason` shown to the user, not shown to Claude.
|
|
38
|
+
*
|
|
39
|
+
* NOT FOR CLAUDE
|
|
40
|
+
*/
|
|
41
|
+
stopReason?: string;
|
|
42
|
+
/**
|
|
43
|
+
* If `true`, user cannot see the stdout of this hook.
|
|
44
|
+
*
|
|
45
|
+
* @default false
|
|
46
|
+
*/
|
|
47
|
+
suppressOutput?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Optional warning message shown to the user
|
|
50
|
+
*/
|
|
51
|
+
systemMessage?: string;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* @see {@link https://docs.anthropic.com/en/docs/claude-code/hooks#pretooluse-decision-control}
|
|
55
|
+
*/
|
|
56
|
+
interface PreToolUseHookOutput extends CommonHookOutputs {
|
|
57
|
+
/**
|
|
58
|
+
* - `block` automatically prompts Claude with `reason`.
|
|
59
|
+
* - `undefined` does nothing. `reason` is ignored.
|
|
60
|
+
*/
|
|
61
|
+
decision: "block" | undefined;
|
|
62
|
+
hookSpecificOutput?: {
|
|
63
|
+
hookEventName: "PreToolUse";
|
|
64
|
+
/**
|
|
65
|
+
* Adds context for Claude to consider.
|
|
66
|
+
*/
|
|
67
|
+
additionalContext?: string;
|
|
68
|
+
};
|
|
69
|
+
reason?: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* @see {@link https://docs.anthropic.com/en/docs/claude-code/hooks#posttooluse-decision-control}
|
|
73
|
+
*/
|
|
74
|
+
interface PostToolUseHookOutput extends CommonHookOutputs {
|
|
75
|
+
/**
|
|
76
|
+
* - `block` automatically prompts Claude with `reason`.
|
|
77
|
+
* - `undefined` does nothing. `reason` is ignored.
|
|
78
|
+
*/
|
|
79
|
+
decision: "block" | undefined;
|
|
80
|
+
hookSpecificOutput?: {
|
|
81
|
+
hookEventName: "PostToolUse";
|
|
82
|
+
/**
|
|
83
|
+
* Adds context for Claude to consider.
|
|
84
|
+
*/
|
|
85
|
+
additionalContext?: string;
|
|
86
|
+
};
|
|
87
|
+
reason?: string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* @see {@link https://docs.anthropic.com/en/docs/claude-code/hooks#userpromptsubmit-decision-control}
|
|
91
|
+
*/
|
|
92
|
+
interface UserPromptSubmitHookOutput extends CommonHookOutputs {
|
|
93
|
+
/**
|
|
94
|
+
* - `block` prevents the prompt from being processed.
|
|
95
|
+
* The submitted prompt is erased from context. `reason` is shown to the user but not added to context.
|
|
96
|
+
*
|
|
97
|
+
* - `undefined` allows the prompt to proceed normally. `reason` is ignored.
|
|
98
|
+
*/
|
|
99
|
+
decision?: "block" | undefined;
|
|
100
|
+
hookSpecificOutput?: {
|
|
101
|
+
hookEventName: "UserPromptSubmit";
|
|
102
|
+
/**
|
|
103
|
+
* Adds the string to the context if not blocked.
|
|
104
|
+
*/
|
|
105
|
+
additionalContext?: string;
|
|
106
|
+
};
|
|
107
|
+
reason?: string;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* @see {@link https://docs.anthropic.com/en/docs/claude-code/hooks#stop%2Fsubagentstop-decision-control}
|
|
111
|
+
*/
|
|
112
|
+
interface StopHookOutput extends CommonHookOutputs {
|
|
113
|
+
/**
|
|
114
|
+
* - `block` prevents Claude from stopping. You must populate `reason` for Claude to know how to proceed.
|
|
115
|
+
*
|
|
116
|
+
* - `undefined` allows Claude to stop. `reason` is ignored.
|
|
117
|
+
*/
|
|
118
|
+
decision: "block" | undefined;
|
|
119
|
+
/**
|
|
120
|
+
* Reason for the decision.
|
|
121
|
+
*/
|
|
122
|
+
reason?: string;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* @see {@link https://docs.anthropic.com/en/docs/claude-code/hooks#stop%2Fsubagentstop-decision-control}
|
|
126
|
+
*/
|
|
127
|
+
interface SubagentStopHookOutput extends CommonHookOutputs {
|
|
128
|
+
/**
|
|
129
|
+
* - `block` prevents Claude from stopping. You must populate `reason` for Claude to know how to proceed.
|
|
130
|
+
*
|
|
131
|
+
* - `undefined` allows Claude to stop. `reason` is ignored.
|
|
132
|
+
*/
|
|
133
|
+
decision: "block" | undefined;
|
|
134
|
+
/**
|
|
135
|
+
* Reason for the decision.
|
|
136
|
+
*/
|
|
137
|
+
reason?: string;
|
|
138
|
+
}
|
|
139
|
+
interface SessionStartHookOutput extends CommonHookOutputs {
|
|
140
|
+
hookSpecificOutput?: {
|
|
141
|
+
hookEventName: "SessionStart";
|
|
142
|
+
/**
|
|
143
|
+
* Adds the string to the context.
|
|
144
|
+
*/
|
|
145
|
+
additionalContext?: string;
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/input/schemas.d.ts
|
|
150
|
+
/**
|
|
151
|
+
* @package
|
|
152
|
+
*/
|
|
153
|
+
declare const HookInputSchemas: {
|
|
154
|
+
readonly PreToolUse: v.ObjectSchema<{
|
|
155
|
+
readonly hook_event_name: v.LiteralSchema<"PreToolUse", undefined>;
|
|
156
|
+
readonly cwd: v.StringSchema<undefined>;
|
|
157
|
+
readonly session_id: v.StringSchema<undefined>;
|
|
158
|
+
readonly transcript_path: v.StringSchema<undefined>;
|
|
159
|
+
} & {
|
|
160
|
+
tool_input: v.UnknownSchema;
|
|
161
|
+
tool_name: v.IntersectSchema<[v.StringSchema<undefined>, v.RecordSchema<v.StringSchema<undefined>, v.NeverSchema<undefined>, undefined>], undefined>;
|
|
162
|
+
}, undefined>;
|
|
163
|
+
readonly PostToolUse: v.ObjectSchema<{
|
|
164
|
+
readonly hook_event_name: v.LiteralSchema<"PostToolUse", undefined>;
|
|
165
|
+
readonly cwd: v.StringSchema<undefined>;
|
|
166
|
+
readonly session_id: v.StringSchema<undefined>;
|
|
167
|
+
readonly transcript_path: v.StringSchema<undefined>;
|
|
168
|
+
} & {
|
|
169
|
+
tool_input: v.UnknownSchema;
|
|
170
|
+
tool_name: v.StringSchema<undefined>;
|
|
171
|
+
tool_response: v.UnknownSchema;
|
|
172
|
+
}, undefined>;
|
|
173
|
+
readonly Notification: v.ObjectSchema<{
|
|
174
|
+
readonly hook_event_name: v.LiteralSchema<"Notification", undefined>;
|
|
175
|
+
readonly cwd: v.StringSchema<undefined>;
|
|
176
|
+
readonly session_id: v.StringSchema<undefined>;
|
|
177
|
+
readonly transcript_path: v.StringSchema<undefined>;
|
|
178
|
+
} & {
|
|
179
|
+
message: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
180
|
+
}, undefined>;
|
|
181
|
+
readonly UserPromptSubmit: v.ObjectSchema<{
|
|
182
|
+
readonly hook_event_name: v.LiteralSchema<"UserPromptSubmit", undefined>;
|
|
183
|
+
readonly cwd: v.StringSchema<undefined>;
|
|
184
|
+
readonly session_id: v.StringSchema<undefined>;
|
|
185
|
+
readonly transcript_path: v.StringSchema<undefined>;
|
|
186
|
+
} & {
|
|
187
|
+
prompt: v.StringSchema<undefined>;
|
|
188
|
+
}, undefined>;
|
|
189
|
+
readonly Stop: v.ObjectSchema<{
|
|
190
|
+
readonly hook_event_name: v.LiteralSchema<"Stop", undefined>;
|
|
191
|
+
readonly cwd: v.StringSchema<undefined>;
|
|
192
|
+
readonly session_id: v.StringSchema<undefined>;
|
|
193
|
+
readonly transcript_path: v.StringSchema<undefined>;
|
|
194
|
+
} & {
|
|
195
|
+
stop_hook_active: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
196
|
+
}, undefined>;
|
|
197
|
+
readonly SubagentStop: v.ObjectSchema<{
|
|
198
|
+
readonly hook_event_name: v.LiteralSchema<"SubagentStop", undefined>;
|
|
199
|
+
readonly cwd: v.StringSchema<undefined>;
|
|
200
|
+
readonly session_id: v.StringSchema<undefined>;
|
|
201
|
+
readonly transcript_path: v.StringSchema<undefined>;
|
|
202
|
+
} & {
|
|
203
|
+
stop_hook_active: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
204
|
+
}, undefined>;
|
|
205
|
+
readonly PreCompact: v.ObjectSchema<{
|
|
206
|
+
readonly hook_event_name: v.LiteralSchema<"PreCompact", undefined>;
|
|
207
|
+
readonly cwd: v.StringSchema<undefined>;
|
|
208
|
+
readonly session_id: v.StringSchema<undefined>;
|
|
209
|
+
readonly transcript_path: v.StringSchema<undefined>;
|
|
210
|
+
} & {
|
|
211
|
+
custom_instructions: v.StringSchema<undefined>;
|
|
212
|
+
trigger: v.UnionSchema<[v.LiteralSchema<"manual", undefined>, v.LiteralSchema<"auto", undefined>], undefined>;
|
|
213
|
+
}, undefined>;
|
|
214
|
+
readonly SessionStart: v.ObjectSchema<{
|
|
215
|
+
readonly hook_event_name: v.LiteralSchema<"SessionStart", undefined>;
|
|
216
|
+
readonly cwd: v.StringSchema<undefined>;
|
|
217
|
+
readonly session_id: v.StringSchema<undefined>;
|
|
218
|
+
readonly transcript_path: v.StringSchema<undefined>;
|
|
219
|
+
} & {
|
|
220
|
+
source: v.StringSchema<undefined>;
|
|
221
|
+
}, undefined>;
|
|
222
|
+
readonly SessionEnd: v.ObjectSchema<{
|
|
223
|
+
readonly hook_event_name: v.LiteralSchema<"SessionEnd", undefined>;
|
|
224
|
+
readonly cwd: v.StringSchema<undefined>;
|
|
225
|
+
readonly session_id: v.StringSchema<undefined>;
|
|
226
|
+
readonly transcript_path: v.StringSchema<undefined>;
|
|
227
|
+
} & {
|
|
228
|
+
reason: v.StringSchema<undefined>;
|
|
229
|
+
}, undefined>;
|
|
230
|
+
};
|
|
231
|
+
//#endregion
|
|
232
|
+
//#region src/input/types.d.ts
|
|
233
|
+
/**
|
|
234
|
+
* Internal type that combines base hook inputs with tool-specific inputs for PreToolUse events.
|
|
235
|
+
* For non-PreToolUse events, this is equivalent to BaseHookInputs.
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* ```ts
|
|
239
|
+
* type PreToolUseInputs = HookInputs["PreToolUse"]
|
|
240
|
+
* // Result: Base inputs + { Read: SpecifiedToolUseInput<"Read">, WebFetch: SpecifiedToolUseInput<"WebFetch"> }
|
|
241
|
+
* ```
|
|
242
|
+
* @package
|
|
243
|
+
*/
|
|
244
|
+
type HookInputs = { [EventKey in SupportedHookEvent]: EventKey extends "PreToolUse" ? ToolSpecificPreToolUseInput & {
|
|
245
|
+
default: BaseHookInputs["PreToolUse"];
|
|
246
|
+
} : EventKey extends "PostToolUse" ? ToolSpecificPostToolUseInput & {
|
|
247
|
+
default: BaseHookInputs["PostToolUse"];
|
|
248
|
+
} : {
|
|
249
|
+
default: BaseHookInputs[EventKey];
|
|
250
|
+
} };
|
|
251
|
+
/**
|
|
252
|
+
* Extracts all possible hook input types for a specific event type.
|
|
253
|
+
* For non-tool-specific events, this returns only the default input type.
|
|
254
|
+
* For tool-specific events (PreToolUse/PostToolUse), this returns a union of all possible inputs including default and tool-specific variants.
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* ```ts
|
|
258
|
+
* // For non-tool-specific events
|
|
259
|
+
* type SessionStartInputs = ExtractAllHookInputsForEvent<"SessionStart">
|
|
260
|
+
* // Result: { cwd: string; hook_event_name: "SessionStart"; session_id: string; transcript_path: string }
|
|
261
|
+
*
|
|
262
|
+
* // For tool-specific events
|
|
263
|
+
* type PreToolUseInputs = ExtractAllHookInputsForEvent<"PreToolUse">
|
|
264
|
+
* // Result: Union of default input + all tool-specific inputs (Read, WebFetch, etc.)
|
|
265
|
+
* ```
|
|
266
|
+
* @package
|
|
267
|
+
*/
|
|
268
|
+
type ExtractAllHookInputsForEvent<TEvent extends SupportedHookEvent> = { [K in keyof HookInputs[TEvent]]: HookInputs[TEvent][K] }[keyof HookInputs[TEvent]];
|
|
269
|
+
/**
|
|
270
|
+
* Extracts the hook input type for a specific tool within a given event type.
|
|
271
|
+
* This type utility is used to get strongly-typed inputs for tool-specific hook handlers.
|
|
272
|
+
* The second parameter is constrained to valid tool names for the given event type.
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* ```ts
|
|
276
|
+
* // Extract Read tool input for PreToolUse event
|
|
277
|
+
* type ReadPreInput = ExtractSpecificHookInputForEvent<"PreToolUse", "Read">
|
|
278
|
+
* // Result: { cwd: string; hook_event_name: "PreToolUse"; session_id: string; transcript_path: string; tool_input: ReadInput; tool_name: "Read" }
|
|
279
|
+
*
|
|
280
|
+
* // Extract WebFetch tool input for PostToolUse event
|
|
281
|
+
* type WebFetchPostInput = ExtractSpecificHookInputForEvent<"PostToolUse", "WebFetch">
|
|
282
|
+
* // Result: { cwd: string; hook_event_name: "PostToolUse"; session_id: string; transcript_path: string; tool_input: WebFetchInput; tool_name: "WebFetch"; tool_response: WebFetchResponse }
|
|
283
|
+
*
|
|
284
|
+
* // Type error: "Read" is not valid for non-tool-specific events
|
|
285
|
+
* type Invalid = ExtractSpecificHookInputForEvent<"SessionStart", "Read"> // ❌ Compile error
|
|
286
|
+
* ```
|
|
287
|
+
* @package
|
|
288
|
+
*/
|
|
289
|
+
type ExtractSpecificHookInputForEvent<TEvent extends SupportedHookEvent, TSpecificKey extends ExtractExtendedSpecificKeys<TEvent>> = { [K in keyof HookInputs[TEvent]]: K extends TSpecificKey ? HookInputs[TEvent][K] : never }[keyof HookInputs[TEvent]];
|
|
290
|
+
/**
|
|
291
|
+
* @package
|
|
292
|
+
*/
|
|
293
|
+
type ExtractExtendedSpecificKeys<TEvent extends SupportedHookEvent> = Exclude<keyof HookInputs[TEvent], "default">;
|
|
294
|
+
type BaseHookInputs = { [EventKey in SupportedHookEvent]: v.InferOutput<(typeof HookInputSchemas)[EventKey]> };
|
|
295
|
+
type ToolSpecificPreToolUseInput = { [K in keyof ToolSchema]: Omit<BaseHookInputs["PreToolUse"], "tool_input" | "tool_name"> & {
|
|
296
|
+
tool_input: ToolSchema[K]["input"];
|
|
297
|
+
tool_name: K;
|
|
298
|
+
} };
|
|
299
|
+
type ToolSpecificPostToolUseInput = { [K in keyof ToolSchema]: Omit<BaseHookInputs["PostToolUse"], "tool_input" | "tool_name" | "tool_response"> & {
|
|
300
|
+
tool_input: ToolSchema[K]["input"];
|
|
301
|
+
tool_name: K;
|
|
302
|
+
tool_response: ToolSchema[K]["response"];
|
|
303
|
+
} };
|
|
304
|
+
//#endregion
|
|
305
|
+
//#region src/types.d.ts
|
|
306
|
+
type HookTrigger = Partial<{ [TEvent in SupportedHookEvent]: true | Partial<{ [SchemaKey in ExtractExtendedSpecificKeys<TEvent>]?: true }> }>;
|
|
307
|
+
type ExtractTriggeredHookInput<TTrigger extends HookTrigger> = { [EventKey in keyof TTrigger]: EventKey extends SupportedHookEvent ? TTrigger[EventKey] extends true ? ExtractAllHookInputsForEvent<EventKey> : TTrigger[EventKey] extends Record<PropertyKey, true> ? { [SpecificKey in keyof TTrigger[EventKey]]: SpecificKey extends ExtractExtendedSpecificKeys<EventKey> ? ExtractSpecificHookInputForEvent<EventKey, SpecificKey> : never }[keyof TTrigger[EventKey]] : never : never }[keyof TTrigger];
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region src/context.d.ts
|
|
310
|
+
interface HookContext<THookTrigger extends HookTrigger> {
|
|
311
|
+
/**
|
|
312
|
+
* Cause a blocking error.
|
|
313
|
+
*
|
|
314
|
+
* @param error `error` is fed back to Claude to process automatically
|
|
315
|
+
*/
|
|
316
|
+
blockingError: (error: string) => HookResponseBlockingError;
|
|
317
|
+
input: ExtractTriggeredHookInput<THookTrigger>;
|
|
318
|
+
/**
|
|
319
|
+
* Direct access to advanced JSON output.
|
|
320
|
+
*
|
|
321
|
+
* @see {@link https://docs.anthropic.com/en/docs/claude-code/hooks#advanced%3A-json-output}
|
|
322
|
+
*/
|
|
323
|
+
json: (payload: HookResultJSON<THookTrigger>) => HookResponseJSON<THookTrigger>;
|
|
324
|
+
/**
|
|
325
|
+
* Cause a non-blocking error.
|
|
326
|
+
*
|
|
327
|
+
* @param message `message` is shown to the user and execution continues.
|
|
328
|
+
*/
|
|
329
|
+
nonBlockingError: (message?: string) => HookResponseNonBlockingError;
|
|
330
|
+
/**
|
|
331
|
+
* Indicate successful handling of the hook.
|
|
332
|
+
*/
|
|
333
|
+
success: (payload?: HookSuccessPayload) => HookResponseSuccess;
|
|
334
|
+
}
|
|
335
|
+
type HookResponse<THookTrigger extends HookTrigger> = HookResponseBlockingError | HookResponseJSON<THookTrigger> | HookResponseNonBlockingError | HookResponseSuccess;
|
|
336
|
+
type HookResponseNonBlockingError = {
|
|
337
|
+
kind: "non-blocking-error";
|
|
338
|
+
payload?: string;
|
|
339
|
+
};
|
|
340
|
+
type HookResponseBlockingError = {
|
|
341
|
+
kind: "blocking-error";
|
|
342
|
+
payload: string;
|
|
343
|
+
};
|
|
344
|
+
type HookResponseSuccess = {
|
|
345
|
+
kind: "success";
|
|
346
|
+
payload: HookSuccessPayload;
|
|
347
|
+
};
|
|
348
|
+
type HookSuccessPayload = {
|
|
349
|
+
/**
|
|
350
|
+
* Message shown to the user in transcript mode.
|
|
351
|
+
*/
|
|
352
|
+
messageForUser?: string | undefined;
|
|
353
|
+
/**
|
|
354
|
+
* Additional context for Claude.
|
|
355
|
+
*
|
|
356
|
+
* Only works for `UserPromptSubmit` and `SessionStart` hooks.
|
|
357
|
+
*/
|
|
358
|
+
additionalClaudeContext?: string | undefined;
|
|
359
|
+
};
|
|
360
|
+
type HookResponseJSON<TTrigger extends HookTrigger> = {
|
|
361
|
+
kind: "json";
|
|
362
|
+
payload: HookResultJSON<TTrigger>;
|
|
363
|
+
};
|
|
364
|
+
type HookResultJSON<TTrigger extends HookTrigger> = { [EventKey in keyof TTrigger]: EventKey extends SupportedHookEvent ? TTrigger[EventKey] extends true | Record<PropertyKey, true> ? {
|
|
365
|
+
/**
|
|
366
|
+
* The name of the event being triggered.
|
|
367
|
+
*
|
|
368
|
+
* Required for proper TypeScript inference.
|
|
369
|
+
*/
|
|
370
|
+
event: EventKey;
|
|
371
|
+
/**
|
|
372
|
+
* The output data for the event.
|
|
373
|
+
*/
|
|
374
|
+
output: ExtractHookOutput<EventKey>;
|
|
375
|
+
} : never : never }[keyof TTrigger];
|
|
376
|
+
//#endregion
|
|
377
|
+
//#region src/utils/types.d.ts
|
|
378
|
+
type Awaitable<T> = Promise<T> | T;
|
|
379
|
+
//#endregion
|
|
380
|
+
//#region src/define.d.ts
|
|
381
|
+
/**
|
|
382
|
+
* Creates a type-safe Claude Code hook definition.
|
|
383
|
+
*
|
|
384
|
+
* This function provides a way to define hooks with full TypeScript type safety,
|
|
385
|
+
* including input validation using Valibot schemas and strongly typed context.
|
|
386
|
+
*
|
|
387
|
+
* @param definition - The hook definition object containing trigger events and handler function
|
|
388
|
+
* @returns The same hook definition, but with enhanced type safety
|
|
389
|
+
*
|
|
390
|
+
* @example
|
|
391
|
+
* ```ts
|
|
392
|
+
* // Basic session start hook
|
|
393
|
+
* const sessionHook = defineHook({
|
|
394
|
+
* trigger: { SessionStart: true },
|
|
395
|
+
* run: (context) => {
|
|
396
|
+
* console.log(`Session started: ${context.input.session_id}`);
|
|
397
|
+
* return context.success({
|
|
398
|
+
* messageForUser: "Welcome to your coding session!"
|
|
399
|
+
* });
|
|
400
|
+
* }
|
|
401
|
+
* });
|
|
402
|
+
*
|
|
403
|
+
* // Tool-specific PreToolUse hook
|
|
404
|
+
* const readHook = defineHook({
|
|
405
|
+
* trigger: { PreToolUse: { Read: true } },
|
|
406
|
+
* run: (context) => {
|
|
407
|
+
* // context.input.tool_input is typed as { file_path: string }
|
|
408
|
+
* const { file_path } = context.input.tool_input;
|
|
409
|
+
*
|
|
410
|
+
* if (file_path.includes('.env')) {
|
|
411
|
+
* return context.blockingError('Cannot read environment files');
|
|
412
|
+
* }
|
|
413
|
+
*
|
|
414
|
+
* return context.success();
|
|
415
|
+
* }
|
|
416
|
+
* });
|
|
417
|
+
*
|
|
418
|
+
* // Multiple event triggers with conditional logic
|
|
419
|
+
* const multiEventHook = defineHook({
|
|
420
|
+
* trigger: {
|
|
421
|
+
* PreToolUse: { Read: true, WebFetch: true },
|
|
422
|
+
* PostToolUse: { Read: true }
|
|
423
|
+
* },
|
|
424
|
+
* shouldRun: () => process.env.NODE_ENV === 'development',
|
|
425
|
+
* run: (context) => {
|
|
426
|
+
* // Handle different events and tools based on context.input
|
|
427
|
+
* return context.success();
|
|
428
|
+
* }
|
|
429
|
+
* });
|
|
430
|
+
* ```
|
|
431
|
+
*/
|
|
432
|
+
declare function defineHook<THookTrigger extends HookTrigger = HookTrigger>(definition: HookDefinition<THookTrigger>): HookDefinition<THookTrigger>;
|
|
433
|
+
type HookDefinition<THookTrigger extends HookTrigger = HookTrigger> = {
|
|
434
|
+
/**
|
|
435
|
+
* The event that triggers the hook.
|
|
436
|
+
*/
|
|
437
|
+
trigger: THookTrigger;
|
|
438
|
+
/**
|
|
439
|
+
* The function to run when the hook is triggered.
|
|
440
|
+
*
|
|
441
|
+
* @example
|
|
442
|
+
* ```ts
|
|
443
|
+
* // Example usage
|
|
444
|
+
* (context) => {
|
|
445
|
+
* const input = context.input;
|
|
446
|
+
*
|
|
447
|
+
* // Your hook logic here
|
|
448
|
+
*
|
|
449
|
+
* return context.success();
|
|
450
|
+
* }
|
|
451
|
+
*/
|
|
452
|
+
run: HookHandler<THookTrigger>;
|
|
453
|
+
/**
|
|
454
|
+
* Determines whether the hook should run.
|
|
455
|
+
*
|
|
456
|
+
* @default true
|
|
457
|
+
*
|
|
458
|
+
* @returns Whether the hook should run.
|
|
459
|
+
*/
|
|
460
|
+
shouldRun?: (() => Awaitable<boolean>) | boolean;
|
|
461
|
+
};
|
|
462
|
+
type HookHandler<THookTrigger extends HookTrigger> = (context: HookContext<THookTrigger>) => Awaitable<HookResponse<THookTrigger>>;
|
|
463
|
+
//#endregion
|
|
464
|
+
//#region src/run.d.ts
|
|
465
|
+
/**
|
|
466
|
+
* Executes a Claude Code hook with runtime input validation and error handling.
|
|
467
|
+
*
|
|
468
|
+
* This function handles the complete lifecycle of hook execution including:
|
|
469
|
+
* - Reading input from stdin
|
|
470
|
+
* - Validating input against Valibot schemas
|
|
471
|
+
* - Creating typed context
|
|
472
|
+
* - Executing the hook handler
|
|
473
|
+
* - Formatting and outputting results
|
|
474
|
+
*
|
|
475
|
+
* @param definition - The hook definition to execute
|
|
476
|
+
*
|
|
477
|
+
* @example
|
|
478
|
+
* ```ts
|
|
479
|
+
* // CLI usage: echo '{"hook_event_name":"SessionStart",...}' | node hook.js
|
|
480
|
+
* const hook = defineHook({
|
|
481
|
+
* trigger: { SessionStart: true },
|
|
482
|
+
* run: (context) => context.success()
|
|
483
|
+
* });
|
|
484
|
+
*
|
|
485
|
+
* // Execute the hook (typically called from CLI)
|
|
486
|
+
* await runHook(hook);
|
|
487
|
+
*
|
|
488
|
+
* // Hook with error handling
|
|
489
|
+
* const validationHook = defineHook({
|
|
490
|
+
* trigger: { PreToolUse: { Read: true } },
|
|
491
|
+
* run: (context) => {
|
|
492
|
+
* try {
|
|
493
|
+
* const { file_path } = context.input.tool_input;
|
|
494
|
+
*
|
|
495
|
+
* if (!file_path.endsWith('.ts')) {
|
|
496
|
+
* return context.nonBlockingError('Warning: Non-TypeScript file detected');
|
|
497
|
+
* }
|
|
498
|
+
*
|
|
499
|
+
* return context.success();
|
|
500
|
+
* } catch (error) {
|
|
501
|
+
* return context.blockingError(`Validation failed: ${error.message}`);
|
|
502
|
+
* }
|
|
503
|
+
* }
|
|
504
|
+
* });
|
|
505
|
+
*
|
|
506
|
+
* await runHook(validationHook);
|
|
507
|
+
* ```
|
|
508
|
+
*/
|
|
509
|
+
declare function runHook<THookTrigger extends HookTrigger = HookTrigger>(def: HookDefinition<THookTrigger>): Promise<void>;
|
|
510
|
+
//#endregion
|
|
511
|
+
//#region src/index.d.ts
|
|
512
|
+
/**
|
|
513
|
+
* Represents the input schema for each tool in the `PreToolUse` and `PostToolUse` hooks.
|
|
514
|
+
*
|
|
515
|
+
* This interface enables type-safe hook handling with tool-specific inputs and responses.
|
|
516
|
+
* Users can extend this interface with declaration merging to add custom tool definitions.
|
|
517
|
+
*
|
|
518
|
+
* @example
|
|
519
|
+
* ```ts
|
|
520
|
+
* // Extend with custom tools
|
|
521
|
+
* declare module "cc-hooks-ts" {
|
|
522
|
+
* interface ToolSchema {
|
|
523
|
+
* MyCustomTool: {
|
|
524
|
+
* input: {
|
|
525
|
+
* customParam: string;
|
|
526
|
+
* optionalParam?: number;
|
|
527
|
+
* };
|
|
528
|
+
* response: {
|
|
529
|
+
* result: string;
|
|
530
|
+
* };
|
|
531
|
+
* };
|
|
532
|
+
* }
|
|
533
|
+
* }
|
|
534
|
+
*
|
|
535
|
+
* // Use in hook definition
|
|
536
|
+
* const hook = defineHook({
|
|
537
|
+
* trigger: { PreToolUse: { MyCustomTool: true } },
|
|
538
|
+
* run: (context) => {
|
|
539
|
+
* // context.input.tool_input is now typed as { customParam: string; optionalParam?: number; }
|
|
540
|
+
* const { customParam, optionalParam } = context.input.tool_input;
|
|
541
|
+
* return context.success();
|
|
542
|
+
* }
|
|
543
|
+
* });
|
|
544
|
+
* ```
|
|
545
|
+
*/
|
|
546
|
+
interface ToolSchema {
|
|
547
|
+
Read: {
|
|
548
|
+
input: {
|
|
549
|
+
file_path: string;
|
|
550
|
+
};
|
|
551
|
+
response: unknown;
|
|
552
|
+
};
|
|
553
|
+
WebFetch: {
|
|
554
|
+
input: {
|
|
555
|
+
prompt: string;
|
|
556
|
+
url: string;
|
|
557
|
+
};
|
|
558
|
+
response: unknown;
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
//#endregion
|
|
562
|
+
export { type ExtractAllHookInputsForEvent, type ExtractSpecificHookInputForEvent, ToolSchema, defineHook, runHook };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import * as v from "valibot";
|
|
4
|
+
function defineHook(definition) {
|
|
5
|
+
return definition;
|
|
6
|
+
}
|
|
7
|
+
function createContext(input) {
|
|
8
|
+
return {
|
|
9
|
+
blockingError: (error) => ({
|
|
10
|
+
kind: "blocking-error",
|
|
11
|
+
payload: error
|
|
12
|
+
}),
|
|
13
|
+
input,
|
|
14
|
+
nonBlockingError: (message) => ({
|
|
15
|
+
kind: "non-blocking-error",
|
|
16
|
+
payload: message
|
|
17
|
+
}),
|
|
18
|
+
success: (result) => ({
|
|
19
|
+
kind: "success",
|
|
20
|
+
payload: {
|
|
21
|
+
additionalClaudeContext: result?.additionalClaudeContext,
|
|
22
|
+
messageForUser: result?.messageForUser
|
|
23
|
+
}
|
|
24
|
+
}),
|
|
25
|
+
json: (payload) => ({
|
|
26
|
+
kind: "json",
|
|
27
|
+
payload
|
|
28
|
+
})
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const SUPPORTED_HOOK_EVENTS = [
|
|
32
|
+
"PreToolUse",
|
|
33
|
+
"PostToolUse",
|
|
34
|
+
"Notification",
|
|
35
|
+
"UserPromptSubmit",
|
|
36
|
+
"Stop",
|
|
37
|
+
"SubagentStop",
|
|
38
|
+
"PreCompact",
|
|
39
|
+
"SessionStart",
|
|
40
|
+
"SessionEnd"
|
|
41
|
+
];
|
|
42
|
+
const baseHookInputSchema = v.object({
|
|
43
|
+
cwd: v.string(),
|
|
44
|
+
hook_event_name: v.union(SUPPORTED_HOOK_EVENTS.map((e) => v.literal(e))),
|
|
45
|
+
session_id: v.string(),
|
|
46
|
+
transcript_path: v.string()
|
|
47
|
+
});
|
|
48
|
+
function buildHookInputSchema(hook_event_name, entries) {
|
|
49
|
+
return v.object({
|
|
50
|
+
...baseHookInputSchema.entries,
|
|
51
|
+
hook_event_name: v.literal(hook_event_name),
|
|
52
|
+
...entries
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const HookInputSchemas = {
|
|
56
|
+
PreToolUse: buildHookInputSchema("PreToolUse", {
|
|
57
|
+
tool_input: v.unknown(),
|
|
58
|
+
tool_name: v.intersect([v.string(), v.record(v.string(), v.never())])
|
|
59
|
+
}),
|
|
60
|
+
PostToolUse: buildHookInputSchema("PostToolUse", {
|
|
61
|
+
tool_input: v.unknown(),
|
|
62
|
+
tool_name: v.string(),
|
|
63
|
+
tool_response: v.unknown()
|
|
64
|
+
}),
|
|
65
|
+
Notification: buildHookInputSchema("Notification", { message: v.optional(v.string()) }),
|
|
66
|
+
UserPromptSubmit: buildHookInputSchema("UserPromptSubmit", { prompt: v.string() }),
|
|
67
|
+
Stop: buildHookInputSchema("Stop", { stop_hook_active: v.optional(v.boolean()) }),
|
|
68
|
+
SubagentStop: buildHookInputSchema("SubagentStop", { stop_hook_active: v.optional(v.boolean()) }),
|
|
69
|
+
PreCompact: buildHookInputSchema("PreCompact", {
|
|
70
|
+
custom_instructions: v.string(),
|
|
71
|
+
trigger: v.union([v.literal("manual"), v.literal("auto")])
|
|
72
|
+
}),
|
|
73
|
+
SessionStart: buildHookInputSchema("SessionStart", { source: v.string() }),
|
|
74
|
+
SessionEnd: buildHookInputSchema("SessionEnd", { reason: v.string() })
|
|
75
|
+
};
|
|
76
|
+
function isNonEmptyString(value) {
|
|
77
|
+
return typeof value === "string" && value.length > 0;
|
|
78
|
+
}
|
|
79
|
+
async function runHook(def) {
|
|
80
|
+
const { run, shouldRun = true, trigger } = def;
|
|
81
|
+
let eventName = null;
|
|
82
|
+
try {
|
|
83
|
+
const proceed = typeof shouldRun === "function" ? await shouldRun() : shouldRun;
|
|
84
|
+
if (!proceed) return handleHookResult(eventName, {
|
|
85
|
+
kind: "success",
|
|
86
|
+
payload: {}
|
|
87
|
+
});
|
|
88
|
+
const inputSchema = extractInputSchemaFromTrigger(trigger);
|
|
89
|
+
const rawInput = readFileSync(process.stdin.fd, "utf-8");
|
|
90
|
+
const parsed = v.parse(inputSchema, JSON.parse(rawInput));
|
|
91
|
+
eventName = parsed.hook_event_name;
|
|
92
|
+
const context = createContext(parsed);
|
|
93
|
+
const result = await run(context);
|
|
94
|
+
handleHookResult(eventName, result);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
handleHookResult(eventName, {
|
|
97
|
+
kind: "non-blocking-error",
|
|
98
|
+
payload: `Error in hook: ${error instanceof Error ? error.message : String(error)}`
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function handleHookResult(eventName, hookResult) {
|
|
103
|
+
switch (hookResult.kind) {
|
|
104
|
+
case "success":
|
|
105
|
+
if (eventName === "UserPromptSubmit" || eventName === "SessionStart") {
|
|
106
|
+
if (isNonEmptyString(hookResult.payload.additionalClaudeContext)) {
|
|
107
|
+
console.log(hookResult.payload.additionalClaudeContext);
|
|
108
|
+
return process.exit(0);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (isNonEmptyString(hookResult.payload.messageForUser)) console.log(hookResult.payload.messageForUser);
|
|
112
|
+
return process.exit(0);
|
|
113
|
+
case "blocking-error":
|
|
114
|
+
if (hookResult.payload) console.error(hookResult.payload);
|
|
115
|
+
return process.exit(2);
|
|
116
|
+
case "non-blocking-error":
|
|
117
|
+
if (isNonEmptyString(hookResult.payload)) console.error(hookResult.payload);
|
|
118
|
+
return process.exit(1);
|
|
119
|
+
case "json":
|
|
120
|
+
console.log(JSON.stringify(hookResult.payload));
|
|
121
|
+
return process.exit(0);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function extractInputSchemaFromTrigger(trigger) {
|
|
125
|
+
const schemas = Object.keys(trigger).map((hookEvent) => {
|
|
126
|
+
return HookInputSchemas[hookEvent];
|
|
127
|
+
});
|
|
128
|
+
return v.union(schemas);
|
|
129
|
+
}
|
|
130
|
+
export { defineHook, runHook };
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cc-hooks-ts",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Write claude code hooks with type safety",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": {
|
|
9
|
+
"name": "sushichan044",
|
|
10
|
+
"url": "https://github.com/sushichan044"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/sushichan044/cc-hooks-ts.git"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public",
|
|
18
|
+
"registry": "https://registry.npmjs.org/",
|
|
19
|
+
"provenance": true
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/sushichan044/cc-hooks-ts/issues"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": "^20.12.0 || ^22.0.0 || >=24.0.0"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"claude",
|
|
29
|
+
"claude-code",
|
|
30
|
+
"hooks",
|
|
31
|
+
"typescript",
|
|
32
|
+
"type-safety",
|
|
33
|
+
"anthropic"
|
|
34
|
+
],
|
|
35
|
+
"files": [
|
|
36
|
+
"dist"
|
|
37
|
+
],
|
|
38
|
+
"exports": {
|
|
39
|
+
".": {
|
|
40
|
+
"types": "./dist/index.d.mts",
|
|
41
|
+
"import": "./dist/index.mjs"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@arethetypeswrong/core": "0.18.2",
|
|
46
|
+
"@biomejs/biome": "2.1.2",
|
|
47
|
+
"@types/node": "24.3.0",
|
|
48
|
+
"@virtual-live-lab/eslint-config": "2.2.24",
|
|
49
|
+
"@virtual-live-lab/tsconfig": "2.1.21",
|
|
50
|
+
"eslint": "9.31.0",
|
|
51
|
+
"eslint-plugin-import-access": "3.0.0",
|
|
52
|
+
"pkg-pr-new": "0.0.54",
|
|
53
|
+
"publint": "0.3.12",
|
|
54
|
+
"release-it": "19.0.4",
|
|
55
|
+
"release-it-pnpm": "4.6.6",
|
|
56
|
+
"tsdown": "0.14.1",
|
|
57
|
+
"typescript": "5.9.2",
|
|
58
|
+
"typescript-eslint": "8.39.0",
|
|
59
|
+
"unplugin-unused": "0.5.2",
|
|
60
|
+
"vitest": "3.2.4"
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"valibot": "^1.1.0"
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"lint": "eslint --max-warnings 0 --fix .",
|
|
67
|
+
"format": "biome format --write .",
|
|
68
|
+
"format:check": "biome format . --reporter=github",
|
|
69
|
+
"typecheck": "tsc --noEmit",
|
|
70
|
+
"test": "vitest --run",
|
|
71
|
+
"build": "tsdown",
|
|
72
|
+
"pkg-pr-new": "pkg-pr-new publish --compact --comment=update --pnpm"
|
|
73
|
+
}
|
|
74
|
+
}
|