@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
package/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # `@goodfoot/agent-hooks`
2
+
3
+ Unified agent hooks library for AI coding CLIs. One package consolidating the
4
+ runtime previously split across `@goodfoot/claude-code-hooks` and
5
+ `@goodfoot/codex-hooks`, with per-agent entry points.
6
+
7
+ **Status: `0.1.0` — core only.** This release ships the agent-neutral core:
8
+ the shared driver, transport abstraction, hook factory primitive, `Logger`,
9
+ and env utilities. Per-agent entry points (`@goodfoot/agent-hooks/claude-code`,
10
+ `/codex`, `/antigravity`) do not exist yet; they are added to the package
11
+ `exports` map only in the version that lands their implementation — never
12
+ pre-declared.
13
+
14
+ ## Exports map policy
15
+
16
+ The `exports` map is an explicit, closed list of subpaths. It deliberately
17
+ does **not** include a `./*` wildcard subpath (unlike the source packages):
18
+ a wildcard would make every internal module independently importable,
19
+ defeating the root-export lint. This is a permanent rule enforced by the
20
+ package's own test suite (`tests/export-surface.test.ts`), which also carries
21
+ a fail-case fixture proving the check rejects a wildcard entry.
22
+
23
+ ## Core API
24
+
25
+ ### `defineHook(eventName, config, handler, policyGate?)`
26
+
27
+ Factory primitive every agent's per-event factory builds on:
28
+
29
+ ```typescript
30
+ import { defineHook } from '@goodfoot/agent-hooks';
31
+
32
+ const hook = defineHook(
33
+ 'UserPromptSubmit',
34
+ { unexpectedError: 'continue' },
35
+ async (input, { logger }) => {
36
+ logger.info('Enriching prompt');
37
+ return { additionalContext: '…' };
38
+ },
39
+ // Optional per-event policy gate injected by each agent; core never
40
+ // hardcodes an event list. Returning false (or throwing) fails closed.
41
+ (eventName, policy) => policy === undefined || policy === 'continue',
42
+ );
43
+ ```
44
+
45
+ ### `Transport` and `drive(transport, hookFn)`
46
+
47
+ Core owns the try/catch skeleton — stdin read, JSON parse, handler invocation,
48
+ outcome classification, buffered single-write emission, process exit — and has
49
+ no knowledge of exit codes or stdout policy. Every wire decision belongs to
50
+ the agent's transport:
51
+
52
+ ```typescript
53
+ import { drive, type Transport } from '@goodfoot/agent-hooks';
54
+
55
+ const transport: Transport<MyOutput> = {
56
+ finalize: (outcome) => {
57
+ switch (outcome.kind) {
58
+ case 'response':
59
+ return { stdout: JSON.stringify(outcome.output ?? {}), exitCode: 0 };
60
+ case 'block':
61
+ return { stderr: outcome.error.message, exitCode: 2 };
62
+ default:
63
+ return { stderr: 'internal error', exitCode: 1 };
64
+ }
65
+ },
66
+ };
67
+
68
+ await drive(transport, myHook);
69
+ ```
70
+
71
+ `finalize(outcome)` is a pure mapping called exactly once per invocation. The
72
+ optional `rawStdout(output)` predicate declares which returned outputs carry
73
+ a plain-text protocol payload (e.g. worktree events whose stdout the host
74
+ reads verbatim); matching outputs are classified as `{ kind: "rawStdout",
75
+ stdout }` instead of `response`. Returned outputs are never modified by core.
76
+
77
+ ### Outcomes
78
+
79
+ `HookOutcome<TOutput>` is a discriminated union:
80
+
81
+ | `kind` | Payload | Produced when |
82
+ |---|---|---|
83
+ | `response` | `output: TOutput \| undefined` | handler returned normally (`undefined` = empty response), or a throw was swallowed under `unexpectedError: "continue"` |
84
+ | `block` | `error: HookBlockError` | handler threw a `HookBlockError` |
85
+ | `handlerError` | `error: unknown; phase` | any other failure, classified at its phase (`read`, `parse`, `handler`, …) |
86
+ | `rawStdout` | `stdout: string` | the transport's `rawStdout` predicate matched a normal return |
87
+
88
+ ### `HookBlockError`
89
+
90
+ ```typescript
91
+ import { HookBlockError } from '@goodfoot/agent-hooks';
92
+
93
+ throw new HookBlockError('blocked: writes outside workspace', { tool: 'Bash' });
94
+ ```
95
+
96
+ `drive()` classifies a caught `HookBlockError` **before** consulting the
97
+ `unexpectedError` policy: it always routes to the `block` outcome, under every
98
+ policy value. This is what lets an advisory event's intentional block survive
99
+ a crash mid-computation instead of being silently swallowed by the
100
+ fail-open policy. Any other throw remains subject to policy exactly as in the
101
+ source packages: `"continue"` swallows it into the empty response;
102
+ the default `"error"` policy yields the `handlerError` outcome. A normally
103
+ returned value always passes through to `finalize` unmodified — core never
104
+ inspects returned output for block-signaling fields.
105
+
106
+ Agents may subclass or re-export `HookBlockError`; each agent's transport
107
+ owns the wire translation of the `block` outcome for its events.
108
+
109
+ ### `unexpectedError` policy
110
+
111
+ - `"error"` (default): unexpected failures surface to the transport as
112
+ `handlerError` outcomes.
113
+ - `"continue"`: opt-in fail-open for advisory hooks — unexpected failures are
114
+ swallowed into the empty response, reported through the optional
115
+ `onUnexpectedError` callback and the logger. Never use it for hooks that
116
+ enforce permission, safety, or policy decisions; a `HookBlockError` still
117
+ gets through (see above), but ordinary bugs will not.
118
+
119
+ Malformed or unreadable stdin is classified at its phase like any other
120
+ unexpected failure (`handlerError` with `phase: "parse"`) and logged as
121
+ `Failed to parse stdin JSON`; each agent's transport decides the wire shape
122
+ for that classification.
123
+
124
+ ### `Logger`
125
+
126
+ Structured logging ported behavior-identically from the source packages:
127
+ silent by default, never writes stdout/stderr, optional JSON Lines file output
128
+ via `AGENT_HOOKS_LOG_FILE`, event subscription via `logger.on(level, handler)`.
129
+
130
+ ## Development
131
+
132
+ ```bash
133
+ yarn workspace @goodfoot/agent-hooks run typecheck
134
+ yarn workspace @goodfoot/agent-hooks run build
135
+ yarn workspace @goodfoot/agent-hooks run lint
136
+ yarn workspace @goodfoot/agent-hooks run test
137
+ ```
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Antigravity hook event names and the (currently empty) advisory allow-list.
3
+ *
4
+ * Unlike Claude Code's and Codex's allow-lists, which transcribe a named set
5
+ * of enrichment hooks out of a shipped README, Antigravity has no in-repo
6
+ * doc to transcribe from this release (Step 0 descope — see the plan's Step
7
+ * 5 blockquote). The allow-list therefore defaults to **every event
8
+ * excluded**, not a curated subset, until a future card's `CONTRACT.md`
9
+ * names a specific enrichment hook safe to opt into `unexpectedError:
10
+ * "continue"`.
11
+ *
12
+ * That default is itself gated on a second precondition: because Antigravity
13
+ * has no exit-code channel at all (every event is payload-only — see
14
+ * `transport.ts` and `outputs.ts`'s {@link AntigravityBlockError}), a
15
+ * fail-open swallow on this agent has no narrower fallback than Claude
16
+ * Code's or Codex's — there is no stderr+exit-2 loud path to fall back to.
17
+ * `AntigravityBlockError` landing first (this same step) is what makes it
18
+ * safe to ever widen this list at all: an intentional block computed
19
+ * mid-throw still survives `unexpectedError: "continue"` via `drive()`'s
20
+ * pre-policy classification, exactly as it does for the other two agents.
21
+ * @module
22
+ */
23
+ /** All Antigravity hook event names as a readonly array. */
24
+ export const HOOK_EVENT_NAMES = [
25
+ "PreToolUse",
26
+ "PostToolUse",
27
+ "PreInvocation",
28
+ "PostInvocation",
29
+ "Stop",
30
+ ];
31
+ /** The two events whose config accepts a `matcher` (plan step 5.3: "matcher on the two ToolUse events"). */
32
+ export const EVENTS_WITH_MATCHER = ["PreToolUse", "PostToolUse"];
33
+ /**
34
+ * Events never allowed the fail-open `unexpectedError: "continue"` policy.
35
+ *
36
+ * Every shipped event, by default — see module docs. This is a stated
37
+ * default (plan Decisions: "advisory list default empty until CONTRACT.md
38
+ * justifies an entry"), not a silent gap.
39
+ */
40
+ export const EXCLUDED_FROM_ADVISORY = [...HOOK_EVENT_NAMES];
41
+ /** The advisory allow-list. Empty until a future `CONTRACT.md` names an entry. */
42
+ export const ADVISORY_EVENTS = HOOK_EVENT_NAMES.filter((eventName) => !EXCLUDED_FROM_ADVISORY.includes(eventName));
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Typed factory functions for Antigravity hooks, built on the shared core
3
+ * {@link ../../core/define-hook.ts|defineHook}.
4
+ *
5
+ * This release ships the typed factory surface and output builders only
6
+ * (plan Step 5, item-6 descope) — no `CONTRACT.md`-derived e2e file, no
7
+ * `./antigravity` export. Every factory injects the advisory allow-list
8
+ * policy gate, so `unexpectedError: "continue"` fails closed at
9
+ * factory-call time for every event ({@link ADVISORY_EVENTS} is empty this
10
+ * release); {@link HookConfigFor} additionally narrows the policy field at
11
+ * compile time.
12
+ *
13
+ * Antigravity's config surface is a single conditional type rather than the
14
+ * separate matcher/no-matcher interface pair Claude Code and Codex use: only
15
+ * two of the five events (`PreToolUse`, `PostToolUse`) carry a `matcher`
16
+ * ({@link EVENTS_WITH_MATCHER} in `events.ts`), and a conditional type on the
17
+ * bound event expresses that distinction without duplicating the base field
18
+ * list twice.
19
+ * @module
20
+ */
21
+ import { defineHook } from "../../core/define-hook.js";
22
+ import { ADVISORY_EVENTS } from "./events.js";
23
+ /**
24
+ * Rejects `unexpectedError: "continue"` unless the bound event is on
25
+ * {@link ADVISORY_EVENTS} — every event, this release, since that list is
26
+ * currently empty.
27
+ */
28
+ const advisoryPolicyGate = (eventName, policy) => policy !== "continue" || ADVISORY_EVENTS.includes(eventName);
29
+ function bindHook(event, config, handler) {
30
+ const coreConfig = {
31
+ matcher: config.matcher,
32
+ timeout: config.timeout,
33
+ unexpectedError: config.unexpectedError,
34
+ onUnexpectedError: config.onUnexpectedError,
35
+ };
36
+ const bound = defineHook(event, coreConfig, handler, advisoryPolicyGate);
37
+ bound.hookEventName = event;
38
+ return bound;
39
+ }
40
+ /** Creates a PreToolUse hook handler. */
41
+ export function preToolUseHook(config, handler) {
42
+ return bindHook("PreToolUse", config, handler);
43
+ }
44
+ /** Creates a PostToolUse hook handler. */
45
+ export function postToolUseHook(config, handler) {
46
+ return bindHook("PostToolUse", config, handler);
47
+ }
48
+ /** Creates a PreInvocation hook handler. */
49
+ export function preInvocationHook(config, handler) {
50
+ return bindHook("PreInvocation", config, handler);
51
+ }
52
+ /** Creates a PostInvocation hook handler. */
53
+ export function postInvocationHook(config, handler) {
54
+ return bindHook("PostInvocation", config, handler);
55
+ }
56
+ /** Creates a Stop hook handler. */
57
+ export function stopHook(config, handler) {
58
+ return bindHook("Stop", config, handler);
59
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * `@goodfoot/agent-hooks/antigravity` — the Antigravity agent surface.
3
+ *
4
+ * **Not yet published as a package subpath.** Step 5 ships under its item-6
5
+ * descope (Step 0 concluded "not runnable / unknown" for the real
6
+ * `antigravity` CLI in this release's environment): typed factories, output
7
+ * builders, and a conformance matrix only. `packages/agent-hooks/package.json`
8
+ * deliberately does not add a `./antigravity` `exports` entry this release —
9
+ * see `tests/export-surface.test.ts`, which asserts the exports map stays
10
+ * closed at `.`/`./claude-code`/`./codex`. This module exists for in-repo
11
+ * testing (the conformance matrix imports it directly by relative path) and
12
+ * for the follow-up card that resolves the CLI-availability question to
13
+ * build on without re-deriving the surface from scratch.
14
+ *
15
+ * Re-exports the 5 hook factories, their output builders (reserved fields
16
+ * emitted only when passed), the `AntigravityBlockError` block signal,
17
+ * input/output types, and the runtime `execute` entry point. No default
18
+ * export.
19
+ * @module
20
+ */
21
+ // Advisory allow-list events
22
+ export { ADVISORY_EVENTS, EVENTS_WITH_MATCHER, EXCLUDED_FROM_ADVISORY, HOOK_EVENT_NAMES } from "./events.js";
23
+ // Hook factory functions - all 5 event types
24
+ export { postInvocationHook, postToolUseHook, preInvocationHook, preToolUseHook, stopHook } from "./hooks.js";
25
+ // Output types and builders (AntigravityBlockError subclasses core HookBlockError)
26
+ export { AntigravityBlockError, EXIT_CODES, postInvocationOutput, postToolUseOutput, preInvocationOutput, preToolUseOutput, stopOutput, } from "./outputs.js";
27
+ // Runtime exports - execute function and transport factory
28
+ export { createAntigravityTransport, execute } from "./transport.js";
@@ -0,0 +1,81 @@
1
+ import { HookBlockError } from "../../core/transport.js";
2
+ /**
3
+ * Antigravity has no exit-code signaling channel: every event replies at
4
+ * exit 0 and every decision — allow, deny, or ask — is expressed inside the
5
+ * JSON payload. `EXIT_CODES.SUCCESS` is the only member for that reason; see
6
+ * `transport.ts`'s module docs for the consequence this has for
7
+ * {@link AntigravityBlockError}.
8
+ */
9
+ export const EXIT_CODES = {
10
+ SUCCESS: 0,
11
+ };
12
+ /**
13
+ * The Antigravity block signal, carried as a subclass of the shared core
14
+ * {@link ../../core/transport.ts|HookBlockError} so `drive()` classifies it
15
+ * **before** consulting `unexpectedError` policy — exactly mirroring Claude
16
+ * Code's `HookBlockError` usage and Codex's `BlockError` re-export.
17
+ *
18
+ * This mechanism is a stated *prerequisite*, not a follow-up, for
19
+ * Antigravity specifically: because every Antigravity event replies at exit
20
+ * 0 with the decision expressed only in the JSON payload (Research, plan
21
+ * Step 5), a handler that throws mid-decision has no narrower exit-code
22
+ * fallback to land on the way Codex's stderr+exit-2 channel does — the block
23
+ * decision would otherwise be indistinguishable from a swallowed crash. This
24
+ * class landing is what makes it safe to ever widen `events.ts`'s advisory
25
+ * allow-list past empty.
26
+ */
27
+ export class AntigravityBlockError extends HookBlockError {
28
+ reason;
29
+ constructor(reason) {
30
+ super(reason);
31
+ this.name = "AntigravityBlockError";
32
+ this.reason = reason;
33
+ }
34
+ }
35
+ function omitUndefined(value) {
36
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
37
+ }
38
+ function buildOutput(type, stdout) {
39
+ return {
40
+ _type: type,
41
+ stdout: omitUndefined(stdout),
42
+ };
43
+ }
44
+ export function preToolUseOutput(options = {}) {
45
+ return buildOutput("PreToolUse", {
46
+ decision: options.decision,
47
+ reason: options.reason,
48
+ additionalContext: options.additionalContext,
49
+ updatedInput: options.updatedInput,
50
+ systemMessage: options.systemMessage,
51
+ });
52
+ }
53
+ export function postToolUseOutput(options = {}) {
54
+ return buildOutput("PostToolUse", {
55
+ decision: options.decision,
56
+ reason: options.reason,
57
+ additionalContext: options.additionalContext,
58
+ systemMessage: options.systemMessage,
59
+ });
60
+ }
61
+ export function preInvocationOutput(options = {}) {
62
+ return buildOutput("PreInvocation", {
63
+ decision: options.decision,
64
+ reason: options.reason,
65
+ additionalContext: options.additionalContext,
66
+ systemMessage: options.systemMessage,
67
+ });
68
+ }
69
+ export function postInvocationOutput(options = {}) {
70
+ return buildOutput("PostInvocation", {
71
+ additionalContext: options.additionalContext,
72
+ systemMessage: options.systemMessage,
73
+ });
74
+ }
75
+ export function stopOutput(options = {}) {
76
+ return buildOutput("Stop", {
77
+ stop: options.stop,
78
+ reason: options.reason,
79
+ systemMessage: options.systemMessage,
80
+ });
81
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Wire transport for Antigravity hooks, built against the core
3
+ * `drive()`/`Transport` interface — the same seam Claude Code's and Codex's
4
+ * transports are rewritten/re-exported against.
5
+ *
6
+ * Antigravity's wire semantics are the simplest of the three shipped agents,
7
+ * and also the least forgiving: **every reply exits 0, unconditionally.**
8
+ * There is no exit-code channel at all — not the 2-of-30-events native-deny
9
+ * split Claude Code has, not Codex's uniform stderr+exit-2 `BlockError`
10
+ * channel. Every decision (allow/deny/ask and its finer variants) and every
11
+ * unexpected-failure diagnostic lives inside the JSON stdout payload:
12
+ *
13
+ * - success → `JSON.stringify(output.stdout)` at exit 0; `null`/`undefined`
14
+ * serializes as `{}`.
15
+ * - a caught {@link AntigravityBlockError} → `{ decision: "deny", reason,
16
+ * ...fields }` on stdout at exit 0 — this is why the class is a stated
17
+ * prerequisite (`outputs.ts` module docs, `events.ts` module docs): with no
18
+ * exit-code fallback, a block computed mid-throw has nowhere else to land.
19
+ * - an unexpected handler/runtime failure → its stacktrace on stderr
20
+ * (diagnostic only, not a wire signal) plus the empty response `{}` on
21
+ * stdout, still at exit 0.
22
+ * @module
23
+ * @example
24
+ * ```typescript
25
+ * // In a compiled hook file
26
+ * import { execute } from './transport.js';
27
+ * import myHook from './my-hook.js';
28
+ *
29
+ * execute(myHook);
30
+ * ```
31
+ */
32
+ import { drive } from "../../core/transport.js";
33
+ import { AntigravityBlockError, EXIT_CODES } from "./outputs.js";
34
+ function formatErrorText(error) {
35
+ return error instanceof Error ? `${error.stack ?? error.message}\n` : `${String(error)}\n`;
36
+ }
37
+ // ============================================================================
38
+ // Transport
39
+ // ============================================================================
40
+ /**
41
+ * Builds the Antigravity wire transport. Every branch returns
42
+ * `exitCode: EXIT_CODES.SUCCESS` — see module docs — so the only thing that
43
+ * varies per outcome is what lands on stdout (the machine-readable payload)
44
+ * versus stderr (a diagnostic no host is asserted to read).
45
+ */
46
+ export function createAntigravityTransport() {
47
+ return {
48
+ finalize(outcome) {
49
+ switch (outcome.kind) {
50
+ case "response":
51
+ case "rawStdout": {
52
+ // rawStdout cannot occur here — no rawStdout predicate is
53
+ // registered below — but the union demands handling.
54
+ const stdoutJson = outcome.kind === "response" && outcome.output !== null && outcome.output !== undefined
55
+ ? JSON.stringify(outcome.output.stdout)
56
+ : "{}";
57
+ return { stdout: stdoutJson, exitCode: EXIT_CODES.SUCCESS };
58
+ }
59
+ case "block": {
60
+ const reason = outcome.error instanceof AntigravityBlockError ? outcome.error.reason : outcome.error.message;
61
+ const fields = outcome.error.fields ?? {};
62
+ return { stdout: JSON.stringify({ decision: "deny", reason, ...fields }), exitCode: EXIT_CODES.SUCCESS };
63
+ }
64
+ case "handlerError": {
65
+ return {
66
+ stderr: formatErrorText(outcome.error),
67
+ stdout: "{}",
68
+ exitCode: EXIT_CODES.SUCCESS,
69
+ };
70
+ }
71
+ }
72
+ },
73
+ };
74
+ }
75
+ // ============================================================================
76
+ // Execute
77
+ // ============================================================================
78
+ /**
79
+ * Executes a hook handler with full runtime orchestration (delegates to the
80
+ * shared core driver with this agent's transport).
81
+ */
82
+ export async function execute(hookFn) {
83
+ await drive(createAntigravityTransport(), hookFn);
84
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Antigravity wire types (plan step 5, item-6 descope: typed factories +
3
+ * output builders + conformance matrix only — no `CONTRACT.md` pin, no
4
+ * published protocol doc exists for this release). Shapes below are the
5
+ * minimal surface the plan names for the five shipped events
6
+ * (`PreToolUse`/`PostToolUse`/`PreInvocation`/`PostInvocation`/`Stop`),
7
+ * derived structurally from Claude Code's and Codex's `types.ts` rather than
8
+ * from any Antigravity-specific reference — there is none to cite yet.
9
+ * @module
10
+ */
11
+ export {};
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Environment variable utilities for Claude Code hooks.
3
+ *
4
+ * The implementation lives in the committed core (`../../core/env.ts`), which
5
+ * is a byte-for-byte port of `claude-code-hooks/src/env.ts`; this module is
6
+ * the per-agent import site so the Claude Code surface keeps its historical
7
+ * `env.js` path on the barrel.
8
+ * @module
9
+ * @example
10
+ * ```typescript
11
+ * import { getProjectDir, persistEnvVar, isRemoteEnvironment } from '@goodfoot/agent-hooks/claude-code';
12
+ *
13
+ * // Get project directory
14
+ * const projectDir = getProjectDir();
15
+ *
16
+ * // Check if running remotely
17
+ * if (isRemoteEnvironment()) {
18
+ * // Handle remote-specific logic
19
+ * }
20
+ *
21
+ * // In SessionStart hook: persist environment variables
22
+ * persistEnvVar('NODE_ENV', 'production');
23
+ * persistEnvVar('API_KEY', 'secret-key');
24
+ * ```
25
+ * @see https://code.claude.com/docs/en/hooks#hook-execution-details
26
+ */
27
+ export { CLAUDE_ENV_VARS, getEnvFilePath, getProjectDir, isRemoteEnvironment, persistEnvVar, persistEnvVars, } from "../../core/env.js";
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Claude Code hook event names and the advisory fail-open allow-list.
3
+ *
4
+ * `HOOK_EVENT_NAMES` is copied verbatim from `claude-code-hooks/src/types.ts`
5
+ * (all 30 events, same order). The advisory allow-list is **not** authored
6
+ * here: per the plan's derivation rule it is transcribed from CARD.md:75 —
7
+ * quoted below above `EXCLUDED_FROM_ADVISORY` — and `ADVISORY_EVENTS` is
8
+ * derived mechanically as "every event except the exclusion set". The unit
9
+ * test in `tests/agents/claude-code/` transcribes the same sentence a second
10
+ * time, independently, so the runtime list and the test oracle are two
11
+ * copies of the citation rather than one list read twice.
12
+ * @module
13
+ */
14
+ /**
15
+ * All Claude Code hook event names as a readonly array.
16
+ *
17
+ * Useful for iteration and validation.
18
+ * @example
19
+ * ```typescript
20
+ * for (const eventName of HOOK_EVENT_NAMES) {
21
+ * console.log(`Supported hook: ${eventName}`);
22
+ * }
23
+ * ```
24
+ */
25
+ export const HOOK_EVENT_NAMES = [
26
+ "PreToolUse",
27
+ "PostToolUse",
28
+ "PostToolUseFailure",
29
+ "PostToolBatch",
30
+ "Notification",
31
+ "UserPromptExpansion",
32
+ "UserPromptSubmit",
33
+ "SessionStart",
34
+ "SessionEnd",
35
+ "Stop",
36
+ "StopFailure",
37
+ "SubagentStart",
38
+ "SubagentStop",
39
+ "PreCompact",
40
+ "PostCompact",
41
+ "PermissionRequest",
42
+ "PermissionDenied",
43
+ "Setup",
44
+ "TeammateIdle",
45
+ "TaskCreated",
46
+ "TaskCompleted",
47
+ "Elicitation",
48
+ "ElicitationResult",
49
+ "ConfigChange",
50
+ "InstructionsLoaded",
51
+ "WorktreeCreate",
52
+ "WorktreeRemove",
53
+ "CwdChanged",
54
+ "FileChanged",
55
+ "MessageDisplay",
56
+ ];
57
+ /**
58
+ * Events never allowed the fail-open `unexpectedError: "continue"` policy.
59
+ *
60
+ * Transcribed verbatim from CARD.md:75:
61
+ *
62
+ * > `unexpectedError: "continue"` is rejected at the type level and at
63
+ * > runtime for any event not on that agent's advisory list (never
64
+ * > `PreToolUse`, `PermissionRequest`, blocking `Stop`/`SubagentStop`,
65
+ * > `WorktreeCreate`/`WorktreeRemove` on Claude Code; ...).
66
+ *
67
+ * The first five refuse because a swallowed failure silently grants the very
68
+ * decision the hook was supposed to make; the worktree pair additionally
69
+ * speaks a plain-text stdout protocol with no safe generic fallback, so a
70
+ * swallowed failure would write literal `{}` where the host expects a path.
71
+ */
72
+ export const EXCLUDED_FROM_ADVISORY = [
73
+ "PreToolUse",
74
+ "PermissionRequest",
75
+ "Stop",
76
+ "SubagentStop",
77
+ "WorktreeCreate",
78
+ "WorktreeRemove",
79
+ ];
80
+ /**
81
+ * The advisory allow-list: every Claude Code event except
82
+ * {@link EXCLUDED_FROM_ADVISORY}. Only these events accept
83
+ * `unexpectedError: "continue"` — enforced at factory-call time by the
84
+ * policy gate injected into {@link ../core/define-hook.ts|defineHook} and at
85
+ * compile time by the excluded events' narrowed config types.
86
+ */
87
+ export const ADVISORY_EVENTS = HOOK_EVENT_NAMES.filter((eventName) => !EXCLUDED_FROM_ADVISORY.includes(eventName));