For AI agents: a documentation index is available at https://www.plungeai.com/docs/llms.txt. Append .md to any page URL, or send Accept: text/markdown, to get markdown. Setup instructions for agents are at https://www.plungeai.com/docs/agents.md. One ozk_ key opens every plane, models included.

Documentation Index: fetch the complete documentation index at /llms.txt. Use this file to discover all available pages before exploring further.

A hook runs your own command (or an HTTP endpoint) at a fixed point in a run and hands back a typed decision the engine acts on. Hooks are configured in a Claude Code settings.json / hooks.json shape, so a hook you already wrote for Claude Code runs here unchanged.

The nine events

A hook is registered against one of nine lifecycle events:

EventFires
PreToolUseBefore a tool runs — can deny it or rewrite its input.
PostToolUseAfter a tool returns.
StopWhen a turn is about to settle — can continue the run instead.
PermissionDeniedWhen a permission check refuses an action.
UserPromptSubmitWhen you submit a prompt — can add context.
SessionStartWhen a session opens.
SessionEndWhen a session closes.
SubagentStopWhen a spawned sub-agent settles.
PreTideBefore the ledger cuts context out of the window (past the tideline). The old event name PreShelve still fires it as an alias.

PreToolUse and PostToolUse carry the tool_name, so a handler can scope itself to one tool.

The settings.json shape

Handlers live in a settings.json (or hooks.json) under the same keys Claude Code uses — the event name maps to a list of hook groups, each group a matcher plus one or more handlers:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "./scripts/guard.sh", "timeout": 10 }
        ]
      }
    ]
  }
}

A command handler is run through <shell> -c <command> (shell defaults to sh) in the project directory, with the event payload as JSON on stdin and these variables on its environment: OC_HOOK_EVENT (the event name), OC_PROJECT_DIR and CLAUDE_PROJECT_DIR (both the project directory, so a Claude Code hook script that reads $CLAUDE_PROJECT_DIR works unchanged). An http handler POSTs the payload to a URL with your headers. Hooks are discovered as capabilities (see 09 · Capabilities and plugins) and, because they execute on your behalf, they run only from a trusted source.

The payload is the Claude Code shape: every event object carries hook_event_name and cwd (inserted only when absent, never overwriting a field the event already set); SessionStart carries source: "startup", SessionEnd carries reason: "exit". A group's matcher is a regex tested against tool_name for PreToolUse / PostToolUse / PermissionDenied, against source for SessionStart and against reason for SessionEnd; the other events ignore it. An absent or empty matcher matches everything. One documented gap: session_id is not available at the hook seam, so a handler that keys on it sees no such field.

Where hooks are read from — and when they run

At boot (and on every reload) oceancode walks every ecosystem's hook files — its own .oceancode/, Claude Code's .claude/ and Grok Build's .grok/ — in this order, and merges their groups per event, so earlier tiers' handlers run first:

SourceTierRuns when
.oceancode/hooks.json in the projectlocalalways — the local .oceancode/ tier is your machine's own copy, trusted.
.claude/settings.json, .claude/settings.local.json in the projectrepoonly when the project is trusted (oceancode trust repo on, or t in /hooks); otherwise held.
~/.claude/settings.jsonuseralways — the user tier is trusted.
.grok/hooks.json in the project, ~/.grok/hooks.jsonrepo / userGrok Build's hook files, gated exactly like the repo and user tiers above.
<plugin>/hooks/hooks.json of an installed pluginpluginonly when the plugin is enabled and trusted; ${CLAUDE_PLUGIN_ROOT} in its commands is replaced with the plugin's root.

A held source is still parsed and listed — with its handlers — so you can see exactly what trusting it would enable. The reasons are held: untrusted (repo tier, project not trusted), held: disabled plugin, unreadable and invalid.

Since 2026-09-10 the three Claude Code settings files are also watched: saving one reloads the hooks (the same path r in /hooks and POST /hooks/reload take) together with the Claude Code settings mirror — no reload needed after editing ~/.claude/settings.json.

On the claude CLI route there is no double firing. When a session's provider route is the claude command-line protocol, the claude subprocess reads the very same ~/.claude/settings.json, .claude/settings.json, .claude/settings.local.json and its installed plugins' hooks/hooks.json, and runs those hooks itself. On that route oceancode therefore dispatches only its own .oceancode/ hooks (the ocean_code ecosystem, local and user tiers) and leaves every Claude Code-shared source to Claude Code — a UserPromptSubmit handler in .claude/settings.local.json fires once per prompt, not twice. Every other route (Anthropic API, OpenAI-compatible, Grok, …) dispatches everything in the table above, since nothing else would. /hooks and GET /hooks still list all sources on every route; the by-source report marks the shared ones (shared_with_claude_code).

/hooks and oceancode hooks list

In Ocean pager, /hooks opens the Hooks tab of the capabilities picker (toggle and reload go through the engine); see 19 · Ocean pager.

Without a session:

oceancode hooks list            # one line per source, then its handlers indented
oceancode hooks list --json     # {"repo_trusted": bool, "sources": [...]}
oceancode hooks add PreToolUse --matcher Bash --command ./scripts/guard.sh [--timeout 10] [--scope user|project|local]
oceancode hooks remove PreToolUse --matcher Bash --command ./scripts/guard.sh [--scope user|project|local]

list ends with HOOKS_OK sources=<n> handlers=<m> loaded=<k>; add prints HOOK_ADDED <path> and remove prints HOOK_REMOVED <path> (--scope defaults to project; remove of a handler that is not there exits 1 with hooks remove: no such handler); --dir <d> and --home <h> select another project or home. The scope names are Claude Code's: user, project and local are the three files. Note that both project files — .claude/settings.json and .claude/settings.local.json — are listed at tier repo: they live in the checkout, so they run only when the project is trusted. (local the scope is Claude Code's name for the git-ignored project file; local the tier is .oceancode/, your machine's private copy.) Over HTTP a client reads GET /hooks, reloads with POST /hooks/reload, toggles repo trust with POST /trust/repo {"on": true|false}, adds with POST /hooks {event, matcher, command, timeout?, scope} and removes with POST /hooks/remove {path, event, matcher, target} — every one answers the same {repo_trusted, sources} document.

The decision contract

A handler communicates back two ways: its exit code and a JSON object on stdout.

SignalMeaning
exit 2Deny. On PreToolUse the tool does not run; on Stop it is a deny that does not count toward the Stop cap.
any other non-zero, or a spawn / timeout / IO errorNo opinion — fail-open, the run proceeds.
stdout hookSpecificOutput.permissionDecisionAn explicit allow / deny / ask.
stdout updatedInputRewrites the tool input before it runs.
stdout additionalContextExtra text folded into the model's context.
stdout stopReasonOn Stop, the reason the run should continue rather than settle.

Grok Build handlers may also return updatedToolOutput (honoured only for a model-visible copy) and defer. Everything but an explicit deny fails open: a handler that crashes, times out or prints garbage never wedges a run.

Only command and http handlers run here, and only the nine events above. A handler of another type — Claude Code's prompt and agent hooks — or one registered under an event this runner does not implement (Claude Code's Notification, PreCompact, SubagentStart, …) is skipped and counted (skipped: <n> unsupported handlers beside the source in /hooks), never fatal: the other handlers in the same file still load.

Continuing a Stop

A Stop handler that wants the run to keep going returns a stopReason — the engine treats that as a continuation and drains again. To keep a runaway handler from looping forever, at most 8 Stop continuations are honoured per run; past that the run settles. An exit-2 on Stop is a plain Deny and does not count against those 8.

Provenance and replay

Every hook outcome is recorded with its provenance — which source, which handler, what it decided — so a run can be replayed and audited: you can see exactly which hook denied a tool or continued a turn, and from which tier it came. oceancode inspect lists every hook source with loaded or held beside it.

See also

Planned: TI-34

The docs assistant is not available yet. You can hand these docs to your own assistant instead.