@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 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.