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.
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.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 agit 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..seri/hooks/. seri does not read .cursor/hooks/ itself, for the same
reason it does not read .cursor/agents/.
