The config file — signposts.yml

Everything Signposts needs lives in one file at your repo root, organised around the thing you actually adopt and share — the bundle. Each bundle is one contiguous, cherry-pickable block.

Two top-level keys are reserved; everything else is a bundle.

Key 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
(any other key) A bundle — the unit of value. Its key is its namespace. everything

A bundle is one block

The old layout scattered a namespace across five top-level sections (signs:, rules:, settings:, packs:, install:) — cherry-picking one meant harvesting from all five. Bundle-first puts each bundle in one place: a top-level key carrying a title, a summary, and a signposts: list. Copying a bundle between repos is copying its block plus its rules/<bundle>/ folder — nothing else.

Key What it is
title A human name for the bundle.
summary One line: what this bundle is for.
signposts The list of signposts — signs and rules, interleaved in reading order.
from (vendored bundles only) Provenance pin — github:owner/repo#ref or a local path. The base refresh diffs against. Local bundles omit it.
settings (optional) Host-permission entries merged into .claude/settings.json on install.

A bundle owns rules/<bundle>/** — that ownership convention is how install and uninstall know what to copy and remove, so there’s no machine manifest to drift.

A signpost is a sign or a rule

Every item in a bundle’s signposts: list is a signpost, tagged with type:. A sign steers (injects guidance, never blocks); a rule blocks. Because they share one list, a sign and the rule it explains sit together in reading order.

Field On What it is
type both sign or rule.
id both A short unique name.
description both A one-line, human “what is this” — for reading the file, not shown to the agent.
at both The lifecycle moment it fires on — always explicit, no hidden default.
on both (optional) The file glob(s) it applies to.
text sign The guidance injected when the moment fires.
use rule The script — core/<name> or <bundle>/<name> — with its config inline.
message rule The reason fed back to the agent on a block.

The moments (at:)

at: Fires when Valid for
session a session starts (orientation) sign
touch a file matching on: is edited sign
write an edit is about to land rule
commit at commit (a whole-tree check) rule
delete a command would delete a matched path rule
command a Bash command is about to run (a command-guard) rule
turn-end the agent tries to end its turn both

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

# ── a LOCAL bundle — born here, may propagate upstream one day ──
core:
  title: Core guardrails
  summary: The orientation and blocks I want on every repo I work in.
  signposts:
    - type: sign
      id: orientation
      description: How this repo is laid out and run — shown at session start.
      at: [session]
      text: |-
        docs/arch/ is the system prior — read before structural work.
        Repeated commands live in justfile recipes, never package.json scripts.

    - type: sign
      id: db-area
      description: Steer database code toward the repository layer.
      at: [touch]
      on: ["src/db/**"]
      text: "Queries go through the repository layer, never inline in a route."

    - type: rule
      id: no-edit-generated
      description: Block edits to generated or vendored files.
      at: [write, commit]
      on: ["**/*.generated.ts", "vendor/**"]
      use: core/protected-path
      deny: ["**/*.generated.ts", "vendor/**"]
      message: "Generated / vendored — change the source, then regenerate."

# ── a VENDORED bundle — pinned provenance, content editable in place ──
fcis:
  title: Functional core, imperative shell
  summary: Keeps the pure core from importing the effectful shell.
  from: github:you/hub#v1.4.2
  signposts:
    - type: rule
      id: import-fence
      description: A dependency-cruiser fence — the domain layer can't reach effects.
      at: [write, commit]
      use: core/depcruise
      layers:
        core:    ["src/lib/**/domain.ts"]
        effects: ["src/lib/**/db.ts"]
      forbid:
        - from: core
          to: effects
          transitive: true
          why: "purity is transitive"
  settings:
    permissions:
      deny: ["Read(.env.keys)"]        # merged into .claude/settings.json on install

How a rule reads

Each rule names a script with use: and carries that script’s config inline. The whole signpost is handed to the script verbatim.

  • use: core/<name> — a core script; or <bundle>/<name> for your own.
  • on: — the glob(s) it applies to (some scripts use deny: or run: instead).
  • at: — the moment(s) it fires on, always explicit.
  • message: — the reason, fed back to the agent on a block.

Code patterns are files, not inline. ast-grep rules stay as rules/<bundle>/ast-grep/*.yml (so the engine and signposts test use the same files); the core/ast-grep runner executes them in-process. Import fences ride the declarative core/depcruise dialect. Everything else is a normal signposts: entry.

Filename

Signposts prefers signposts.yml (what a fresh scaffold writes) but still reads a legacy signposts.yaml. See Packs & distribution for the install / refresh / uninstall flow.