@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/types/cli.d.ts ADDED
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI tool for compiling Claude Code hooks using esbuild.
4
+ *
5
+ * Compiles TypeScript hooks to standalone ESM modules and generates hooks.json
6
+ * with correct command paths and matcher configurations.
7
+ * @example
8
+ * ```bash
9
+ * # Compile hooks and generate hooks.json
10
+ * claude-code-hooks -i "hooks/**\/*.ts" -o "./dist/hooks.json"
11
+ *
12
+ * # With runtime logging (equivalent to CLAUDE_CODE_HOOKS_LOG_FILE)
13
+ * claude-code-hooks -i "hooks/**\/*.ts" -o "./dist/hooks.json" --log /tmp/hooks.log
14
+ * ```
15
+ * @module
16
+ */
17
+ import type { HookEventName } from './inputs.js';
18
+ import { HOOK_FACTORY_TO_EVENT } from './constants.js';
19
+ /**
20
+ * Hook context determines how paths are resolved in hooks.json.
21
+ *
22
+ * - `plugin`: Uses `$CLAUDE_PLUGIN_ROOT` for plugin hooks
23
+ * - `agent`: Uses `"$CLAUDE_PROJECT_DIR"` for agent hooks (.claude/hooks/)
24
+ */
25
+ type HookContext = 'plugin' | 'agent';
26
+ /**
27
+ * Command-line arguments parsed from process.argv.
28
+ */
29
+ interface CliArgs {
30
+ /** Glob pattern for hook source files. */
31
+ input: string;
32
+ /** Path for hooks.json output file. */
33
+ output: string;
34
+ /** Optional log file path. */
35
+ log?: string;
36
+ /** Show help. */
37
+ help: boolean;
38
+ /** Show version. */
39
+ version: boolean;
40
+ /** Directory path for scaffolding a new hook project. */
41
+ scaffold?: string;
42
+ /** Comma-separated list of hook types to generate when scaffolding. */
43
+ hooks?: string;
44
+ /** Node executable path to use in command output (default: "node"). */
45
+ executable?: string;
46
+ }
47
+ /**
48
+ * Metadata extracted from a hook file via TypeScript AST analysis.
49
+ */
50
+ interface HookMetadata {
51
+ /** The hook event type (PreToolUse, SessionStart, etc.). */
52
+ hookEventName: HookEventName;
53
+ /** Optional matcher pattern from hook config. */
54
+ matcher?: string;
55
+ /** Optional timeout in milliseconds from hook config. */
56
+ timeout?: number;
57
+ }
58
+ /**
59
+ * A compiled hook with its metadata and output path.
60
+ */
61
+ interface CompiledHook {
62
+ /** Original source file path. */
63
+ sourcePath: string;
64
+ /** Compiled output file path. */
65
+ outputPath: string;
66
+ /** Output filename (e.g., "my-hook.abc123de.mjs"). */
67
+ outputFilename: string;
68
+ /** Extracted hook metadata. */
69
+ metadata: HookMetadata;
70
+ }
71
+ /**
72
+ * Individual hook configuration within a matcher group.
73
+ */
74
+ interface HookConfig {
75
+ /** Hook type - always "command" for compiled hooks. */
76
+ type: 'command';
77
+ /** Absolute path to compiled hook executable. */
78
+ command: string;
79
+ /** Optional timeout in seconds. */
80
+ timeout?: number;
81
+ }
82
+ /**
83
+ * Matcher group entry within an event type.
84
+ */
85
+ interface MatcherEntry {
86
+ /** Matcher pattern (tool name, regex, etc.). Optional for some event types. */
87
+ matcher?: string;
88
+ /** Array of hook configurations in this matcher group. */
89
+ hooks: HookConfig[];
90
+ }
91
+ /**
92
+ * The complete hooks.json structure expected by Claude Code.
93
+ *
94
+ * Format: { hooks: { EventType: [ { matcher?, hooks: [...] } ] } }
95
+ */
96
+ interface HooksJson {
97
+ /** Object keyed by event type (PreToolUse, SessionStart, etc.). */
98
+ hooks: Partial<Record<HookEventName, MatcherEntry[]>>;
99
+ /** Generated file tracking metadata. */
100
+ __generated: {
101
+ /** Array of generated filenames. */
102
+ files: string[];
103
+ /** ISO timestamp of generation. */
104
+ timestamp: string;
105
+ };
106
+ }
107
+ /**
108
+ * Parses command-line arguments.
109
+ * @param argv - Process argv (usually process.argv.slice(2))
110
+ * @returns Parsed arguments
111
+ */
112
+ declare function parseArgs(argv: string[]): CliArgs;
113
+ /**
114
+ * Validates CLI arguments and returns error message if invalid.
115
+ * @param args - Parsed CLI arguments
116
+ * @returns Error message if invalid, undefined if valid
117
+ */
118
+ declare function validateArgs(args: CliArgs): string | undefined;
119
+ /**
120
+ * Extracts hook metadata from a TypeScript source file using AST analysis.
121
+ *
122
+ * Looks for default exports that call hook factory functions (preToolUseHook, etc.)
123
+ * and extracts the hook type, matcher, and timeout from the config object.
124
+ * @param sourcePath - Absolute path to the TypeScript source file
125
+ * @returns Extracted hook metadata or undefined if not a valid hook file
126
+ * @example
127
+ * ```typescript
128
+ * // For a file containing:
129
+ * // export default preToolUseHook({ matcher: 'Bash', timeout: 5000 }, handler);
130
+ *
131
+ * const metadata = analyzeHookFile('/path/to/hook.ts');
132
+ * // { hookEventName: 'PreToolUse', matcher: 'Bash', timeout: 5000 }
133
+ * ```
134
+ */
135
+ declare function analyzeHookFile(sourcePath: string): HookMetadata | undefined;
136
+ /**
137
+ * Discovers hook files matching the glob pattern.
138
+ * @param pattern - Glob pattern for hook files
139
+ * @param cwd - Current working directory for relative patterns
140
+ * @returns Array of absolute paths to hook files
141
+ */
142
+ declare function discoverHookFiles(pattern: string, cwd: string): Promise<string[]>;
143
+ /**
144
+ * Options for compiling a hook.
145
+ */
146
+ interface CompileHookOptions {
147
+ /** Absolute path to source file. */
148
+ sourcePath: string;
149
+ /** Directory for compiled output. */
150
+ outputDir: string;
151
+ /** Optional log file path to inject into compiled hook. */
152
+ logFilePath?: string;
153
+ }
154
+ /**
155
+ * Compiles a TypeScript hook file to a self-contained ESM executable.
156
+ *
157
+ * Creates a wrapper that imports the hook and calls execute(), then bundles
158
+ * everything together including the runtime.
159
+ * @param options - Compilation options
160
+ * @returns Compiled output content as a string
161
+ */
162
+ declare function compileHook(options: CompileHookOptions): Promise<string>;
163
+ /**
164
+ * Generates a content hash (SHA-256, 8-char prefix) for a compiled hook.
165
+ * @param content - Compiled hook content
166
+ * @returns 8-character hex hash
167
+ */
168
+ declare function generateContentHash(content: string): string;
169
+ /**
170
+ * Groups compiled hooks by event type, then by matcher pattern.
171
+ * @param compiledHooks - Array of compiled hooks
172
+ * @returns Nested map: EventType -> Matcher -> Hooks
173
+ */
174
+ declare function groupHooksByEventAndMatcher(
175
+ compiledHooks: CompiledHook[]
176
+ ): Map<HookEventName, Map<string | undefined, CompiledHook[]>>;
177
+ /**
178
+ * Result of detecting the hook context, including the root directory.
179
+ */
180
+ interface HookContextInfo {
181
+ /** Hook context type. */
182
+ context: HookContext;
183
+ /** Absolute path to the root directory (plugin root or project root). */
184
+ rootDir: string;
185
+ }
186
+ /**
187
+ * Auto-detects the hook context and root directory based on directory structure.
188
+ *
189
+ * Detection logic:
190
+ * - If output path contains `.claude/` directory segment → agent context, root is parent of .claude/
191
+ * - If `.claude-plugin/` directory exists within 3 levels up → plugin context, root is that directory
192
+ * - Default: plugin context with hooks.json parent directory as root
193
+ * @param outputPath - Absolute path to the hooks.json output file
194
+ * @returns Detected hook context and root directory
195
+ */
196
+ declare function detectHookContext(outputPath: string): HookContextInfo;
197
+ /**
198
+ * Generates a command path based on the hook context.
199
+ *
200
+ * Calculates the relative path from the root directory to the build directory.
201
+ * Prepends the node executable.
202
+ *
203
+ * - `plugin`: Uses `node $CLAUDE_PLUGIN_ROOT/hooks/build/filename`
204
+ * - `agent`: Uses `node "$CLAUDE_PROJECT_DIR"/.claude/hooks/build/filename`
205
+ * @param filename - The compiled hook filename
206
+ * @param buildDir - Absolute path to the build directory
207
+ * @param contextInfo - Hook context info including root directory
208
+ * @param executable - Node executable path (default: "node")
209
+ * @returns The command path string
210
+ */
211
+ declare function generateCommandPath(
212
+ filename: string,
213
+ buildDir: string,
214
+ contextInfo: HookContextInfo,
215
+ executable?: string
216
+ ): string;
217
+ /**
218
+ * Generates the hooks.json content in Claude Code's expected format.
219
+ *
220
+ * Format: { hooks: { EventType: [ { matcher?, hooks: [...] } ] } }
221
+ * @param compiledHooks - Array of compiled hooks
222
+ * @param buildDir - Absolute path to the build directory
223
+ * @param contextInfo - Hook context info for path resolution
224
+ * @param executable - Node executable path (default: "node")
225
+ * @returns The hooks.json structure
226
+ */
227
+ declare function generateHooksJson(
228
+ compiledHooks: CompiledHook[],
229
+ buildDir: string,
230
+ contextInfo: HookContextInfo,
231
+ executable?: string
232
+ ): HooksJson;
233
+ /**
234
+ * Reads an existing hooks.json file if it exists.
235
+ * @param outputPath - Path to the hooks.json file
236
+ * @returns Parsed HooksJson or undefined if file doesn't exist
237
+ */
238
+ declare function readExistingHooksJson(outputPath: string): HooksJson | undefined;
239
+ /**
240
+ * Removes previously generated hook files from disk.
241
+ * Only removes files that were tracked in __generated.files.
242
+ * @param existingHooksJson - The existing hooks.json content
243
+ * @param outputDir - Directory containing the generated files
244
+ */
245
+ declare function removeOldGeneratedFiles(existingHooksJson: HooksJson, outputDir: string): void;
246
+ /**
247
+ * Extracts hooks from an existing hooks.json that were NOT generated by this package.
248
+ * Identifies generated hooks by checking if their command path matches the generated file pattern.
249
+ * @param existingHooksJson - The existing hooks.json content
250
+ * @returns Object containing preserved hooks (keyed by event type)
251
+ */
252
+ declare function extractPreservedHooks(existingHooksJson: HooksJson): Partial<Record<HookEventName, MatcherEntry[]>>;
253
+ /**
254
+ * Merges preserved hooks with newly generated hooks.
255
+ * Preserved hooks are added first, then new hooks are appended.
256
+ * @param newHooksJson - The newly generated hooks.json content
257
+ * @param preservedHooks - Hooks to preserve from the existing hooks.json
258
+ * @returns Merged HooksJson
259
+ */
260
+ declare function mergeHooksJson(
261
+ newHooksJson: HooksJson,
262
+ preservedHooks: Partial<Record<HookEventName, MatcherEntry[]>>
263
+ ): HooksJson;
264
+ export {
265
+ parseArgs,
266
+ validateArgs,
267
+ analyzeHookFile,
268
+ discoverHookFiles,
269
+ compileHook,
270
+ generateContentHash,
271
+ detectHookContext,
272
+ generateCommandPath,
273
+ generateHooksJson,
274
+ groupHooksByEventAndMatcher,
275
+ readExistingHooksJson,
276
+ removeOldGeneratedFiles,
277
+ extractPreservedHooks,
278
+ mergeHooksJson,
279
+ HOOK_FACTORY_TO_EVENT
280
+ };
281
+ export type { CliArgs, HookMetadata, CompiledHook, HookConfig, MatcherEntry, HooksJson };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Shared constants for the CLI and scaffold modules.
3
+ * @module
4
+ */
5
+ import type { HookEventName } from './inputs.js';
6
+ /**
7
+ * Maps hook factory function names to their event names.
8
+ */
9
+ export declare const HOOK_FACTORY_TO_EVENT: Record<string, HookEventName>;
package/types/env.d.ts ADDED
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Environment variable utilities for Claude Code hooks.
3
+ *
4
+ * Provides typed access to Claude Code's environment variables and utilities
5
+ * for persisting environment variables in SessionStart hooks.
6
+ *
7
+ * ## Environment Variables
8
+ *
9
+ * Claude Code sets these environment variables when running hooks:
10
+ *
11
+ * | Variable | Description | Available In |
12
+ * |----------|-------------|--------------|
13
+ * | `CLAUDE_PROJECT_DIR` | Absolute path to project root | All hooks |
14
+ * | `CLAUDE_ENV_FILE` | Path to file for persisting env vars | SessionStart only |
15
+ * | `CLAUDE_CODE_REMOTE` | `"true"` if running remotely | All hooks |
16
+ * @module
17
+ * @example
18
+ * ```typescript
19
+ * import { getProjectDir, persistEnvVar, isRemoteEnvironment } from '@goodfoot/claude-code-hooks';
20
+ *
21
+ * // Get project directory
22
+ * const projectDir = getProjectDir();
23
+ *
24
+ * // Check if running remotely
25
+ * if (isRemoteEnvironment()) {
26
+ * // Handle remote-specific logic
27
+ * }
28
+ *
29
+ * // In SessionStart hook: persist environment variables
30
+ * persistEnvVar('NODE_ENV', 'production');
31
+ * persistEnvVar('API_KEY', 'secret-key');
32
+ * ```
33
+ * @see https://code.claude.com/docs/en/hooks#hook-execution-details
34
+ */
35
+ /**
36
+ * Claude Code environment variable names.
37
+ *
38
+ * These are the environment variables that Claude Code sets when running hooks.
39
+ */
40
+ export declare const CLAUDE_ENV_VARS: {
41
+ /**
42
+ * Absolute path to the project root directory where Claude Code was started.
43
+ * Available in all hooks.
44
+ */
45
+ readonly PROJECT_DIR: 'CLAUDE_PROJECT_DIR';
46
+ /**
47
+ * Path to a file where SessionStart hooks can persist environment variables.
48
+ * Variables written to this file will be available in all subsequent bash commands.
49
+ * Only available in SessionStart hooks.
50
+ */
51
+ readonly ENV_FILE: 'CLAUDE_ENV_FILE';
52
+ /**
53
+ * Set to "true" when running in a remote (web) environment.
54
+ * Not set or empty when running in local CLI environment.
55
+ */
56
+ readonly REMOTE: 'CLAUDE_CODE_REMOTE';
57
+ };
58
+ /**
59
+ * Gets the Claude Code project directory.
60
+ *
61
+ * This is the absolute path to the project root where Claude Code was started.
62
+ * The value comes from the `CLAUDE_PROJECT_DIR` environment variable.
63
+ * @returns The project directory path, or undefined if not set
64
+ * @example
65
+ * ```typescript
66
+ * const projectDir = getProjectDir();
67
+ * if (projectDir) {
68
+ * const configPath = `${projectDir}/.claude/config.json`;
69
+ * }
70
+ * ```
71
+ */
72
+ export declare function getProjectDir(): string | undefined;
73
+ /**
74
+ * Gets the Claude Code env file path for persisting environment variables.
75
+ *
76
+ * This is only available in SessionStart hooks. The path points to a file
77
+ * where you can write shell export statements to persist environment variables
78
+ * for all subsequent bash commands in the session.
79
+ * @returns The env file path, or undefined if not set (not a SessionStart hook)
80
+ * @example
81
+ * ```typescript
82
+ * const envFile = getEnvFilePath();
83
+ * if (envFile) {
84
+ * // We're in a SessionStart hook and can persist env vars
85
+ * persistEnvVar('MY_VAR', 'my-value');
86
+ * }
87
+ * ```
88
+ */
89
+ export declare function getEnvFilePath(): string | undefined;
90
+ /**
91
+ * Checks if the hook is running in a remote (web) environment.
92
+ *
93
+ * Remote environments may have different capabilities or restrictions
94
+ * compared to local CLI environments.
95
+ * @returns true if running remotely, false if running locally
96
+ * @example
97
+ * ```typescript
98
+ * if (isRemoteEnvironment()) {
99
+ * // Use web-compatible approaches
100
+ * } else {
101
+ * // Can use local CLI features
102
+ * }
103
+ * ```
104
+ */
105
+ export declare function isRemoteEnvironment(): boolean;
106
+ /**
107
+ * Persists an environment variable for use in subsequent bash commands.
108
+ *
109
+ * This function writes a shell export statement to the `CLAUDE_ENV_FILE`,
110
+ * which Claude Code sources before running bash commands. This allows
111
+ * SessionStart hooks to configure the environment for the entire session.
112
+ *
113
+ * **Important**: This function only works in SessionStart hooks where
114
+ * `CLAUDE_ENV_FILE` is set. In other hooks, it will throw an error.
115
+ * @param name - The environment variable name
116
+ * @param value - The environment variable value (will be shell-escaped)
117
+ * @throws Error if CLAUDE_ENV_FILE is not set (not in a SessionStart hook)
118
+ * @example
119
+ * ```typescript
120
+ * import { sessionStartHook, sessionStartOutput, persistEnvVar } from '@goodfoot/claude-code-hooks';
121
+ *
122
+ * export default sessionStartHook({}, async (input) => {
123
+ * // Persist environment variables for the session
124
+ * persistEnvVar('NODE_ENV', 'production');
125
+ * persistEnvVar('API_KEY', process.env.MY_API_KEY ?? 'default');
126
+ * persistEnvVar('PATH', `${process.env.PATH}:./node_modules/.bin`);
127
+ *
128
+ * return sessionStartOutput({});
129
+ * });
130
+ * ```
131
+ * @see https://code.claude.com/docs/en/hooks#persisting-environment-variables
132
+ */
133
+ export declare function persistEnvVar(name: string, value: string): void;
134
+ /**
135
+ * Persists multiple environment variables at once.
136
+ *
137
+ * This is a convenience wrapper around `persistEnvVar` for setting
138
+ * multiple variables in a single call.
139
+ * @param vars - Object mapping variable names to values
140
+ * @throws Error if CLAUDE_ENV_FILE is not set (not in a SessionStart hook)
141
+ * @example
142
+ * ```typescript
143
+ * persistEnvVars({
144
+ * NODE_ENV: 'production',
145
+ * API_KEY: 'secret',
146
+ * DEBUG: 'false'
147
+ * });
148
+ * ```
149
+ */
150
+ export declare function persistEnvVars(vars: Record<string, string>): void;