@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.
- package/README.md +137 -0
- package/dist/agents/antigravity/events.js +42 -0
- package/dist/agents/antigravity/hooks.js +59 -0
- package/dist/agents/antigravity/index.js +28 -0
- package/dist/agents/antigravity/outputs.js +81 -0
- package/dist/agents/antigravity/transport.js +84 -0
- package/dist/agents/antigravity/types.js +11 -0
- package/dist/agents/claude-code/env.js +27 -0
- package/dist/agents/claude-code/events.js +87 -0
- package/dist/agents/claude-code/hooks.js +1000 -0
- package/dist/agents/claude-code/index.js +57 -0
- package/dist/agents/claude-code/outputs.js +579 -0
- package/dist/agents/claude-code/tool-helpers.js +840 -0
- package/dist/agents/claude-code/transport.js +254 -0
- package/dist/agents/claude-code/types.js +11 -0
- package/dist/agents/codex/cli-support.js +318 -0
- package/dist/agents/codex/constants.js +29 -0
- package/dist/agents/codex/events.js +68 -0
- package/dist/agents/codex/hooks.js +93 -0
- package/dist/agents/codex/index.js +21 -0
- package/dist/agents/codex/outputs.js +189 -0
- package/dist/agents/codex/scaffold.js +273 -0
- package/dist/agents/codex/transport.js +139 -0
- package/dist/agents/codex/types.js +1 -0
- package/dist/cli.js +1357 -0
- package/dist/constants.js +39 -0
- package/dist/core/define-hook.js +61 -0
- package/dist/core/env.js +186 -0
- package/dist/core/logger.js +508 -0
- package/dist/core/stdin.js +38 -0
- package/dist/core/transport.js +231 -0
- package/dist/core/types.js +10 -0
- package/dist/index.js +17 -0
- package/dist/scaffold.js +512 -0
- package/package.json +61 -0
- package/types/agents/antigravity/events.d.ts +50 -0
- package/types/agents/antigravity/hooks.d.ts +76 -0
- package/types/agents/antigravity/index.d.ts +27 -0
- package/types/agents/antigravity/outputs.d.ts +107 -0
- package/types/agents/antigravity/transport.d.ts +47 -0
- package/types/agents/antigravity/types.d.ts +58 -0
- package/types/agents/claude-code/env.d.ts +27 -0
- package/types/agents/claude-code/events.d.ts +69 -0
- package/types/agents/claude-code/hooks.d.ts +1182 -0
- package/types/agents/claude-code/index.d.ts +23 -0
- package/types/agents/claude-code/outputs.d.ts +1160 -0
- package/types/agents/claude-code/tool-helpers.d.ts +807 -0
- package/types/agents/claude-code/transport.d.ts +119 -0
- package/types/agents/claude-code/types.d.ts +557 -0
- package/types/agents/codex/cli-support.d.ts +105 -0
- package/types/agents/codex/constants.d.ts +10 -0
- package/types/agents/codex/events.d.ts +63 -0
- package/types/agents/codex/hooks.d.ts +135 -0
- package/types/agents/codex/index.d.ts +19 -0
- package/types/agents/codex/outputs.d.ts +190 -0
- package/types/agents/codex/scaffold.d.ts +14 -0
- package/types/agents/codex/transport.d.ts +74 -0
- package/types/agents/codex/types.d.ts +131 -0
- package/types/cli.d.ts +315 -0
- package/types/constants.d.ts +9 -0
- package/types/core/define-hook.d.ts +39 -0
- package/types/core/env.d.ts +150 -0
- package/types/core/logger.d.ts +476 -0
- package/types/core/stdin.d.ts +21 -0
- package/types/core/transport.d.ts +131 -0
- package/types/core/types.d.ts +137 -0
- package/types/index.d.ts +18 -0
- 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;
|