The config file — signposts.yaml
Everything Signposts needs lives in one file at your repo root. A handful of sections, each with a single consumer, so nothing is wired in two places.
| Section | Holds | Read by |
|---|---|---|
project |
Stack identity — a name and description. | the CLI |
config |
Engine runtime settings — currently drift_tokens (the sign re-injection cadence). |
the sign injector |
signs |
Your sign notes, grouped by namespace. | the sign injector |
rules |
Your rule blocks, grouped by namespace. | the engine |
settings |
Optional. Host-permission entries a pack carries, per namespace — merged into .claude/settings.json on install. |
the CLI (install) |
packs |
The installed packs + what each owns, so refresh / uninstall --pack can track them. |
the CLI |
install |
What to copy, which deps to add, how to arm it. | the CLI |
Grouped by namespace
Both signs: and rules: are grouped by namespace — the pack each belongs to. core is the built-in pack; local is your own ad-hoc stuff; a name like neon is a coherent set you might share. A namespace is what install and propagate move as one unit — its signs and rules together.
A worked example
project:
name: my-core
description: My personal dev guardrails. Reused on every repo; irrelevant rules sleep.
config:
drift_tokens: 200000 # re-inject a sign after ~this many tokens of drift
signs:
local:
- id: db-area
globs: ["src/db/**"]
text: |-
Queries go through the repository layer, never inline in a route.
rules:
local:
- id: no-edit-generated
use: core/protected-path
deny: ["**/*.generated.ts", "vendor/**"]
message: "Generated / vendored — change the source, then regenerate."
imports:
- id: boundaries # a tool-gate is heavy → commit-only, so set when:
use: core/tool-gate
run: "npx depcruise src --config .dependency-cruiser.cjs"
carries: [".dependency-cruiser.cjs"]
when: [commit]
packs:
- "@signposts/core" # the baked-in pack
install:
activate: ["npm install"] # arms the gate (lefthook writes .git/hooks/*)
How a rule reads
Each entry names a script with use: and carries that script’s config inline. The whole entry is handed to the script verbatim.
use: core/<name>— a core script; or<namespace>/<name>for your own.on:— the glob(s) it applies to (some scripts usedeny:orrun:instead).when:— defaults to[edit, commit]; omit it unless you need to override (a tool-gate that’s[commit]only).message:— the reason, fed back to the agent on a block.
Code patterns are files, not inline. ast-grep rules stay as rules/ast-grep/*.yml (so the engine and signposts test use the same files); the core/ast-grep runner executes them in-process. See core/ast-grep. Everything else is a normal rules: entry.
Permissions a pack carries — settings: and the packs: ledger
A pack can carry host permissions the engine can’t enforce (denying a read, an MCP tool call) via an optional settings: block — install merges them into .claude/settings.json. And packs: records what each install owns, so refresh and uninstall --pack know exactly what to touch. A plain string entry is still valid; install writes the richer object form:
settings:
neon:
permissions:
deny: ["Read(./.env.keys)"] # enforced by the host, delivered by the pack
packs:
- "@signposts/core" # legacy string — fine
- source: "github:you/hub#main" # object form, written by install
namespaces: [neon]
settings: { deny: ["Read(./.env.keys)"] } # the ledger uninstall reverses
installed: "2026-07-06"
See Packs & distribution for the install / refresh / uninstall flow.