wish(plan): hookify third-party absorption — daemon as the only CC hook entry - #1489
Conversation
…ery #2) Crystallized brainstorm + scaffolded wish for delivery #2 of the genie hookify umbrella (delivery #1 shipped as PR #1485). Delivery #2 makes genie the only Claude Code hook entry: absorbs existing foreign hooks (Token Optimizer, ultratoken, future plugins) via a one-time settings.json rewrite + subprocess-passthrough handlers, and lets operators deploy custom hooks (e.g. rlmx run on every Bash) as plain TS code in .genie/hooks/. Three-tier scoping (per-team > per-repo > global), trust- allowlisted, with reload + test for inner-loop iteration. Council reviewed (sentinel + architect + ergonomist + operator) — all four push-backs folded into the design + wish: - Sentinel: trust allowlist required (filesystem presence is not consent), versioned absorb snapshots, two-mode env capture (probe + offline) with denylist, threat model documented (single-operator machine). - Architect: Handler interface gains version/source/manifest_path discriminated union; registry migrates const handlers to let registryRef ReadonlyArray Handler; loud shadowing instead of silent. - Ergonomist: defineHook() config-object scaffold; genie hook reload + test ship with this delivery; rename absorb to import; broken hooks loud in list. - Operator: versioned snapshots last 10; per-team archive lifecycle; 5 ms passthrough is a measured SLO with bench + alert template. Plan-review fix-loop 1 closed all 6 reviewer gaps (trust threat model, probe offline fallback, registry mutation contract, per-team archive lifecycle, Handler vNext strategy, broken-hook recovery). WISH.md plan-reviewed SHIP. 4 execution groups, 3 waves: - Wave 1: G1 foundation (registry + Handler v1 + loader + trust gate) - Wave 2 parallel: G2 operator inner loop + G3 foreign-hook absorption - Wave 3: G4 lifecycle wiring + telemetry/microbench + docs + delivery report Files: - .genie/brainstorms/hookify-third-party-absorption/DRAFT.md - .genie/brainstorms/hookify-third-party-absorption/DESIGN.md - .genie/wishes/hookify-third-party-absorption/WISH.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…PU follow-ups Three Mac CPU follow-up PRs landed on dev after delivery #1 merged (PR #1475 retention, #1476 GENIE_SKIP_DB_BOOT, #1481 narrowed matchers). Wish plan shape unchanged but two implementation realities now apply: 1. DISPATCHED_EVENTS renamed to DISPATCHED_EVENT_MATCHERS in src/hooks/types.ts and shrunk from 6 events to 2 (PreToolUse plus PostToolUse:SendMessage). Group 3's import logic must consult the new constant; foreign hooks on un-wired events (PreCompact, SessionStart, etc.) cannot be absorbed and must be left in place with explicit reporting in --dry-run. 2. Tools that invoke hook code outside genie serve set GENIE_SKIP_DB_BOOT=1 to mirror the bun-fallback path's behavior (genie hook test, scaffold validation). Added [left-in-place] reporting requirement to import --dry-run plus a new acceptance criterion covering the three-status output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces the design and execution plan for the 'Hookify Third-Party Absorption' feature, which enables Genie to act as a universal multiplexing layer for Claude Code hooks. The proposed system includes a trust-gated loader for TypeScript-based hooks, a three-tier scoping model (team, repo, and global), and a migration path for existing third-party hooks via subprocess-passthrough handlers. The review feedback identifies a recurring typo referring to 'S3 tiers' instead of 'three tiers' and points out critical implementation risks, specifically the potential for long-running top-level awaits to block the daemon and the risk of filename collisions in the flat quarantine directory for broken hooks.
| ## Scope | ||
|
|
||
| ### IN | ||
| 1. **Trust-gated `.genie/hooks/` loader** — daemon scans three S3 tiers at boot, refuses to load any `.ts` file not present in `~/.genie/hooks/trusted.json` (path → SHA-256 + scope), dynamic-`import()`s trusted modules, validates exports, registers them in the dispatch chain. |
There was a problem hiding this comment.
The term "S3 tiers" is used here and in several other places (lines 18, 63, 84). This appears to be a typo for "3 tiers" or "three tiers", referring to the three-tier scoping model (team, repo, global). Using "S3" is confusing as it is the standard name for Amazon's storage service.
| 1. **Trust-gated `.genie/hooks/` loader** — daemon scans three S3 tiers at boot, refuses to load any `.ts` file not present in `~/.genie/hooks/trusted.json` (path → SHA-256 + scope), dynamic-`import()`s trusted modules, validates exports, registers them in the dispatch chain. | |
| 1. **Trust-gated `.genie/hooks/` loader** — daemon scans three tiers at boot, refuses to load any `.ts` file not present in `~/.genie/hooks/trusted.json` (path → SHA-256 + scope), dynamic-`import()`s trusted modules, validates exports, registers them in the dispatch chain. |
| ### IN | ||
| 1. **Trust-gated `.genie/hooks/` loader** — daemon scans three S3 tiers at boot, refuses to load any `.ts` file not present in `~/.genie/hooks/trusted.json` (path → SHA-256 + scope), dynamic-`import()`s trusted modules, validates exports, registers them in the dispatch chain. | ||
| 2. **Three-tier scoping with explicit precedence** — `~/.claude/teams/<team>/hooks/*.ts` (per-team) > `<repo>/.genie/hooks/*.ts` (per-repo) > `~/.genie/hooks/*.ts` (global). Same-`name` collisions emit `console.warn` listing both source paths and surface in `genie hook list` as `[shadowed by <path>]`. `genie serve --strict-hooks` refuses to start on any collision. | ||
| 3. **External `Handler` interface contract** — `src/hooks/types.ts` `Handler` extends with `version: '1'`, `source: 'builtin' | 'repo' | 'team' | 'global' | 'absorbed'`, `manifest_path: string`. Loader validates the export shape before registering; partial registration is forbidden. In-process invariants documented as a hard contract: no `process.exit`, no `process.env` writes, no top-level await > 100 ms, no direct `pg` imports (handlers receive a daemon-supplied `Context`). |
There was a problem hiding this comment.
The design specifies a contract forbidding top-level await > 100ms, but it does not mention an enforcement mechanism in the loader. Since dynamic-import() is used, a hook with a long-running top-level await could block the daemon's boot or reload process indefinitely. Consider wrapping the dynamic import in a timeout or implementing a mechanism to detect and skip hooks that exceed this limit during the loading phase to ensure daemon stability.
| 8. **`genie hook reload`** — SIGHUP-style command that re-runs the boot scan, rebuilds the registry, and atomically replaces the live reference (in-flight dispatches finish on the old reference). Pairs with frozen-by-construction registry: `let handlers: ReadonlyArray<Handler>` populated once before `server.listen()`. | ||
| 9. **`genie hook import --from claude-settings`** (renamed from `absorb`) — `--dry-run` (default) emits a settings.json diff + the list of generated `_absorbed/*.ts` files + a checksum of the proposed final state. `--apply` writes a versioned snapshot at `~/.genie/hooks/_absorbed/snapshots/<ISO>-<sha256>.json` (keep last 10, `latest` symlink, atomic GC), an audit-log entry at `~/.genie/audit/import.jsonl`, and rewrites settings. Refuses if already-imported (idempotency check). Subprocess-passthrough handlers capture the full CC environment via **two-mode capture**: (a) preferred — if a CC session is live, daemon spawns a probe hook through CC and captures `env` + `pwd` + stdin verbatim; (b) offline fallback — when no CC session is reachable, capture from the daemon's parent shell environment (`process.env`), `pwd` from the resolved repo root, and stdin shape from a known-good fixture stored in `~/.genie/hooks/_absorbed/probes/`. Both modes apply the same denylist for sensitive vars (`GENIE_*`, `ANTHROPIC_API_KEY`, etc.). The captured env always includes at minimum `CLAUDE_PLUGIN_ROOT`, `CLAUDE_PROJECT_DIR`, `CLAUDE_SESSION_ID` (synthetic UUID in offline mode), `LANG`, `LC_ALL`, `PATH`, `HOME`, `USER`, `TMPDIR`. If both capture modes fail (e.g., `process.env.CLAUDE_PLUGIN_ROOT` missing in offline mode and no live CC session), `--apply` halts with a remediation hint: "Start a Claude Code session and retry, or use `--probe-fixture <path>` to point at a saved probe capture." `--apply --offline-only` skips the live probe attempt; `--apply --probe-fixture <path>` uses a hand-curated capture. | ||
| 10. **`genie hook import --eject`** — verifies the current settings.json checksum matches the post-apply hash; refuses without `--force` if drift detected; restores the original from the latest snapshot. | ||
| 11. **Quarantine for broken hook files** — daemon startup ALWAYS succeeds even when hook files are broken; the loader catches parse/validation errors per-file, moves the offending file to `_quarantine/<basename>` with a sidecar `<basename>.error` containing the parse error + line, and continues registering the other hooks. `genie hook list` shows quarantined files as `[BROKEN]` rows with file path + first error line + remediation hint (`fix the file then run 'genie hook reload'`). `genie doctor --hooks` enumerates loaded / skipped / quarantined. `genie hook quarantine --revert <name>` moves a file back from `_quarantine/` to its original tier (after the operator fixes it on disk) and triggers a re-validation pass. |
There was a problem hiding this comment.
Moving broken hook files to a flat _quarantine/<basename> directory introduces a collision risk. Since hooks can exist in three different tiers (per-team, per-repo, global), multiple files might share the same basename (e.g., init.ts). If two such files are broken, one will overwrite the other in the quarantine directory, leading to data loss and incorrect error reporting. The quarantine path should incorporate the source tier or a hash of the original path to ensure uniqueness.
|
|
||
| ### IN | ||
|
|
||
| - Trust-gated `.genie/hooks/` loader scanning three S3 tiers (`~/.claude/teams/<team>/hooks/`, `<repo>/.genie/hooks/`, `~/.genie/hooks/`) at boot, refusing any `.ts` not in `~/.genie/hooks/trusted.json` (path → SHA-256 + scope), dynamic-`import()`ing trusted modules, validating exports, registering them in the dispatch chain. |
There was a problem hiding this comment.
The term "S3 tiers" appears to be a typo for "3 tiers", consistent with the three-tier scoping model described in the design.
| - Trust-gated `.genie/hooks/` loader scanning three S3 tiers (`~/.claude/teams/<team>/hooks/`, `<repo>/.genie/hooks/`, `~/.genie/hooks/`) at boot, refusing any `.ts` not in `~/.genie/hooks/trusted.json` (path → SHA-256 + scope), dynamic-`import()`ing trusted modules, validating exports, registering them in the dispatch chain. | |
| - Trust-gated `.genie/hooks/` loader scanning three tiers (`~/.claude/teams/<team>/hooks/`, `<repo>/.genie/hooks/`, `~/.genie/hooks/`) at boot, refusing any `.ts` not in `~/.genie/hooks/trusted.json` (path → SHA-256 + scope), dynamic-`import()`ing trusted modules, validating exports, registering them in the dispatch chain. |
| - `genie hook list` — debug surface showing discovered + trusted + loaded hooks per scope; annotates same-`name` shadowing as `[shadowed by <path>]`, broken imports as `[BROKEN]`, stale absorbed targets as `[STALE]`; runs `which` on each absorbed hook's `run` command at list time. | ||
| - `genie hook test <name> --payload <fixture.json>` — runs a single hook against a recorded payload without daemon restart. | ||
| - `genie hook reload` — SIGHUP-style command that re-runs the boot scan, rebuilds the registry, atomically replaces the live reference (in-flight dispatches finish on the old reference). Single-writer guarantee enforced at the CLI level. | ||
| - Quarantine for broken hook files: daemon startup ALWAYS succeeds; loader catches parse/validation errors per-file, moves the offender to `_quarantine/<basename>` with sidecar `<basename>.error`, continues registering other hooks. `genie hook list` shows `[BROKEN]` rows. `genie hook quarantine --revert <name>` moves a file back from `_quarantine/` and re-validates. |
There was a problem hiding this comment.
The quarantine logic for broken hook files (_quarantine/<basename>) is susceptible to filename collisions across different tiers. If multiple tiers contain a broken file with the same name, they will overwrite each other in the quarantine directory. Consider using a unique naming scheme for quarantined files that preserves their origin context.
Summary
Plan-only PR for delivery #2 of the genie hookify umbrella. Adds the brainstorm + wish artifacts for `hookify-third-party-absorption`. No source code changes — the WISH is the contract, the next PR will execute it.
Delivery #1 (`hookify-perf-foundation`, PR #1485) shipped the daemon dispatcher + native client + perf telemetry view. Three Mac CPU follow-ups (#1475, #1476, #1481) landed afterward and are reflected in this wish's "Post-merge delta" note.
What this delivery does
Make `genie` the only Claude Code hook entry. Absorb existing foreign hooks (Token Optimizer, ultratoken, etc.) into genie's daemon dispatch via a one-time `
/.claude/settings.json` rewrite + subprocess-passthrough handlers. Let operators deploy custom hooks (e.g. `rlmx run` on every Bash) as plain TS code in `.genie/hooks/`. Three-tier scoping (per-team > per-repo > global), trust-allowlisted via `/.genie/hooks/trusted.json`, with `genie hook reload` + `test` for inner-loop iteration.Files in this PR
Council-folded changes vs the original sketch
Round 1 deliberation with sentinel + architect + ergonomist + operator surfaced six gaps that are now baked into Decisions + Success Criteria:
Plan-review fix-loop 1 closed three additional gaps (trust threat model documentation, probe offline-fallback story, registry mutation contract clarity).
Execution plan (4 groups, 3 waves)
Both Wave 2 groups depend only on G1; the file lists are disjoint. Total: ~4 agents over 14–18 calendar days. Properly scoped for `genie team create`.
Out of scope (deferred to follow-up wishes)
Test plan
This is a plan-only PR — no code changes to test. Validation:
🤖 Generated with Claude Code