> ## Documentation Index
> Fetch the complete documentation index at: https://www.plungeai.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Hooks

> 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.


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:

| Event | Fires |
|---|---|
| `PreToolUse` | Before a tool runs — can deny it or rewrite its input. |
| `PostToolUse` | After a tool returns. |
| `Stop` | When a turn is about to settle — can continue the run instead. |
| `PermissionDenied` | When a permission check refuses an action. |
| `UserPromptSubmit` | When you submit a prompt — can add context. |
| `SessionStart` | When a session opens. |
| `SessionEnd` | When a session closes. |
| `SubagentStop` | When a spawned sub-agent settles. |
| `PreTide` | Before 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:

```json
{
  "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](/oceancode/capabilities)) 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:

| Source | Tier | Runs when |
|---|---|---|
| `.oceancode/hooks.json` in the project | local | always — the local `.oceancode/` tier is your machine's own copy, trusted. |
| `.claude/settings.json`, `.claude/settings.local.json` in the project | repo | only when the project is trusted (`oceancode trust repo on`, or `t` in `/hooks`); otherwise **held**. |
| `~/.claude/settings.json` | user | always — the user tier is trusted. |
| `.grok/hooks.json` in the project, `~/.grok/hooks.json` | repo / user | Grok Build's hook files, gated exactly like the repo and user tiers above. |
| `<plugin>/hooks/hooks.json` of an installed plugin | plugin | only 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](/oceancode/configuration#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](/oceancode/pager#what-is-wired-to-the-ocean-engine).

Without a session:

```bash
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**.

| Signal | Meaning |
|---|---|
| exit `2` | Deny. 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 error | No opinion — **fail-open**, the run proceeds. |
| stdout `hookSpecificOutput.permissionDecision` | An explicit `allow` / `deny` / `ask`. |
| stdout `updatedInput` | Rewrites the tool input before it runs. |
| stdout `additionalContext` | Extra text folded into the model's context. |
| stdout `stopReason` | On `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

- [09 · Capabilities and plugins](/oceancode/capabilities) — discovery, tiers, and the trust gate hooks depend on.
- [11 · MCP servers](/oceancode/mcp) — the other trust-gated capability.
- [02 · Configuration](/oceancode/configuration) — `OC_HOOK_EVENT` and `OC_PROJECT_DIR`, set on every command handler.
- [19 · Ocean pager](/oceancode/pager#what-is-wired-to-the-ocean-engine) — `/hooks` in the terminal UI.
- [07 · Headless and server](/oceancode/headless-and-server#managing-capabilities-from-the-cli) — `oceancode hooks list` and `oceancode trust`.
