@goodfoot/claude-code-hooks 1.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 +317 -0
- package/dist/cli.js +914 -0
- package/dist/constants.js +21 -0
- package/dist/env.js +188 -0
- package/dist/hooks.js +391 -0
- package/dist/index.js +77 -0
- package/dist/inputs.js +35 -0
- package/dist/logger.js +494 -0
- package/dist/outputs.js +282 -0
- package/dist/runtime.js +222 -0
- package/dist/scaffold.js +466 -0
- package/dist/tool-helpers.js +366 -0
- package/dist/tool-inputs.js +21 -0
- package/package.json +68 -0
- package/types/cli.d.ts +281 -0
- package/types/constants.d.ts +9 -0
- package/types/env.d.ts +150 -0
- package/types/hooks.d.ts +851 -0
- package/types/index.d.ts +137 -0
- package/types/inputs.d.ts +601 -0
- package/types/logger.d.ts +471 -0
- package/types/outputs.d.ts +643 -0
- package/types/runtime.d.ts +75 -0
- package/types/scaffold.d.ts +46 -0
- package/types/tool-helpers.d.ts +336 -0
- package/types/tool-inputs.d.ts +228 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Goodfoot Media, LLC
|
|
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,317 @@
|
|
|
1
|
+
# @goodfoot/claude-code-hooks
|
|
2
|
+
|
|
3
|
+
> **Build Claude Code hooks in TypeScript.**
|
|
4
|
+
|
|
5
|
+
This package is not just a library; it is a **build system** and a **runtime wrapper**. You write TypeScript, this package compiles it into self-contained executables, and _those_ are what Claude runs.
|
|
6
|
+
|
|
7
|
+
## Skills
|
|
8
|
+
|
|
9
|
+
Load the "claude-code-hooks:sdk" skill to enable Claude to use this package.
|
|
10
|
+
|
|
11
|
+
Run:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
claude plugin marketplace add goodfoot-io/marketplace && claude plugin install claude-code-hooks@goodfoot"
|
|
15
|
+
|
|
16
|
+
claude -p "Load the 'claude-code-hooks:sdk' skill then scaffold a new hook package in ./packages/hooks that outputs to '.claude/hooks/hooks.json' and contains an example SessionStart hook."
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## ⚡ Quick Start
|
|
20
|
+
|
|
21
|
+
### 1. Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
yarn add @goodfoot/claude-code-hooks
|
|
25
|
+
# or npm install, pnpm, etc.
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 2. Write a Hook
|
|
29
|
+
|
|
30
|
+
Create `hooks/allow-ls.ts`. **Note:** You _must_ use `export default` and the factory function.
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { preToolUseHook, preToolUseOutput } from '@goodfoot/claude-code-hooks';
|
|
34
|
+
|
|
35
|
+
export default preToolUseHook({ matcher: 'Bash' }, async (input, { logger }) => {
|
|
36
|
+
const { command } = input.tool_input as { command: string };
|
|
37
|
+
|
|
38
|
+
// Use logger, NEVER console.log
|
|
39
|
+
logger.info('Checking command', { command });
|
|
40
|
+
|
|
41
|
+
if (command.trim() === 'ls') {
|
|
42
|
+
return preToolUseOutput({
|
|
43
|
+
systemMessage: 'Auto-approved: ls command is safe.',
|
|
44
|
+
hookSpecificOutput: { permissionDecision: 'allow' }
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return preToolUseOutput({
|
|
49
|
+
systemMessage: 'Command passed through for review.'
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 3. Compile
|
|
55
|
+
|
|
56
|
+
The CLI compiles your TS into `.mjs` and generates the `hooks.json` manifest.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# -i: Input glob
|
|
60
|
+
# -o: Output manifest path
|
|
61
|
+
npx -y @goodfoot/claude-code-hooks -i "hooks/*.ts" -o "dist/hooks.json"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 4. Configure Claude
|
|
65
|
+
|
|
66
|
+
Tell Claude where your hooks are. The location depends on your setup:
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Scaffolding a New Project
|
|
71
|
+
|
|
72
|
+
Bootstrap a complete hook project with TypeScript, testing, and build configuration:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npx @goodfoot/claude-code-hooks --scaffold /path/to/my-hooks --hooks Stop,SubagentStop -o ./hooks.json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
This creates a ready-to-use project structure:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
my-hooks/
|
|
82
|
+
├── src/
|
|
83
|
+
│ ├── stop.ts # Hook implementation
|
|
84
|
+
│ └── subagent-stop.ts # Hook implementation
|
|
85
|
+
├── test/
|
|
86
|
+
│ ├── stop.test.ts # Vitest tests
|
|
87
|
+
│ └── subagent-stop.test.ts
|
|
88
|
+
├── package.json # Dependencies + build script
|
|
89
|
+
├── tsconfig.json # TypeScript config
|
|
90
|
+
├── vitest.config.ts # Test config
|
|
91
|
+
├── biome.json # Linting config
|
|
92
|
+
└── CLAUDE.md # Skill loading instruction
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Next steps:**
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
cd my-hooks
|
|
99
|
+
npm install
|
|
100
|
+
npm run build # Outputs hooks.json to specified -o path
|
|
101
|
+
npm test # Run tests
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Available Hook Types
|
|
105
|
+
|
|
106
|
+
The `--hooks` argument accepts a comma-separated list of any of these 12 event types:
|
|
107
|
+
|
|
108
|
+
| Hook Type | Description |
|
|
109
|
+
| -------------------- | ------------------------------------------ |
|
|
110
|
+
| `PreToolUse` | Before a tool executes (allow/deny/modify) |
|
|
111
|
+
| `PostToolUse` | After a tool completes successfully |
|
|
112
|
+
| `PostToolUseFailure` | After a tool fails |
|
|
113
|
+
| `Notification` | When Claude requests permissions |
|
|
114
|
+
| `UserPromptSubmit` | When user submits a prompt |
|
|
115
|
+
| `SessionStart` | When session begins |
|
|
116
|
+
| `SessionEnd` | When session terminates |
|
|
117
|
+
| `Stop` | After main agent finishes |
|
|
118
|
+
| `SubagentStart` | When a Task tool starts |
|
|
119
|
+
| `SubagentStop` | When a Task tool completes |
|
|
120
|
+
| `PreCompact` | Before context compaction |
|
|
121
|
+
| `PermissionRequest` | When permission is requested |
|
|
122
|
+
|
|
123
|
+
Hook names are case-insensitive: `stop`, `Stop`, and `STOP` all work.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
**Standalone Project:**
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# In ~/.claude/config.json or project-local .claude/config.json
|
|
131
|
+
{
|
|
132
|
+
"hooks": "/absolute/path/to/your/project/dist/hooks.json"
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Claude Code Plugin:**
|
|
137
|
+
Plugins automatically load `hooks.json` from the plugin root. Place your output there:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
npx -y @goodfoot/claude-code-hooks -i "hooks/*.ts" -o "./hooks.json"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The `CLAUDE_PLUGIN_ROOT` variable is set automatically, so paths resolve correctly.
|
|
144
|
+
|
|
145
|
+
**User-level Hooks:**
|
|
146
|
+
For hooks that apply to all sessions, build to `~/.claude/hooks/`:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
npx -y @goodfoot/claude-code-hooks -i "hooks/*.ts" -o ~/.claude/hooks/hooks.json
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 💀 The "Third Rail" (Critical Safety Rules)
|
|
155
|
+
|
|
156
|
+
Violating these rules will cause your hooks to fail silently or block Claude entirely.
|
|
157
|
+
|
|
158
|
+
1. **NO `console.log`**: The hook communicates with Claude via `stdout`. If you print "Hello world", you corrupt the JSON protocol.
|
|
159
|
+
- **Bad:** `console.log("Checking command")`
|
|
160
|
+
- **Good:** `context.logger.info("Checking command")`
|
|
161
|
+
2. **Relative Paths via Environment Variable**: The generated `hooks.json` uses `node $CLAUDE_PLUGIN_ROOT/hooks/build/` paths.
|
|
162
|
+
- Compiled hooks are placed in a `build/` subdirectory relative to `hooks.json`.
|
|
163
|
+
- The `$CLAUDE_PLUGIN_ROOT` environment variable is set by Claude Code at runtime.
|
|
164
|
+
- Use the `--executable` CLI option to specify a custom executable (e.g., `bun`, `/usr/local/bin/node22`).
|
|
165
|
+
3. **`export default` is Mandatory**: The CLI uses static analysis to find your hooks. It looks specifically for `export default factory(...)`.
|
|
166
|
+
- **Ignored:** `export const myHook = ...`
|
|
167
|
+
- **Ignored:** `module.exports = ...`
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## 🧰 The Toolbox
|
|
172
|
+
|
|
173
|
+
### Type-Safe Inputs
|
|
174
|
+
|
|
175
|
+
Input properties use the wire format (snake_case) directly for consistency.
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
// Claude sends: { "file_path": "src/main.ts", "tool_name": "Read" }
|
|
179
|
+
// You receive:
|
|
180
|
+
export default preToolUseHook({}, async (input) => {
|
|
181
|
+
console.log(input.tool_name); // "Read"
|
|
182
|
+
console.log(input.tool_input.file_path); // "src/main.ts" (when typed)
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Output Builders
|
|
187
|
+
|
|
188
|
+
Don't construct raw JSON. Use the builders to ensure wire-format compatibility.
|
|
189
|
+
|
|
190
|
+
| Builder | Use Case |
|
|
191
|
+
| :----------------------- | :----------------------------------------------------------------- |
|
|
192
|
+
| `preToolUseOutput` | Allow/Deny permissions, modify inputs. |
|
|
193
|
+
| `postToolUseOutput` | Inject context after a tool runs (e.g., "File read successfully"). |
|
|
194
|
+
| `stopOutput` | Block Claude from quitting (`decision: 'block'`). |
|
|
195
|
+
| `userPromptSubmitOutput` | Inject context when the user types a message. |
|
|
196
|
+
|
|
197
|
+
### The Logger
|
|
198
|
+
|
|
199
|
+
The Logger is **silent by default** — no output to stdout, stderr, or files unless explicitly configured. This design ensures hooks never corrupt the JSON protocol.
|
|
200
|
+
|
|
201
|
+
**Enable file logging:**
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
# Option A: Environment Variable
|
|
205
|
+
export CLAUDE_CODE_HOOKS_LOG_FILE=/tmp/claude-hooks.log
|
|
206
|
+
|
|
207
|
+
# Option B: CLI Argument (during build)
|
|
208
|
+
npx -y @goodfoot/claude-code-hooks ... --log /tmp/claude-hooks.log
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**View logs:**
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
tail -f /tmp/claude-hooks.log | jq
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Programmatic usage:**
|
|
218
|
+
|
|
219
|
+
The `Logger` class can be instantiated directly for testing or advanced use cases:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
import { Logger } from '@goodfoot/claude-code-hooks';
|
|
223
|
+
|
|
224
|
+
// Silent by default — perfect for unit tests
|
|
225
|
+
const logger = new Logger();
|
|
226
|
+
|
|
227
|
+
// With file output
|
|
228
|
+
const fileLogger = new Logger({ logFilePath: '/tmp/my-hooks.log' });
|
|
229
|
+
|
|
230
|
+
// Subscribe to events programmatically
|
|
231
|
+
const unsubscribe = logger.on('error', (event) => {
|
|
232
|
+
sendToMonitoring(event);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Clean up when done
|
|
236
|
+
unsubscribe();
|
|
237
|
+
fileLogger.close();
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
See the skill documentation for event subscription, log levels, and debugging tips.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 📁 Recommended Plugin Structure
|
|
245
|
+
|
|
246
|
+
For Claude Code plugins, use this directory layout:
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
plugins/my-plugin/
|
|
250
|
+
├── hooks/
|
|
251
|
+
│ └── src/
|
|
252
|
+
│ ├── block-dangerous.ts
|
|
253
|
+
│ └── inject-context.ts
|
|
254
|
+
├── hooks.json # Build output (auto-loaded by plugin)
|
|
255
|
+
└── build/
|
|
256
|
+
├── block-dangerous.abc123.mjs
|
|
257
|
+
└── inject-context.def456.mjs
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Build command:
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
npx -y @goodfoot/claude-code-hooks -i "hooks/src/*.ts" -o "./hooks.json"
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## 🤝 Coexistence with Other Hooks
|
|
269
|
+
|
|
270
|
+
The build tool is designed to **play well with others**:
|
|
271
|
+
|
|
272
|
+
- **External hooks are preserved**: Hooks not in `__generated.files` are never touched
|
|
273
|
+
- **Atomic writes**: Uses temp-file-then-rename for safe updates
|
|
274
|
+
- **Clean rebuilds**: Only removes files it previously generated
|
|
275
|
+
|
|
276
|
+
You can safely:
|
|
277
|
+
|
|
278
|
+
- Mix TypeScript hooks with shell script hooks in the same `hooks.json`
|
|
279
|
+
- Let multiple tools contribute to the same manifest
|
|
280
|
+
- Manually add hooks without worrying about them being overwritten
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## 🔍 Debugging Guide
|
|
285
|
+
|
|
286
|
+
**"My hook isn't running!"**
|
|
287
|
+
|
|
288
|
+
1. Did you run the build command? (`npx -y @goodfoot/claude-code-hooks ...`)
|
|
289
|
+
2. Did you `export default` the hook?
|
|
290
|
+
3. Is the path in `hooks.json` correct for _this_ machine?
|
|
291
|
+
4. Is the timeout too short? (Units are **milliseconds**, `timeout: 5000` = 5s).
|
|
292
|
+
|
|
293
|
+
**"Claude shows an error when my hook runs."**
|
|
294
|
+
|
|
295
|
+
1. Did you `console.log`? (Check your code).
|
|
296
|
+
2. Did your hook throw an error? (Uncaught errors exit with code 2, which blocks Claude).
|
|
297
|
+
3. Check the log file defined in `CLAUDE_CODE_HOOKS_LOG_FILE`.
|
|
298
|
+
|
|
299
|
+
**"I can't see the tool input."**
|
|
300
|
+
|
|
301
|
+
1. Use the logger to dump it: `logger.info('Input', { input })`.
|
|
302
|
+
2. Remember `input.tool_input` is `unknown`. Cast it safely, or use typed matchers.
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## 🏗️ Architecture
|
|
307
|
+
|
|
308
|
+
1. **CLI (`claude-code-hooks`)**: Scans your TS files, extracts metadata (events, matchers) via AST, and compiles them using `esbuild`.
|
|
309
|
+
2. **Runtime (`runtime.ts`)**: The compiled files import a runtime wrapper. This wrapper:
|
|
310
|
+
- Reads `stdin`.
|
|
311
|
+
- Parses JSON (wire format with snake_case properties).
|
|
312
|
+
- Injects context (`logger`, and `persistEnvVar`/`persistEnvVars` for SessionStart hooks).
|
|
313
|
+
- Executes your handler.
|
|
314
|
+
- Formats the output.
|
|
315
|
+
- Writes to `stdout`.
|
|
316
|
+
|
|
317
|
+
This separation ensures your hooks are fast, type-safe, and isolated.
|