How it wires together
Three small mechanisms, one config file. You don’t wire signposts into triggers by hand — a rule’s when: does it, and a sign’s globs: do.
rule · edit-time (the pre-emptive block)
The file never touches disk. The deny message is fed back to the agent, which retries correctly.
rule · commit-time (the backstop)
Same engine, same rules — over the staged files, with no agent in the loop.
sign · read-time (just-in-time context)
Re-injected every drift_tokens as the context grows.
scan · on-demand (the whole-tree audit)
npx signposts scan→engine · phase scan→every per-file rule, whole tree→report the violationsThe third door into the same engine — but it never blocks (always exits 0). Edit and commit stop new violations; scan reveals the ones already there (adopting a rule on an old repo, or pre-sizing a proposed rule). See the CLI.
The engine keeps its own event log
Every evaluation appends one line to a local, gitignored log at .signposts/log/<session>.jsonl: a run with per-rule tallies, a deny per violation, and a sign per injection. It’s the deterministic ground truth behind npx signposts facts and its HTML report card — per-rule fires, edit-catches vs commit-leaks, rules that never fired. It stays on your machine (it’s for your own metrics, never sent anywhere), and — like every hook path — writing it is fail-safe: an unwritable log can never block an edit. The report card reads numbers from here and narrative from the transcript, kept strictly apart.
How config reaches a script
The engine reads rules: from signposts.yaml. For each entry it loads the script named by use: and calls it with the entry verbatim as the rule config, plus the file (in-memory at edit, from disk at commit). One uniform path:
| Step | What happens |
|---|---|
| 1 · resolve | use: core/ast-grep → the core script; use: neon/x → your stack script. No special cases — even code patterns are a script. |
| 2 · pass config | The whole YAML entry is handed to the script as-is. The script reads its own fields (rule, sibling, deny, …). |
| 3 · feed the target | The script’s kind says what it needs — file content, just the path, the command, or the whole project. |
Two things that always hold
- Fail-safe. If a script or the hook itself errors, it exits cleanly and lets the edit through. A broken rule can never wedge your agent — it just stops enforcing until fixed.
- All local, in-process. Rules run inside the hook (no temp files, nothing sent anywhere). The pre-emptive check is fast enough to sit on every write — which is exactly why ast-grep is a core script, not a tool-gate: it runs in-process, so it can block before the write. A whole-project tool can’t, so it’s commit-only.