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.