Runtime Hook Surfaces — D1 Research Matrix

Date of survey: 2026-04-18 Hard cap: 60 minutes (Pod 8 flush) Method: Empirical inspection of local config dirs (~/.{claude,codex,gemini, cursor,windsurf,openclaw}/), runtime binaries (--version), installed hook scripts, and existing state/bin/sync-runtimes.ts + .claude/hooks/ contents. Runtimes not installed on this machine are documented from public sources with URL citation.

This matrix is the input for D2 (per-runtime adapters) and D3 (parity test harness). Runtimes outside parity get a measured number, not a hand-wave.


Summary table (runtime × mode × hook events × install path)

RuntimeInstalledModeHook events exposedInstall path (config)Wiring status
Claude Code 2.1.114yesagenticUserPromptSubmit, PreToolUse, PostToolUse, Stop, SessionStart, SessionEnd~/.claude/settings.json + ~/.claude/hooks/*.shreference impl — wired via snappy-os-inject.sh on UserPromptSubmit + `PreToolUse:Task\Agent; push/pull on Stop/SessionStart`
Codex 0.121.0yesagenticSessionStart, UserPromptSubmit, Stop, PreToolUse:Bash, PostToolUse:Bash~/.codex/hooks.json + ~/.codex/hooks/*.shwiredSessionStart loads compact snappy-os context once; UserPromptSubmit is intentionally empty because Codex adds that context every turn; Stop uses the silent wrapper
Gemini CLI 0.37.1yescontext-onlyNONE (no hooks key in settings.json; no hooks dir under ~/.gemini/)~/.gemini/settings.json + GEMINI.md in cwdcontext-only — sync writes GEMINI.md; mention-trigger not possible
openclaw 2026.4.9 (0512059)yesagentic-via-pluginbefore_prompt_build, before_tool_call (plugin SDK events; not file-drop)~/.openclaw/plugins/<id>/index.ts + openclaw.json hooks.internal.entrieswiredsnappy-loader plugin installed at ~/.openclaw/plugins/snappy-loader/ reads ~/.claude/skills/ and injects
Cursorabsent on this machinecontext-only (per docs)NONE documented.cursorrules (project root) or ~/.cursor/rules (global)syncs .cursorrules; no execution hook per public docs
Windsurfabsent on this machinecontext-only (per docs)NONE documented.windsurfrules (project root) or global rules UIsyncs .windsurfrules; no execution hook per public docs

Mode definitions

commands. snappy-os-inject.sh (or an adapter) injects <skill-context> blocks per-turn. Parity metric: skill output scored against the eval gate.

plugin SDK with typed lifecycle events. Snappy plugin subscribes and injects. Same parity metric as agentic.

.cursorrules, .windsurfrules). snappy-os ships per-skill rules via the file's body but cannot mention-trigger per-turn. Parity metric: loader fidelity — does the skill's .agents.md body appear in next-turn context verbatim?

sources with URL cites.


1. Claude Code (reference implementation)

under ~/.claude/hooks/.

{ prompt, tool_input: { prompt }, ... }. Hook detects mode:

    { "hookSpecificOutput": { "hookEventName": "UserPromptSubmit", "additionalContext": "..." } }

plus permissionDecision: "allow".

hooks block, per-event entries wrapped as { "hooks": [ { "type": "command", "command": "...", "timeout": N } ] }. Bare { "command": "..." } entries at the event level silently reject the entire settings.json (killed byline 2026-04-17; see feedback_settings_hooks_must_be_wrapped.md).

skills.snappy.ai, caches 5 min under ~/.cache/snappy-os/, falls back to local repo.

<skill-context name="<name>"> blocks (this very session shows it).

runs 4 on Stop sequentially with no coordination — keep each idempotent.


2. Codex CLI

(confirmed present).

becomes developer context.

every prompt. suppressOutput is parsed but not implemented, so this must not be used for large snappy-os context in Codex.

That adapter runs cli.js pull --auto internally with stdout/stderr redirected to ~/.codex/logs/snappy-os-session-start.log, then emits one compact valid JSON additionalContext payload.

<snappy-os-system> / <skill-context> blocks.

skill-check-session.sh.

emits hookSpecificOutput.hookEventName = "SessionStart" with compact additionalContext pointing the agent at program.md, state/index.md, and the relevant state/skills/*.md files on disk.

{ "hooks": [ { "type": "command", "command": "..." } ] } per event. File is hand-edited JSON.

stale project-local prompt hook would also fire. Current scan found only ~/.codex/hooks.json on this machine.

Do not attempt to hide a prompt hook with it.

config. If one appears, point it at state/bin/statusline.sh.


3. Gemini CLI

mcpServers, security, model keys — no hooks key. ~/.gemini/extensions/ is empty. No ~/.gemini/hooks/ dir.

auto-reading GEMINI.md when Gemini CLI starts in a repo directory.

maintains this via state/bin/sync-runtimes.ts (GEMINI.md listed in TARGETS).

shows no hooks key.

Claude Code), but MCP is tool-surface not hook-surface — cannot inject per-turn context.

packaging format for MCP+context, not an execution-time hook API per current 0.37.1.


4. openclaw

~/.openclaw/openclaw.json.

mutates event.additionalContext.

event.toolName and event.args to rewrite sub-agent tasks.

event-reporter enabled) — internal telemetry hooks, separate surface.

Events are typed objects passed to api.on(eventName, handler). No JSON-over- stdin — plugin runs in-process.

  1. Write TypeScript plugin at ~/.openclaw/plugins/<id>/index.ts exporting

definePluginEntry({...}).

  1. Add manifest openclaw.plugin.json in same dir with id, entry,

capabilities, configSchema.

  1. Plugin auto-loaded by openclaw on startup.

session start, and scans sub-agent spawn tasks for snappy-* skill names via regex /snappy-[\w-]+/g.

convention). Does NOT yet read snappy-os's state/skills/*.agents.md per-turn loader format — a documented gap; openclaw plugin needs update to track the snappy-os convention.

event-reporter subscribes currently.

verified), but plugin is reading legacy kernel skill paths, not snappy-os paths.


5. Cursor

~/.cursor/rules/*.mdc for global rules (Cursor 0.45+).

session start. snappy-os maintains this via state/bin/sync-runtimes.ts.

is static context only.

installed.


6. Windsurf

global rules configured via Windsurf settings UI.

snappy-os maintains via state/bin/sync-runtimes.ts.

rules are static context.

installed.


What D2 / D3 / D4 consume from this matrix

  1. D2 (per-runtime adapters, state/lib/runtime.ts): the two agentic

runtimes (Claude, Codex) share one JSON-over-stdin hook contract; one adapter shape. openclaw needs a distinct plugin-SDK adapter. Three context-only runtimes share one file-drop adapter.

  1. D3 (state/lint/parity-test.ts): two parity metrics by mode:
  1. D4 (system-view TUI Parity panel): filter rows by mode before

averaging. Comparing an agentic score to a context-only score is category-mixing and the UI will not do it.


Honest unknowns post-D1

~/.claude/skills/<skill>/AGENTS.md (kernel convention) not state/skills/<name>.agents.md (snappy-os convention). D2 must update the plugin to point at snappy-os sidecars, or snappy-os must keep writing kernel- style AGENTS.md under ~/.claude/skills/ as a compatibility layer.

whether codex 0.118.0 emits PreToolUse at all for subagent spawns requires a test run. Assume yes for D2 design; verify in D3.

Parity rows for both will be null until a machine runs the test suite under those editors. Document as gap, not failure.

docs don't yet describe an extension hook API equivalent to Claude's hook events. Re-check each Gemini CLI release.