@goodfoot/agent-hooks 1.0.0

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.
Files changed (68) hide show
  1. package/README.md +137 -0
  2. package/dist/agents/antigravity/events.js +42 -0
  3. package/dist/agents/antigravity/hooks.js +59 -0
  4. package/dist/agents/antigravity/index.js +28 -0
  5. package/dist/agents/antigravity/outputs.js +81 -0
  6. package/dist/agents/antigravity/transport.js +84 -0
  7. package/dist/agents/antigravity/types.js +11 -0
  8. package/dist/agents/claude-code/env.js +27 -0
  9. package/dist/agents/claude-code/events.js +87 -0
  10. package/dist/agents/claude-code/hooks.js +1000 -0
  11. package/dist/agents/claude-code/index.js +57 -0
  12. package/dist/agents/claude-code/outputs.js +579 -0
  13. package/dist/agents/claude-code/tool-helpers.js +840 -0
  14. package/dist/agents/claude-code/transport.js +254 -0
  15. package/dist/agents/claude-code/types.js +11 -0
  16. package/dist/agents/codex/cli-support.js +318 -0
  17. package/dist/agents/codex/constants.js +29 -0
  18. package/dist/agents/codex/events.js +68 -0
  19. package/dist/agents/codex/hooks.js +93 -0
  20. package/dist/agents/codex/index.js +21 -0
  21. package/dist/agents/codex/outputs.js +189 -0
  22. package/dist/agents/codex/scaffold.js +273 -0
  23. package/dist/agents/codex/transport.js +139 -0
  24. package/dist/agents/codex/types.js +1 -0
  25. package/dist/cli.js +1357 -0
  26. package/dist/constants.js +39 -0
  27. package/dist/core/define-hook.js +61 -0
  28. package/dist/core/env.js +186 -0
  29. package/dist/core/logger.js +508 -0
  30. package/dist/core/stdin.js +38 -0
  31. package/dist/core/transport.js +231 -0
  32. package/dist/core/types.js +10 -0
  33. package/dist/index.js +17 -0
  34. package/dist/scaffold.js +512 -0
  35. package/package.json +61 -0
  36. package/types/agents/antigravity/events.d.ts +50 -0
  37. package/types/agents/antigravity/hooks.d.ts +76 -0
  38. package/types/agents/antigravity/index.d.ts +27 -0
  39. package/types/agents/antigravity/outputs.d.ts +107 -0
  40. package/types/agents/antigravity/transport.d.ts +47 -0
  41. package/types/agents/antigravity/types.d.ts +58 -0
  42. package/types/agents/claude-code/env.d.ts +27 -0
  43. package/types/agents/claude-code/events.d.ts +69 -0
  44. package/types/agents/claude-code/hooks.d.ts +1182 -0
  45. package/types/agents/claude-code/index.d.ts +23 -0
  46. package/types/agents/claude-code/outputs.d.ts +1160 -0
  47. package/types/agents/claude-code/tool-helpers.d.ts +807 -0
  48. package/types/agents/claude-code/transport.d.ts +119 -0
  49. package/types/agents/claude-code/types.d.ts +557 -0
  50. package/types/agents/codex/cli-support.d.ts +105 -0
  51. package/types/agents/codex/constants.d.ts +10 -0
  52. package/types/agents/codex/events.d.ts +63 -0
  53. package/types/agents/codex/hooks.d.ts +135 -0
  54. package/types/agents/codex/index.d.ts +19 -0
  55. package/types/agents/codex/outputs.d.ts +190 -0
  56. package/types/agents/codex/scaffold.d.ts +14 -0
  57. package/types/agents/codex/transport.d.ts +74 -0
  58. package/types/agents/codex/types.d.ts +131 -0
  59. package/types/cli.d.ts +315 -0
  60. package/types/constants.d.ts +9 -0
  61. package/types/core/define-hook.d.ts +39 -0
  62. package/types/core/env.d.ts +150 -0
  63. package/types/core/logger.d.ts +476 -0
  64. package/types/core/stdin.d.ts +21 -0
  65. package/types/core/transport.d.ts +131 -0
  66. package/types/core/types.d.ts +137 -0
  67. package/types/index.d.ts +18 -0
  68. package/types/scaffold.d.ts +46 -0
@@ -0,0 +1,131 @@
1
+ export type HookEventName = "PreToolUse" | "PostToolUse" | "PermissionRequest" | "UserPromptSubmit" | "SessionStart" | "SubagentStart" | "Stop" | "SubagentStop" | "PreCompact" | "PostCompact";
2
+ export type PermissionMode = "default" | "acceptEdits" | "plan" | "dontAsk" | "bypassPermissions";
3
+ export type SessionStartSource = "startup" | "resume" | "clear" | "compact";
4
+ export type PreCompactTrigger = "manual" | "auto";
5
+ export interface BaseHookInput {
6
+ cwd: string;
7
+ hook_event_name: HookEventName;
8
+ model: string;
9
+ session_id: string;
10
+ transcript_path: string | null;
11
+ }
12
+ export interface PreToolUseInput extends BaseHookInput {
13
+ hook_event_name: "PreToolUse";
14
+ permission_mode: PermissionMode;
15
+ tool_input: unknown;
16
+ tool_name: string;
17
+ tool_use_id: string;
18
+ turn_id: string;
19
+ agent_id?: string;
20
+ agent_type?: string;
21
+ }
22
+ export interface PostToolUseInput extends BaseHookInput {
23
+ hook_event_name: "PostToolUse";
24
+ permission_mode: PermissionMode;
25
+ tool_input: unknown;
26
+ tool_name: string;
27
+ tool_response: unknown;
28
+ tool_use_id: string;
29
+ turn_id: string;
30
+ agent_id?: string;
31
+ agent_type?: string;
32
+ }
33
+ export interface PermissionRequestInput extends BaseHookInput {
34
+ hook_event_name: "PermissionRequest";
35
+ permission_mode: PermissionMode;
36
+ tool_input: unknown;
37
+ tool_name: string;
38
+ turn_id: string;
39
+ agent_id?: string;
40
+ agent_type?: string;
41
+ }
42
+ export interface UserPromptSubmitInput extends BaseHookInput {
43
+ hook_event_name: "UserPromptSubmit";
44
+ permission_mode: PermissionMode;
45
+ prompt: string;
46
+ turn_id: string;
47
+ agent_id?: string;
48
+ agent_type?: string;
49
+ }
50
+ export interface SessionStartInput extends BaseHookInput {
51
+ hook_event_name: "SessionStart";
52
+ permission_mode: PermissionMode;
53
+ source: SessionStartSource;
54
+ }
55
+ export interface SubagentStartInput extends BaseHookInput {
56
+ hook_event_name: "SubagentStart";
57
+ permission_mode: PermissionMode;
58
+ agent_id: string;
59
+ agent_type: string;
60
+ turn_id: string;
61
+ }
62
+ export interface StopInput extends BaseHookInput {
63
+ hook_event_name: "Stop";
64
+ permission_mode: PermissionMode;
65
+ last_assistant_message: string | null;
66
+ stop_hook_active: boolean;
67
+ turn_id: string;
68
+ }
69
+ export interface SubagentStopInput extends BaseHookInput {
70
+ hook_event_name: "SubagentStop";
71
+ permission_mode: PermissionMode;
72
+ agent_id: string;
73
+ agent_transcript_path: string | null;
74
+ agent_type: string;
75
+ last_assistant_message: string | null;
76
+ stop_hook_active: boolean;
77
+ turn_id: string;
78
+ }
79
+ export interface PreCompactInput extends BaseHookInput {
80
+ hook_event_name: "PreCompact";
81
+ trigger: PreCompactTrigger;
82
+ turn_id: string;
83
+ agent_id?: string;
84
+ agent_type?: string;
85
+ }
86
+ export interface PostCompactInput extends BaseHookInput {
87
+ hook_event_name: "PostCompact";
88
+ trigger: PreCompactTrigger;
89
+ turn_id: string;
90
+ agent_id?: string;
91
+ agent_type?: string;
92
+ }
93
+ export type HookInput = PreToolUseInput | PostToolUseInput | PermissionRequestInput | UserPromptSubmitInput | SessionStartInput | SubagentStartInput | StopInput | SubagentStopInput | PreCompactInput | PostCompactInput;
94
+ /**
95
+ * "error" (default) preserves existing behavior: an unhandled exception
96
+ * anywhere in the runtime (stdin read, parse, handler, serialize, write,
97
+ * cleanup) writes a stack trace to stderr and exits non-zero.
98
+ *
99
+ * "continue" is an opt-in fail-open policy for advisory hooks whose only
100
+ * purpose is to enrich context (e.g. UserPromptSubmit additionalContext).
101
+ * Under this policy, unexpected runtime failures are swallowed, the event's
102
+ * valid empty output (`{}`) is emitted if no response was already written,
103
+ * and the process exits 0 so Codex does not show a failed-hook banner.
104
+ * `BlockError` is unaffected: it always writes its reason and exits 2.
105
+ * Do not use "continue" for hooks that enforce permission, safety, or
106
+ * policy decisions — a swallowed failure there silently grants the
107
+ * decision the hook was supposed to make.
108
+ */
109
+ export type UnexpectedErrorPolicy = "error" | "continue";
110
+ /** The runtime phase in which an unexpected error occurred. */
111
+ export type HookErrorPhase = "read" | "parse" | "handler" | "serialize" | "write" | "cleanup";
112
+ /**
113
+ * Best-effort diagnostic sink for unexpected errors under the "continue"
114
+ * policy. Called in addition to (not instead of) the runtime's own logger.
115
+ * Errors thrown by this handler are swallowed — a broken diagnostic sink
116
+ * must never itself fail the invocation.
117
+ */
118
+ export type UnexpectedErrorHandler = (error: unknown, phase: HookErrorPhase) => void;
119
+ export interface MatcherHookConfig {
120
+ matcher?: string;
121
+ timeout?: number;
122
+ statusMessage?: string;
123
+ unexpectedError?: UnexpectedErrorPolicy;
124
+ onUnexpectedError?: UnexpectedErrorHandler;
125
+ }
126
+ export interface NoMatcherHookConfig {
127
+ timeout?: number;
128
+ statusMessage?: string;
129
+ unexpectedError?: UnexpectedErrorPolicy;
130
+ onUnexpectedError?: UnexpectedErrorHandler;
131
+ }
package/types/cli.d.ts ADDED
@@ -0,0 +1,315 @@
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
+ * agent-hooks --agent claude-code -i "hooks/**\/*.ts" -o "./dist/hooks.json"
11
+ *
12
+ * # With runtime logging (equivalent to AGENT_HOOKS_LOG_FILE)
13
+ * agent-hooks --agent claude-code -i "hooks/**\/*.ts" -o "./dist/hooks.json" --log /tmp/hooks.log
14
+ * ```
15
+ * @module
16
+ */
17
+ import * as esbuild from "esbuild";
18
+ import type { HookEventName } from "./agents/claude-code/types.js";
19
+ import { HOOK_FACTORY_TO_EVENT } from "./constants.js";
20
+ /**
21
+ * Hook context determines how paths are resolved in hooks.json.
22
+ *
23
+ * - `plugin`: Uses `$CLAUDE_PLUGIN_ROOT` for plugin hooks
24
+ * - `agent`: Uses `"$CLAUDE_PROJECT_DIR"` for agent hooks (.claude/hooks/)
25
+ */
26
+ type HookContext = "plugin" | "agent";
27
+ /**
28
+ * Command-line arguments parsed from process.argv.
29
+ */
30
+ interface CliArgs {
31
+ /** Target agent runtime (required): "claude-code" | "codex" | "antigravity". */
32
+ agent: string;
33
+ /** Glob pattern for hook source files. */
34
+ input: string;
35
+ /** Path for hooks.json output file. */
36
+ output: string;
37
+ /** Optional log file path (hardcoded into bundle). */
38
+ log?: string;
39
+ /** Optional env var name whose value supplies the log file path at runtime. */
40
+ logEnvVar?: string;
41
+ /** Show help. */
42
+ help: boolean;
43
+ /** Show version. */
44
+ version: boolean;
45
+ /** Directory path for scaffolding a new hook project. */
46
+ scaffold?: string;
47
+ /** Comma-separated list of hook types to generate when scaffolding. */
48
+ hooks?: string;
49
+ /** Node executable path to use in command output (default: "node"). */
50
+ executable?: string;
51
+ /** Repeated esbuild loader flags in EXT=TYPE form. */
52
+ loaderFlags: string[];
53
+ /** Force Codex plugin mode: ${PLUGIN_ROOT}-relative commands + stable names default. */
54
+ pluginRoot?: boolean;
55
+ /** Emit hash-free `<name>.mjs` bundles. Defaults to true; set false with --no-stable-names. */
56
+ stableNames?: boolean;
57
+ /** Embed an inline sourcemap in compiled bundles. Defaults to true; set false with --no-sourcemap. */
58
+ sourcemap?: boolean;
59
+ }
60
+ type HookLoaderMap = Record<string, esbuild.Loader>;
61
+ /**
62
+ * Metadata extracted from a hook file via TypeScript AST analysis.
63
+ */
64
+ interface HookMetadata {
65
+ /** The hook event type (PreToolUse, SessionStart, etc.). */
66
+ hookEventName: HookEventName;
67
+ /** Optional matcher pattern from hook config. */
68
+ matcher?: string;
69
+ /** Optional timeout in milliseconds from hook config. */
70
+ timeout?: number;
71
+ }
72
+ /**
73
+ * A compiled hook with its metadata and output path.
74
+ */
75
+ interface CompiledHook {
76
+ /** Original source file path. */
77
+ sourcePath: string;
78
+ /** Compiled output file path. */
79
+ outputPath: string;
80
+ /** Output filename (e.g., "my-hook.abc123de.mjs"). */
81
+ outputFilename: string;
82
+ /** Extracted hook metadata. */
83
+ metadata: HookMetadata;
84
+ }
85
+ /**
86
+ * Individual hook configuration within a matcher group.
87
+ */
88
+ interface HookConfig {
89
+ /** Hook type - always "command" for compiled hooks. */
90
+ type: "command";
91
+ /** Absolute path to compiled hook executable. */
92
+ command: string;
93
+ /** Optional timeout in seconds. */
94
+ timeout?: number;
95
+ }
96
+ /**
97
+ * Matcher group entry within an event type.
98
+ */
99
+ interface MatcherEntry {
100
+ /** Matcher pattern (tool name, regex, etc.). Optional for some event types. */
101
+ matcher?: string;
102
+ /** Array of hook configurations in this matcher group. */
103
+ hooks: HookConfig[];
104
+ }
105
+ /**
106
+ * The complete hooks.json structure expected by Claude Code.
107
+ *
108
+ * Format: { hooks: { EventType: [ { matcher?, hooks: [...] } ] } }
109
+ */
110
+ interface HooksJson {
111
+ /** Object keyed by event type (PreToolUse, SessionStart, etc.). */
112
+ hooks: Partial<Record<HookEventName, MatcherEntry[]>>;
113
+ /** Generated file tracking metadata. */
114
+ __generated: {
115
+ /** Array of generated filenames. */
116
+ files: string[];
117
+ /** ISO timestamp of generation. */
118
+ timestamp: string;
119
+ };
120
+ }
121
+ /**
122
+ * Parses command-line arguments.
123
+ * @param argv - Process argv (usually process.argv.slice(2))
124
+ * @returns Parsed arguments
125
+ */
126
+ declare function parseArgs(argv: string[]): CliArgs;
127
+ /**
128
+ * Validates CLI arguments and returns error message if invalid.
129
+ * @param args - Parsed CLI arguments
130
+ * @returns Error message if invalid, undefined if valid
131
+ */
132
+ declare function validateArgs(args: CliArgs): string | undefined;
133
+ declare function parseLoaderFlag(spec: string): {
134
+ extension: string;
135
+ loader: esbuild.Loader;
136
+ } | undefined;
137
+ declare function buildLoaderMap(loaderFlags?: string[]): HookLoaderMap;
138
+ /**
139
+ * Extracts hook metadata from a TypeScript source file using AST analysis.
140
+ *
141
+ * Looks for default exports that call hook factory functions (preToolUseHook, etc.)
142
+ * and extracts the hook type, matcher, and timeout from the config object.
143
+ * @param sourcePath - Absolute path to the TypeScript source file
144
+ * @returns Extracted hook metadata or undefined if not a valid hook file
145
+ * @example
146
+ * ```typescript
147
+ * // For a file containing:
148
+ * // export default preToolUseHook({ matcher: 'Bash', timeout: 5000 }, handler);
149
+ *
150
+ * const metadata = analyzeHookFile('/path/to/hook.ts');
151
+ * // { hookEventName: 'PreToolUse', matcher: 'Bash', timeout: 5000 }
152
+ * ```
153
+ */
154
+ declare function analyzeHookFile(sourcePath: string): HookMetadata | undefined;
155
+ /**
156
+ * Discovers hook files matching the glob pattern.
157
+ * @param pattern - Glob pattern for hook files
158
+ * @param cwd - Current working directory for relative patterns
159
+ * @returns Array of absolute paths to hook files
160
+ */
161
+ declare function discoverHookFiles(pattern: string, cwd: string): Promise<string[]>;
162
+ /**
163
+ * Options for compiling a hook.
164
+ */
165
+ interface CompileHookOptions {
166
+ /** Absolute path to source file. */
167
+ sourcePath: string;
168
+ /** Directory for compiled output. */
169
+ outputDir: string;
170
+ /** Optional log file path to hardcode into the bundle via the banner. */
171
+ logFilePath?: string;
172
+ /** Optional env var name the Logger should read for the log file path at runtime. */
173
+ logEnvVar?: string;
174
+ /** Explicit esbuild loaders for non-code imports. */
175
+ loaders: HookLoaderMap;
176
+ /** Embed an inline sourcemap in the compiled content. Defaults to true; set false with --no-sourcemap. */
177
+ sourcemap?: boolean;
178
+ }
179
+ /**
180
+ * Result of compiling a hook.
181
+ */
182
+ interface CompileHookResult {
183
+ /** Compiled content, with inline sourcemaps unless disabled. */
184
+ content: string;
185
+ /** Stable content hash generated from sourcemap-free output. */
186
+ contentHash: string;
187
+ }
188
+ /**
189
+ * Re-roots a realpath through the node_modules symlink chain visible from
190
+ * `resolveDir`, so relative paths computed against it do not depend on how
191
+ * deeply the checkout is nested below a shared install.
192
+ *
193
+ * Walks `resolveDir`'s ancestors for a `node_modules` directory whose
194
+ * realpath contains `realPath` (e.g. a checkout-level symlink to a shared
195
+ * install) and returns `realPath` as seen through that directory. Also
196
+ * recognizes the package directory itself being symlinked into that
197
+ * `node_modules` — whether the package lives in a store (pnpm-style store
198
+ * link, npx cache, external install) or in the checkout's own source tree
199
+ * (`packages/` with a `node_modules` entry symlinked back into it) — and
200
+ * re-roots through the symlink form, so esbuild records one module identity
201
+ * regardless of install topology. Falls back to the realpath when no such
202
+ * node_modules exists — with no symlink involved, the realpath is already
203
+ * checkout-local.
204
+ */
205
+ declare function symlinkVisiblePath(realPath: string, resolveDir: string): string;
206
+ /**
207
+ * Compiles a TypeScript hook file to a self-contained ESM executable.
208
+ *
209
+ * Uses esbuild's stdin option to avoid writing temporary wrapper files to disk.
210
+ * This produces stable, reproducible builds by:
211
+ * - Using a distinct sourcefile name to avoid import resolution conflicts
212
+ * - Eliminating environment-specific temp paths from source comments
213
+ *
214
+ * Uses a two-step process to generate stable content hashes:
215
+ * 1. Compile WITHOUT sourcemaps → generate stable content hash
216
+ * 2. Compile WITH sourcemaps → final output content
217
+ *
218
+ * @param options - Compilation options
219
+ * @returns Compiled content and stable content hash
220
+ */
221
+ declare function compileHook(options: CompileHookOptions): Promise<CompileHookResult>;
222
+ /**
223
+ * Generates a content hash (SHA-256, 8-char prefix) for a compiled hook.
224
+ * @param content - Compiled hook content
225
+ * @returns 8-character hex hash
226
+ */
227
+ declare function generateContentHash(content: string): string;
228
+ /**
229
+ * Removes hashed `<name>.<hash>.mjs` siblings of the stable bundles we just
230
+ * wrote, so re-running the build with --stable-names cleans up leftovers from
231
+ * previous --no-stable-names (or pre-1.7) runs.
232
+ */
233
+ declare function pruneStaleHashedBundles(outputDir: string, keepFilenames: Set<string>): void;
234
+ /**
235
+ * Groups compiled hooks by event type, then by matcher pattern.
236
+ * @param compiledHooks - Array of compiled hooks
237
+ * @returns Nested map: EventType -> Matcher -> Hooks
238
+ */
239
+ declare function groupHooksByEventAndMatcher(compiledHooks: CompiledHook[]): Map<HookEventName, Map<string | undefined, CompiledHook[]>>;
240
+ /**
241
+ * Result of detecting the hook context, including the root directory.
242
+ */
243
+ interface HookContextInfo {
244
+ /** Hook context type. */
245
+ context: HookContext;
246
+ /** Absolute path to the root directory (plugin root or project root). */
247
+ rootDir: string;
248
+ }
249
+ /**
250
+ * Auto-detects the hook context and root directory based on directory structure.
251
+ *
252
+ * Detection logic:
253
+ * - If output path contains `.claude/` directory segment → agent context, root is parent of .claude/
254
+ * - If `.claude-plugin/` directory exists within 3 levels up → plugin context, root is that directory
255
+ * - Default: plugin context with hooks.json parent directory as root
256
+ * @param outputPath - Absolute path to the hooks.json output file
257
+ * @returns Detected hook context and root directory
258
+ */
259
+ declare function detectHookContext(outputPath: string): HookContextInfo;
260
+ /**
261
+ * Generates a command path based on the hook context.
262
+ *
263
+ * Calculates the relative path from the root directory to the bin directory.
264
+ * Prepends the node executable.
265
+ *
266
+ * - `plugin`: Uses `node "$CLAUDE_PLUGIN_ROOT"/hooks/bin/filename`
267
+ * - `agent`: Uses `node "$CLAUDE_PROJECT_DIR"/.claude/hooks/bin/filename`
268
+ * @param filename - The compiled hook filename
269
+ * @param buildDir - Absolute path to the bin directory
270
+ * @param contextInfo - Hook context info including root directory
271
+ * @param executable - Node executable path (default: "node")
272
+ * @returns The command path string
273
+ */
274
+ declare function generateCommandPath(filename: string, buildDir: string, contextInfo: HookContextInfo, executable?: string): string;
275
+ /**
276
+ * Generates the hooks.json content in Claude Code's expected format.
277
+ *
278
+ * Format: { hooks: { EventType: [ { matcher?, hooks: [...] } ] } }
279
+ * @param compiledHooks - Array of compiled hooks
280
+ * @param buildDir - Absolute path to the bin directory
281
+ * @param contextInfo - Hook context info for path resolution
282
+ * @param executable - Node executable path (default: "node")
283
+ * @returns The hooks.json structure
284
+ */
285
+ declare function generateHooksJson(compiledHooks: CompiledHook[], buildDir: string, contextInfo: HookContextInfo, executable?: string): HooksJson;
286
+ /**
287
+ * Reads an existing hooks.json file if it exists.
288
+ * @param outputPath - Path to the hooks.json file
289
+ * @returns Parsed HooksJson or undefined if file doesn't exist
290
+ */
291
+ declare function readExistingHooksJson(outputPath: string): HooksJson | undefined;
292
+ /**
293
+ * Removes previously generated hook files from disk.
294
+ * Only removes files that were tracked in __generated.files.
295
+ * @param existingHooksJson - The existing hooks.json content
296
+ * @param outputDir - Directory containing the generated files
297
+ */
298
+ declare function removeOldGeneratedFiles(existingHooksJson: HooksJson, outputDir: string): void;
299
+ /**
300
+ * Extracts hooks from an existing hooks.json that were NOT generated by this package.
301
+ * Identifies generated hooks by checking if their command path matches the generated file pattern.
302
+ * @param existingHooksJson - The existing hooks.json content
303
+ * @returns Object containing preserved hooks (keyed by event type)
304
+ */
305
+ declare function extractPreservedHooks(existingHooksJson: HooksJson): Partial<Record<HookEventName, MatcherEntry[]>>;
306
+ /**
307
+ * Merges preserved hooks with newly generated hooks.
308
+ * Preserved hooks are added first, then new hooks are appended.
309
+ * @param newHooksJson - The newly generated hooks.json content
310
+ * @param preservedHooks - Hooks to preserve from the existing hooks.json
311
+ * @returns Merged HooksJson
312
+ */
313
+ declare function mergeHooksJson(newHooksJson: HooksJson, preservedHooks: Partial<Record<HookEventName, MatcherEntry[]>>): HooksJson;
314
+ export type { CliArgs, CompiledHook, HookConfig, HookLoaderMap, HookMetadata, HooksJson, MatcherEntry };
315
+ export { analyzeHookFile, buildLoaderMap, buildLoaderMap as buildEsbuildLoaderMap, compileHook, detectHookContext, discoverHookFiles, discoverHookFiles as discoverHookSourceFiles, extractPreservedHooks, generateCommandPath, generateContentHash, generateContentHash as sha256Prefix8, generateHooksJson, groupHooksByEventAndMatcher, HOOK_FACTORY_TO_EVENT, mergeHooksJson, parseArgs, parseLoaderFlag, parseLoaderFlag as parseEsbuildLoaderFlag, pruneStaleHashedBundles, readExistingHooksJson, removeOldGeneratedFiles, symlinkVisiblePath, validateArgs, };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Shared constants for the CLI and scaffold modules.
3
+ * @module
4
+ */
5
+ import type { HookEventName } from "./agents/claude-code/types.js";
6
+ /**
7
+ * Maps hook factory function names to their event names.
8
+ */
9
+ export declare const HOOK_FACTORY_TO_EVENT: Record<string, HookEventName>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Generic hook factory primitive.
3
+ *
4
+ * `defineHook` is the agent-neutral seam every agent's per-event factory
5
+ * builds on: the agent supplies its event name, its config shape, and an
6
+ * optional policy gate enforcing that event's advisory allow-list. Core never
7
+ * hardcodes an event list — validation is fully injected via
8
+ * {@link HookPolicyGate} and fails closed when a gate rejects.
9
+ * @module
10
+ * @example
11
+ * ```typescript
12
+ * const hook = defineHook(
13
+ * "UserPromptSubmit",
14
+ * { unexpectedError: "continue" },
15
+ * async (input, { logger }) => {
16
+ * logger.info("Enriching prompt");
17
+ * return { additionalContext: "..." };
18
+ * },
19
+ * (eventName, policy) => policy === undefined || policy === "continue",
20
+ * );
21
+ * ```
22
+ */
23
+ import type { HookConfig, HookContext, HookFunction, HookHandler, HookPolicyGate } from "./types.js";
24
+ /**
25
+ * Creates a hook function for a specific agent event.
26
+ *
27
+ * @param eventName - The agent's event name (e.g. `"PreToolUse"`); attached
28
+ * to the returned {@link HookFunction} as `eventName` for runtime inspection.
29
+ * @param config - Hook configuration (matcher, timeout, unexpected-error
30
+ * policy and diagnostics, optional context factory).
31
+ * @param handler - The handler function to wrap.
32
+ * @param policyGate - Optional per-event policy-validation closure injected
33
+ * by the agent. Called with `(eventName, config.unexpectedError)`; returning
34
+ * `false` or throwing rejects the configuration, and this function throws
35
+ * (fail closed) instead of creating the hook.
36
+ * @returns A wrapped hook function carrying its metadata for the driver.
37
+ * @throws Error when `policyGate` rejects `(eventName, policy)`.
38
+ */
39
+ export declare function defineHook<TInput, TOutput, TContext extends HookContext = HookContext>(eventName: string, config: HookConfig<TInput>, handler: HookHandler<TInput, TOutput, TContext>, policyGate?: HookPolicyGate): HookFunction<TInput, TOutput, TContext>;
@@ -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/agent-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/agent-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;