Packs & distribution

A pack is just a namespace — a coherent set of rules and signs (and the scripts they use). There’s no separate pack format to maintain: any repo with a signposts.yaml can be installed from, including your day-to-day project.

A pack is a namespace

In your config everything is grouped by namespace. A pack is one of those groups — its signs: and its rules: — plus the scripts those rules reference in rules/<namespace>/.

signs:
  neon:
    - { id: migrations, globs: ["src/db/migrations/**"], text: "Migrations are append-only." }
rules:
  neon:
    - { id: no-raw-pool, use: neon/no-raw-pool, on: "src/**/*.ts" }
      # + rules/neon/no-raw-pool.mjs

Because a pack is a slice of the file plus a folder, signposts.yaml itself is the manifest — nothing extra to write. (This mirrors how Claude Code plugins are just a repo you point at.)

Author in place — never extract

The trap with “packaging” is that it pulls your rules away from where you use them, and then they’re hard to edit. So don’t. You build your neon rules in rules/neon/ in your actual project, and share by letting others point at your repo. The rules never leave the place you author them.

Installing — cherry-pick from any repo

/signposts install points at a repo, diffs it against yours, and lets you pick what to pull — by namespace or by entry. See the skill. The deterministic form, when you already know what you want:

npx signposts install github:you/your-project           # browse what's there
npx signposts install github:you/your-project  neon     # take the neon namespace
npx signposts install ../my-hub  neon                   # …or from a sibling folder on disk

It copies rules/neon/ and merges the neon group from both signs: and rules: — the whole pack, in one move. Comments in your signposts.yaml are preserved (the merge edits the document in place, it doesn’t re-serialise from scratch). To remove a pack later: npx signposts uninstall --pack neon — it drops the namespace’s yaml groups, its rules/neon/, and any permissions it added, leaving your own config untouched.

Install reads the current layout only. An older repo (flat signs:/rules: lists, or the legacy advisory: key) is refused with a clear message — open it with /signposts install (the skill) and cherry-pick by hand. No silent half-install.

The hub: your own repo as the spine

You don’t need a public anything. Keep a personal rules repo (private is fine) as your hub: reflect finds a rule in a project → propagate sends it to the hub → install pulls it into your other projects. Trusted teammates can install from the hub too.

Three install doors

The same pack installs any of three ways — pick by how much ceremony you want.

Local path (private / quick) Git (default) npm-scoped (at scale)
Source ../my-hub · /abs/path github:you/pack#v1, or private over SSH @signposts/neon · @acme/guardrails
Publish step none — install straight from disk just git push (tag to pin) npm publish
Versioning whatever’s on disk tags / refs semver (@^2)
Best for private repos · one cleaned-up hub folder you install everywhere from you · personal · shared official packs · companies · access control

Same cherry-pick semantics for all three — only the fetch differs. The local door fits the common case: most rule-bearing repos are private, and the natural workflow is “tidy one local hub, install from there”. Official packs live under @signposts/*; a company publishes under its own (@acme/*); you just push (or point at) a repo.

A pack can carry host permissions

Some guardrails aren’t enforceable by the engine — denying a read, or an MCP tool call, is the host’s job (Claude Code’s .claude/settings.json), not ours. A pack can carry those so you don’t hand-edit settings after every install. Add a settings: block, per namespace:

settings:
  neon:
    permissions:
      deny:  ["Read(./.env.keys)"]   # the host blocks the read; Signposts just delivers the entry
      allow: []                       # optional

On install these merge into .claude/settings.json, and exactly what was added is recorded as the pack’s ledger in packs: (below), so uninstall --pack removes precisely those entries — never a hand-written one, never one another installed pack still needs.

Refresh keeps your local tweaks

Your config records installed packs so npx signposts refresh can pull updates. Install writes an object entry — the source, the namespaces it owns, the permissions ledger, and the date (a plain string still works for a legacy entry):

packs:
  - "@signposts/neon@^2"                    # legacy string — still valid
  - source: "github:you/signposts-mystyle#v1"
    namespaces: [mystyle]
    settings: { deny: ["Read(./.env.keys)"] }   # exactly what install added
    installed: "2026-07-06"

Refresh does a three-way merge against a snapshot install took (kept under a gitignored .signposts/base/<ns>/): it compares what the pack originally shipped, what’s in your repo now, and what the pack ships today. Only upstream changed → take theirs; only you changed → keep yours; both changed → it surfaces a conflict and touches nothing (for a script it drops a <file>.upstream sidecar beside yours — never conflict markers in a live rule). That’s what makes “install, then tune it locally” safe to combine with “stay current”.

Fresh clone? .signposts/ is gitignored, so the base snapshot isn’t in the repo. Refresh notices and keeps your local versions with a notice — re-install the pack to re-arm merging.

core is a pack too. The baked-in scripts and starter rules ship as the core namespace — same mechanism as neon or your own. Nothing is special; it’s just the pack that comes pre-installed.