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

# Hooks

> Scripts seri runs at fixed points in the loop, outside the model's control.

A hook is a rail rather than an instruction. It is a script seri runs at a fixed point in the
loop, outside the model's control, so what it enforces is a guarantee instead of a request.
Blocking a `git push --force`, formatting a file the moment it is written, and keeping an audit
log are the three things it is for.

## The manifest

Put it at `.seri/hooks/hooks.yaml` for this project, or under your profile root for every
project.

```yaml theme={null}
hooks:
  PreToolUse:
    - script: block-dangerous
      matcher: bash|powershell
  PostToolUse:
    - script: format-on-edit
      matcher: write_file|edit
      timeout: 10
```

There are two events. `PreToolUse` runs before the tool, `PostToolUse` runs after it.

## The script

`script` is a bare name. The file beside it is `block-dangerous.ps1` on Windows and
`block-dangerous.sh` everywhere else.

**Write both halves.** seri runs the one for the platform it is on, and warns at startup naming
the file and the platform if it is missing. A hook that would silently not run on a teammate's
machine says so instead.

## Matchers

`matcher` is a regular expression over the tool name, anchored at both ends, so `edit` means the
`edit` tool and not the tail of something else. Omit it and the hook runs for every tool.
`mcp_github_.*` scopes a hook to one MCP server.

The names it matches are seri's own, `write_file` and `bash`, not another harness's `Write` and
`Bash`. A ported script that switches on the name needs its matcher rewritten, and the matcher is
in `hooks.yaml`, which its author is editing regardless.

`timeout` is in seconds, 30 by default.

## The contract

seri sends the script a JSON payload on stdin and reads its exit code.

| Exit          | Meaning                                                                                                       |
| ------------- | ------------------------------------------------------------------------------------------------------------- |
| `0`           | allow                                                                                                         |
| `2`           | **block.** The first 300 characters of what the script printed on stderr become the reason the model is told. |
| anything else | the hook could not run. It is reported, and the call proceeds.                                                |

```bash theme={null}
#!/usr/bin/env bash
payload=$(cat)
if grep -q 'rm -rf /' <<<"$payload"; then
  echo "BLOCKED: rm -rf /" >&2
  exit 2
fi
```

A `PreToolUse` block runs **before** the permission gate, so no mode reaches around it. `auto`
and `--dangerously-skip-permissions` are blocked exactly as `approve-each` is, and you are never
asked to approve a call a hook is about to refuse.

`PostToolUse` runs after the tool, where exit 2 has nothing left to stop, so it is reported like
any other failure.

A subagent gets the hooks too. A guarantee that one `dispatch_subagents` call routes around is
not a guarantee, and the archivist needs it most, because it runs on a hardcoded auto mode.

## A broken hook never takes the session down

It fails open, loudly. The call proceeds and the error lands in the transcript. That is
deliberate. A typo in a formatter should not stop you working.

## Hooks from a repository you cloned do not run until you say so

This is the one extension seri will not load on sight. Rules and skills carry text. A hook
carries a program, and it runs in front of the permission gate on tools that never prompt, which
means an untrusted one would be code execution from a `git clone` with nothing asked of you
first.

So hooks in **your own profile root** run, because nothing arrives there by cloning anything. A
project's `.seri/hooks/` is found, listed, and left dormant until you review it. Session start
says so, naming the directory.

| Command          | Does                                                           |
| ---------------- | -------------------------------------------------------------- |
| `/hooks`         | list the wiring: which event, which matcher, which script      |
| `/hooks list`    | the same thing. The bare form and `list` are identical.        |
| `/hooks show`    | every script in full, which is what you review before trusting |
| `/hooks trust`   | turn this project's hooks on                                   |
| `/hooks untrust` | turn them back off                                             |

**Trust is bound to the bytes you read, not to the path.** seri digests every file in the
directory. Edit any of them, or pull a change to one, and the hooks stop running until you look
at what moved and trust it again. Symlinks are followed for neither the walk nor the digest,
because a link points at bytes outside the directory that the grant does not cover.

<Note>
  **Trusting, untrusting and a detected edit all take effect at the next session start, or after
  `/clear`.** The hook registry is frozen when a session begins, so hooks you just trusted are not
  yet running, and hooks already loaded keep running for the rest of a session in which you untrust
  or edit them. `/hooks trust` says so when you run it.
</Note>

The contract is Claude Code's and Cursor's, unchanged, so scripts written for either run here
when you copy them into `.seri/hooks/`. seri does not read `.cursor/hooks/` itself, for the same
reason it does not read `.cursor/agents/`.
