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)

agent tries Edit/WritePreToolUse hookreconstruct would-be fileengine · phase editdeny + the fixagent self-corrects

The file never touches disk. The deny message is fed back to the agent, which retries correctly.

rule · commit-time (the backstop)

git commitlefthookengine · phase commitexit 2 — commit blocked

Same engine, same rules — over the staged files, with no agent in the loop.

sign · read-time (just-in-time context)

agent reads a filePostToolUse hookmatch area globsinject the sign

Re-injected every drift_tokens as the context grows.

scan · on-demand (the whole-tree audit)

npx signposts scanengine · phase scanevery per-file rule, whole treereport the violations

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