Skip to content

Commit efc87e6

Browse files
jonathanKingstoncursoragentgithub-actions[bot]
authored
G3: vendored hook schemas + CI drift detector (#947)
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> ## Summary Implements **G3** from the hooks plan: vendored Claude SchemaStore + Cursor community hook schemas; warn-level authoring lint only; CI drift detector vs adapter-known events. **Stacked on #946 (G2) → #945 (G1) → … → #913 (A1).** ## Acceptance criteria (from plan G3 row) - Pin Claude SchemaStore + Cursor community schemas - Warn-level authoring lint only, never a load gate, never remote-fetched - CI test diffs published event lists vs adapter-known events ## Deliverables - `schemas/vendor/` with provenance README - `vendor-schema-drift.test.ts` - Claude unknown/unsupported event warnings (parity with Cursor) ## Tests - `npm run check` green (2965 tests) ## Test plan - [x] `npm run check` - [ ] CI green <!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-8caab6bd-d95d-439e-a65b-bd4c12d04399"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/background-agent?bcId=bc-8caab6bd-d95d-439e-a65b-bd4c12d04399"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 5b80c1c commit efc87e6

11 files changed

Lines changed: 4367 additions & 36 deletions

‎.prettierignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ tests/e2e/electron-shell/.e2e-env.json
1010
tests/fixtures/git-changes-repo/
1111
# E2E markdown fixture with intentional hard-break trailing spaces + &nbsp;.
1212
tests/fixtures/job-description-metadata.md
13+
# Vendored upstream hook schemas (G3) — pinned byte-identical to upstream; never
14+
# reformatted so the drift detector compares against exactly what was published.
15+
schemas/vendor/*.json

‎docs/cursor-hooks.md‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,32 @@ This is the same trust boundary described in
246246
[`docs/supply-chain-security.md`](./supply-chain-security.md): trusting a workspace means
247247
trusting the code it can cause to run.
248248

249+
## Vendored upstream schemas & drift detection (G3)
250+
251+
Copse pins committed copies of the upstream hook-config JSON schemas for both
252+
foreign dialects under [`schemas/vendor/`](../schemas/vendor/) —
253+
`claude-code-settings.schema.json` (Claude Code, from SchemaStore) and
254+
`cursor-hooks.schema.json` (the community `cursor-hooks` npm schema). See
255+
[`schemas/vendor/README.md`](../schemas/vendor/README.md) for provenance, pins,
256+
and the re-vendoring steps.
257+
258+
These exist for exactly two purposes, and are subject to two hard rules — they
259+
are **never fetched over the network** at runtime or in CI, and they are **never
260+
a load gate** (a config that violates an upstream schema still loads):
261+
262+
1. **Warn-level authoring lint.** Parsing a foreign config uses the schema's
263+
published event list to warn when a hooks group targets an event the vendor
264+
recognises but Copse does not act on yet (vs an outright typo). The valid
265+
hooks still load; the warning surfaces in Settings → Sources.
266+
2. **CI drift detector** (`src/main/services/hooks/vendor-schema-drift.test.ts`)
267+
diffs each vendored schema's published events against the events our adapters
268+
wire. Every published event must be either wired or listed in an explicit
269+
intentionally-unsupported set (`src/shared/hooks/vendored-hook-schemas.ts`); an
270+
upstream release adding an unaccounted event fails CI until it is wired or
271+
documented. Copse currently wires Claude `PreToolUse` + `SessionStart` and the
272+
Cursor events above; the long tail of Claude events (`Notification`,
273+
`TeammateIdle`, …) is intentionally-unsupported v1.
274+
249275
## Gaps and future work
250276

251277
1. **Content rewriting** — `updated_input` on tool gates (rewrite the proposed tool
@@ -272,6 +298,9 @@ trusting the code it can cause to run.
272298
- `src/main/services/security/permission-gate.ts` — calls the tool-gate hooks
273299
- `src/shared/types/cursor-hooks.ts` — `CursorHookEvent` / `CursorHookSummary`
274300
- `src/shared/types/hooks.ts` — shared `HookSummary` for Sources / `hooks:list`
301+
- `src/shared/hooks/vendored-hook-schemas.ts` — published-event mirrors + intentionally-unsupported sets (G3)
302+
- `schemas/vendor/` — pinned upstream Cursor + Claude hook schemas (G3); see its `README.md`
303+
- `src/main/services/hooks/vendor-schema-drift.test.ts` — CI drift detector (G3)
275304
- `src/main/services/exec/child-process-env.ts` — secret-scrubbed env for hook processes
276305
- `docs/claude-hooks.md` — Claude Code hooks contract
277306
- `docs/cursor-plugins.md` — sibling exploration of Cursor plugin support

‎docs/plans/hooks-and-feature-packs.md‎

Lines changed: 7 additions & 7 deletions
Large diffs are not rendered by default.

‎schemas/vendor/README.md‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Vendored upstream hook schemas (G3)
2+
3+
Pinned, committed copies of the **upstream** hook-config JSON schemas for the two
4+
foreign dialects Copse imports (`.claude/settings.json` and `.cursor/hooks.json`).
5+
They exist for two reasons, and **only** two:
6+
7+
1. **Warn-level authoring lint.** When Copse parses a foreign hooks config it uses
8+
the _published event list_ from these schemas to tell an author "this event is
9+
recognised upstream but Copse does not act on it yet" vs "this looks like a
10+
typo". This is a **warn-only** lint surfaced in Settings → Sources — it is
11+
**never a load gate**: every valid hook still loads, exactly as before.
12+
2. **CI drift detector.** `src/main/services/hooks/vendor-schema-drift.test.ts`
13+
diffs the event list each vendored schema publishes against the events our
14+
adapters actually wire, and fails CI when an upstream release adds an event we
15+
neither support nor have explicitly listed as intentionally-unsupported. That
16+
forces a deliberate choice (wire it, or document why not) instead of silent
17+
drift.
18+
19+
## Hard rules (do not break)
20+
21+
- **Never remote-fetched.** Neither the runtime app nor CI fetches these at
22+
runtime or test time — the drift detector reads the committed copies from disk
23+
only. The network is used **exactly once**, by a human/agent re-vendoring a pin
24+
(see below). Tests and the app must work fully offline.
25+
- **Never a load gate.** These schemas do not validate/reject a user's hooks
26+
config. A config that violates an upstream schema still loads; at most it earns
27+
a warning row. Copse's own `schemas/copse-hooks.schema.json` is the only schema
28+
we author; these vendored ones are read-only references.
29+
- **Pinned, not tracked live.** We pin a specific upstream revision so the drift
30+
detector is deterministic. Bumping a pin is an explicit, reviewed change.
31+
32+
## Pins
33+
34+
| File | Upstream | Source URL | Pinned | Retrieved | sha256 |
35+
| ---------------------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------- | -------------------------------------- | ---------- | ------------------------------------------------------------------ |
36+
| `claude-code-settings.schema.json` | Claude Code settings (SchemaStore) | <https://www.schemastore.org/claude-code-settings.json> | SchemaStore `last-modified` 2026-07-15 | 2026-07-16 | `2b4004b2af619ce16bd6dafc0a8f1f03974f45740f4212a1f85f236364057d28` |
37+
| `cursor-hooks.schema.json` | `cursor-hooks` npm package (community, "Cursor Community" / johnlindquist) | <https://unpkg.com/cursor-hooks@1.1.5/schema/hooks.schema.json> | npm `cursor-hooks@1.1.5` | 2026-07-16 | `824c8bbe802305827f813c19f2411b883afe8d02f0b947d7c30428f4effc18f3` |
38+
39+
Notes on each pin:
40+
41+
- **Claude — SchemaStore.** SchemaStore's `claude-code-settings.json` is the
42+
canonical community schema for `.claude/settings.json`. Its `hooks` object
43+
enumerates the full upstream event surface (30 events as of this pin), of which
44+
Copse wires `PreToolUse` and `SessionStart`; the rest are intentionally
45+
unsupported v1 (see the drift test's `CLAUDE_INTENTIONALLY_UNSUPPORTED_EVENTS`).
46+
- **Cursor — community schema.** Cursor does not publish a first-party
47+
`hooks.json` JSON schema on SchemaStore. The most widely used community schema
48+
is the one shipped by the `cursor-hooks` npm package (referenced from the Cursor
49+
hooks docs' "enable JSON Schema validation" section). It lags the Cursor docs
50+
(it publishes 6 events; Cursor's docs describe more), which is expected for a
51+
community pin — our adapter deliberately knows a **superset**. The drift detector
52+
only requires that every event this schema _publishes_ is either wired or listed
53+
as intentionally-unsupported; adapter-ahead-of-schema is allowed.
54+
55+
## Re-vendoring a pin
56+
57+
This is the **only** step that touches the network, and it is run by a human/agent
58+
on purpose — never by the app or CI:
59+
60+
```bash
61+
# Claude (SchemaStore):
62+
curl -sSL -o schemas/vendor/claude-code-settings.schema.json \
63+
https://www.schemastore.org/claude-code-settings.json
64+
65+
# Cursor (community, bump the pinned version deliberately):
66+
curl -sSL -o schemas/vendor/cursor-hooks.schema.json \
67+
https://unpkg.com/cursor-hooks@<version>/schema/hooks.schema.json
68+
69+
# Then update the table above (sha256 via `sha256sum`, dates, versions) and
70+
# re-run the drift detector; if it fails, either wire the new event(s) or add
71+
# them to the intentionally-unsupported list with a reason.
72+
npm test -- --test-name-pattern "vendor-schema-drift"
73+
```
74+
75+
See `docs/plans/hooks-and-feature-packs.md` (issue G3) for the design decision.

0 commit comments

Comments
 (0)