@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
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));
|