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)
| Runtime | Installed | Mode | Hook events exposed | Install path (config) | Wiring status | |
|---|---|---|---|---|---|---|
| Claude Code 2.1.114 | yes | agentic | UserPromptSubmit, PreToolUse, PostToolUse, Stop, SessionStart, SessionEnd | ~/.claude/settings.json + ~/.claude/hooks/*.sh | reference impl — wired via snappy-os-inject.sh on UserPromptSubmit + `PreToolUse:Task\ | Agent; push/pull on Stop/SessionStart` |
| Codex 0.121.0 | yes | agentic | SessionStart, UserPromptSubmit, Stop, PreToolUse:Bash, PostToolUse:Bash | ~/.codex/hooks.json + ~/.codex/hooks/*.sh | wired — SessionStart 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.1 | yes | context-only | NONE (no hooks key in settings.json; no hooks dir under ~/.gemini/) | ~/.gemini/settings.json + GEMINI.md in cwd | context-only — sync writes GEMINI.md; mention-trigger not possible | |
| openclaw 2026.4.9 (0512059) | yes | agentic-via-plugin | before_prompt_build, before_tool_call (plugin SDK events; not file-drop) | ~/.openclaw/plugins/<id>/index.ts + openclaw.json hooks.internal.entries | wired — snappy-loader plugin installed at ~/.openclaw/plugins/snappy-loader/ reads ~/.claude/skills/ and injects | |
| Cursor | absent on this machine | context-only (per docs) | NONE documented | .cursorrules (project root) or ~/.cursor/rules (global) | syncs .cursorrules; no execution hook per public docs | |
| Windsurf | absent on this machine | context-only (per docs) | NONE documented | .windsurfrules (project root) or global rules UI | syncs .windsurfrules; no execution hook per public docs |
Mode definitions
agentic— runtime exposes execution-time hooks that fire arbitrary shell
commands. snappy-os-inject.sh (or an adapter) injects <skill-context> blocks per-turn. Parity metric: skill output scored against the eval gate.
agentic-via-plugin— runtime has no raw shell-hook file surface, but a
plugin SDK with typed lifecycle events. Snappy plugin subscribes and injects. Same parity metric as agentic.
context-only— runtime accepts only static context files (GEMINI.md,
.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?
absent— runtime not installed on this machine. Documented from public
sources with URL cites.
unknown— researcher hit the 60-min cap before confirming surface.
1. Claude Code (reference implementation)
- Install path:
~/.claude/settings.json(merged user + local); hook scripts
under ~/.claude/hooks/.
- Version observed:
2.1.114 (Claude Code)viaclaude --version. - Hook events present (confirmed empirically from live
~/.claude/settings.json): UserPromptSubmit— fires on every user promptPreToolUse(matcherTask|Agent) — fires before subagent dispatchPostToolUse(matcherEdit|Write) — fires after writesStop— end-of-turn (4 entries: autopilot, sync-runtimes, cli.js push, auto-regen-skills)SessionStart— start of new session (runscli.js pull --auto)SessionEnd(documented; not currently wired here)- Payload format (stdin → hook): JSON via stdin with shape
{ prompt, tool_input: { prompt }, ... }. Hook detects mode:
task_prompt=.tool_input.prompt(PreToolUse on Task/Agent)user_prompt=.prompt(UserPromptSubmit)- For
UserPromptSubmit, emit on stdout:
{ "hookSpecificOutput": { "hookEventName": "UserPromptSubmit", "additionalContext": "..." } }
- For
PreToolUse, emitupdatedInputwith a modifiedtool_input.prompt
plus permissionDecision: "allow".
- Installation mechanism: hook script path in
~/.claude/settings.json's
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).
- Empirical notes:
- Tested:
snappy-os-inject.shfetches program.md + state/index.md from
skills.snappy.ai, caches 5 min under ~/.cache/snappy-os/, falls back to local repo.
- Observed: loaders for matched skills are injected inside
<skill-context name="<name>"> blocks (this very session shows it).
- Rate-limit:
Stophandler must tolerate multiple entries firing; snappy-os
runs 4 on Stop sequentially with no coordination — keep each idempotent.
- Mode:
agentic(reference implementation for all other runtimes).
2. Codex CLI
- Install path:
~/.codex/hooks.json+~/.codex/hooks/*.sh. - Version observed:
codex-cli 0.121.0viacodex --version. - Feature flag:
[features] codex_hooks = truein~/.codex/config.toml
(confirmed present).
- Hook events present (from OpenAI docs, checked 2026-04-18):
SessionStart— matcher filtersstartup|resume; stdout/additionalContext
becomes developer context.
UserPromptSubmit— matcher ignored; stdout/additionalContext is added on
every prompt. suppressOutput is parsed but not implemented, so this must not be used for large snappy-os context in Codex.
PreToolUse/PostToolUse— currently Bash-only in Codex.Stop— end-of-turn, matcher ignored.- Live wiring (
~/.codex/hooks.json): SessionStartruns onlystate/bin/codex-session-start-context.sh.
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.
UserPromptSubmitis intentionally empty to prevent visible per-prompt
<snappy-os-system> / <skill-context> blocks.
Stoprunsstate/bin/codex-stop-hook.shplus the quiet legacy
skill-check-session.sh.
- Payload format: JSON-over-stdin. For SessionStart, the snappy-os adapter
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.
- Installation mechanism: same wrapping rule as Claude —
{ "hooks": [ { "type": "command", "command": "..." } ] } per event. File is hand-edited JSON.
- Empirical notes:
- Codex discovers all
hooks.jsonfiles next to active config layers, so a
stale project-local prompt hook would also fire. Current scan found only ~/.codex/hooks.json on this machine.
- OpenAI documents
suppressOutputfor hooks as parsed but not implemented.
Do not attempt to hide a prompt hook with it.
- Codex has no observed native
statusLine/ byline hook in docs or local
config. If one appears, point it at state/bin/statusline.sh.
- Mode:
agentic(confirmed — CLAUDE.md claim of parallel impl verified).
3. Gemini CLI
- Install path:
~/.gemini/settings.json+GEMINI.mdin repo cwd. - Version observed:
0.37.1viagemini --version. - Hook events present: NONE.
~/.gemini/settings.jsonhas only
mcpServers, security, model keys — no hooks key. ~/.gemini/extensions/ is empty. No ~/.gemini/hooks/ dir.
- Payload format: N/A — no runtime hook surface. Context arrives by
auto-reading GEMINI.md when Gemini CLI starts in a repo directory.
- Installation mechanism: drop
GEMINI.mdat repo root. snappy-os already
maintains this via state/bin/sync-runtimes.ts (GEMINI.md listed in TARGETS).
- Empirical notes:
- Tested:
ls ~/.gemini/shows no hooks dir;cat ~/.gemini/settings.json
shows no hooks key.
- Gemini CLI supports MCP servers (confirmed: same exa + notion entries as
Claude Code), but MCP is tool-surface not hook-surface — cannot inject per-turn context.
- Extension system (
~/.gemini/extensions/) exists but empty and is the
packaging format for MCP+context, not an execution-time hook API per current 0.37.1.
- Mode:
context-only— confirmed (CLAUDE.md claim verified).
4. openclaw
- Install path:
~/.openclaw/plugins/<id>/index.ts+
~/.openclaw/openclaw.json.
- Version observed:
OpenClaw 2026.4.9 (0512059)viaopenclaw --version. - Hook events present (via plugin SDK, not shell hooks):
before_prompt_build— fires before every user prompt is built; plugin
mutates event.additionalContext.
before_tool_call— fires before any tool invocation; plugin reads
event.toolName and event.args to rewrite sub-agent tasks.
openclaw.jsonalso exposeshooks.internal.entries(currently only
event-reporter enabled) — internal telemetry hooks, separate surface.
- Payload format: TypeScript plugin SDK (
openclaw/plugin-sdk/plugin-entry).
Events are typed objects passed to api.on(eventName, handler). No JSON-over- stdin — plugin runs in-process.
- Installation mechanism:
- Write TypeScript plugin at
~/.openclaw/plugins/<id>/index.tsexporting
definePluginEntry({...}).
- Add manifest
openclaw.plugin.jsonin same dir withid,entry,
capabilities, configSchema.
- Plugin auto-loaded by openclaw on startup.
- Empirical notes:
- Confirmed file:
~/.openclaw/plugins/snappy-loader/index.ts(4.4KB) exists. - Behavior: loads
~/.claude/hooks/always-inject.txt(legacy kernel file) at
session start, and scans sub-agent spawn tasks for snappy-* skill names via regex /snappy-[\w-]+/g.
- Reads AGENTS.md from
~/.claude/skills/<skill>/AGENTS.md(kernel
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.
openclaw.jsonhooks.internal.enabled = true; only bundled
event-reporter subscribes currently.
- Mode:
agentic-via-plugin— confirmed (CLAUDE.md claim of plugin-wired
verified), but plugin is reading legacy kernel skill paths, not snappy-os paths.
5. Cursor
- Installed on this machine: NO (
~/.cursordoes not exist). - Install path (per public docs):
.cursorrulesat project root, or
~/.cursor/rules/*.mdc for global rules (Cursor 0.45+).
- Hook events present: NONE documented in public docs as of 2026-04-18.
- Payload format: N/A — context-only.
- Installation mechanism: drop
.cursorrulesin repo. Cursor auto-reads on
session start. snappy-os maintains this via state/bin/sync-runtimes.ts.
- Empirical notes: N/A on this machine.
- Public source cites:
- Cursor docs: https://docs.cursor.com/context/rules-for-ai
- Cursor rules deep-dive: https://docs.cursor.com/features/rules
- As of Cursor 0.45 there is no documented shell-hook surface; "Cursor rules"
is static context only.
- Mode:
absent; documented from public sources—context-onlyif
installed.
6. Windsurf
- Installed on this machine: NO (
~/.windsurfdoes not exist). - Install path (per public docs):
.windsurfrulesat project root, or
global rules configured via Windsurf settings UI.
- Hook events present: NONE documented.
- Payload format: N/A — context-only.
- Installation mechanism: drop
.windsurfrulesin repo. Windsurf auto-reads.
snappy-os maintains via state/bin/sync-runtimes.ts.
- Empirical notes: N/A on this machine.
- Public source cites:
- Windsurf docs: https://docs.windsurf.com/windsurf/cascade/memories#rules
- As of Windsurf 1.x there is no documented shell-hook surface; memories +
rules are static context.
- Mode:
absent; documented from public sources—context-onlyif
installed.
What D2 / D3 / D4 consume from this matrix
- D2 (per-runtime adapters,
state/lib/runtime.ts): the twoagentic
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.
- D3 (
state/lint/parity-test.ts): two parity metrics by mode:
mode: agenticrows →score = eval_gate(output)(0..1)mode: context-onlyrows →score = loader_substring_match(context)(0/1)
- D4 (system-view TUI Parity panel): filter rows by
modebefore
averaging. Comparing an agentic score to a context-only score is category-mixing and the UI will not do it.
Honest unknowns post-D1
- openclaw snappy-loader is reading the wrong path. The plugin reads
~/.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.
- Codex
PreToolUseevent coverage. Not wired on this machine; confirming
whether codex 0.118.0 emits PreToolUse at all for subagent spawns requires a test run. Assume yes for D2 design; verify in D3.
- Cursor / Windsurf local behavior. Neither installed on this machine.
Parity rows for both will be null until a machine runs the test suite under those editors. Document as gap, not failure.
- Gemini CLI extension hook API. Extensions dir exists but empty. Public
docs don't yet describe an extension hook API equivalent to Claude's hook events. Re-check each Gemini CLI release.