Skip to main content
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.
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.
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. 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.
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.
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/.