// docs
snuffle in one page
snuffle is your coding playbook for AI agents: your skills, rules, workflows, and the decisions behind them live as plain markdown in your repo (.snuffle/) and reach Claude Code, Cursor, Codex, and any MCP-capable agent over MCP.
Everything on this page has two forms. Use the Agent / CLI switch (top right) to flip every example between the prompt you give your coding agent and the equivalent snuffle command. It stays where you set it.
How do we handle retries in this project?
Agent mode: your agent answers from your own playbook over MCP. CLI mode: the same query from your terminal.
// getting started
Install and set up, once
snuffle installs and runs through npx (no daemon, no account). The one-liner npx @snuffle/cli setup scans the agent config directories in your home (~/.claude, ~/.codex) for the skills, subagents, and MCP servers you already use, previews them, and on confirmation records each as canon at your user scope under ~/.snuffle/user/<kind>/. It writes a default ~/.snuffle/config.json if one is absent.
setup is dry-run-first: with no apply signal it writes nothing and just shows the candidates. In an interactive terminal it asks "index these at your user scope? [Y/n]"; in CI or a non-TTY you pass --apply (or -y) to skip the prompt. Every rendered resource is secret-scanned before it is written — user scope is the one scope where an explicit # snuffle:allow directive is honored. It then offers to onboard the current project too (or run it up front with --project).
Once imported, these follow you everywhere: snuffle mcp merges your user scope on serve, so your global skills and MCP servers are available in every project you later onboard. snuffle publishes to npm as @snuffle/cli, so npx @snuffle/cli setup always runs the latest without a global install.
Setup wires the machine before any agent is connected, so there is no MCP path yet. Run: npx @snuffle/cli setup
Dry-run first. Press Y at "index these at your user scope?" to import.
Set up snuffle at my user scope with no prompts by running `npx @snuffle/cli setup`.
--apply (or -y) skips the confirm prompts; still secret-scans each resource before writing.
Set up snuffle and onboard this project in one go by running `npx @snuffle/cli setup`.
--project delegates to `snuffle onboard` for the cwd after the user-scope import.
Onboard a project
snuffle onboard integrates an existing repo in one command. It scaffolds .snuffle/ (via snuffle init), discovers existing agent config and docs — .claude/skills, CLAUDE.md, docs/adr, .cursor/rules — and imports them as canon, reusing anything your user scope already covers by reference instead of copying. It wires the snuffle MCP server into .mcp.json, installs the operating-skill pack into .claude/skills/, installs the decision-before-merge gate (a GitHub Action plus a lefthook pre-push), and wires the advisory agent-side gate hook into .claude/settings.json.
In a terminal you get to edit the selection before anything is imported: it lists the candidates and offers [Y] import all, [n] none, or [e] edit — confirming each one. An agent or CI run imports all. Secret-bearing files are skipped, never copied into canon. Re-running is idempotent: existing files are left untouched and the surgical edits re-apply as no-ops.
An agent can do the same over MCP: the installed snuffle-onboard-project operating skill runs the flow, and the agent then reports what it imported, reused, and skipped. onboard also offers optional, free-by-default AI steps (drafting strategy, learned embedder, drift classification) that are skipped silently when non-interactive.
Onboard this project with snuffle — import our existing skills, CLAUDE.md, and ADRs as canon, wire the MCP server and the decision gate, and skip anything with a secret.
Agent runs the snuffle-onboard-project operating skill; imports all (no interactive edit outside a TTY).
Scaffold a snuffle knowledge base in this project by running `npx snuffle init`.
init just creates .snuffle/{user,project,team}/ + config.json; onboard calls it for you.
Run a snuffle health check on this project and tell me what's indexed.
doctor reports resource counts and a clean-exit verdict.
Wire a second agent to the same store
Onboard wires Claude by default. To make a second agent behave the same way, connect it to the same .snuffle store and install the same operating skills in its own native format — nothing is migrated or copied, both agents resolve one playbook over MCP.
snuffle cursor init wires .cursor/mcp.json and installs the operating skills as .cursor/rules/snuffle-*.mdc. snuffle codex init registers the server in the global ~/.codex/config.toml and installs prompts under ~/.codex/prompts/. snuffle claude init does the Claude equivalent (.mcp.json + .claude/skills/). Each of these commands also accepts refresh and sync (re-apply the projection to current canon) and remove (unwire). snuffle link re-creates the MCP projections for whichever agents the project already uses.
After this, Claude Code, Cursor, and Codex read the same resolved playbook — same skills, same rules, same order of work — each in its own format. Reload the agent to pick up the wiring.
Wire Cursor to this project's snuffle store by running `npx snuffle cursor init`.
Writes .cursor/mcp.json + .cursor/rules/snuffle-*.mdc, then reload the project in Cursor.
Wire Codex to this project's snuffle store by running `npx snuffle codex init`.
Codex MCP config is global (~/.codex/config.toml); restart Codex to pick it up.
Re-create the snuffle MCP wiring for this project's agents by running `npx snuffle link`.
Regenerates .mcp.json / .cursor/mcp.json for the agents the project already uses.
Ask and use your playbook over MCP
On the CLI, snuffle ask "<question>" answers from your own canon without leaving the terminal. It is extractive, not generative: it builds the effective KB (project + your user scope + resolved references), runs the same hybrid lexical+vector search the MCP semantic_search tool uses, and returns the top-ranked resources as citations (scope:id + relpath) plus the leading snippet of the best match. Pass --k N to widen the result set. Prose synthesis over the hits would need a hosted LLM and is deliberately not wired.
Your coding agent reaches the same knowledge over MCP — it starts snuffle mcp on demand, which serves this project's .snuffle store merged with your user scope. The agent uses the read tools (semantic_search, structured_filter, graph_traverse, fetch_resource, assemble_context, context_for_file, overview) to pull the right canon into context, and the write tools (record_skill, record_decision, record_rule, record_workflow) to capture new knowledge as you work — governed by the installed operating skills like snuffle-load-context, snuffle-answer-from-canon, and snuffle-record-decision.
snuffle doctor checks the base's health and clean-exit status and flags reusable skills and rules that live only in this project and are worth lifting to user scope.
How do we handle retries here? Check our snuffle playbook before you answer.
CLI returns ranked citations; agent uses semantic_search / fetch_resource over MCP (snuffle-answer-from-canon skill).
Load the snuffle context for src/http/client.ts before you edit it — which rules and skills govern this file?
Agent uses context_for_file / assemble_context over MCP; CLI `why` lists the governing rules/specs/skills.
Run snuffle doctor and summarize any dangling refs or promote suggestions.
Flags reusable resources living only in this project.
Carry it into the next project (promote)
When you onboard your next repo, anything already covered by your user scope is reused by reference, not copied — you get the same behavior without the copy-paste. To make a project-only resource follow you everywhere, snuffle promote <ref> relocates its bytes to the user home store (~/.snuffle/user/<kind>/) and leaves a body-less source: reference stub in the project. The promoted resource then loads as user scope on every serve and is reused by reference.
promote is fail-closed. It refuses (writing nothing) when the ref is not at project scope, is already a reference, is a governed kind (rule/hook — promoting would invert team precedence), is a decision (append-only project provenance), when --to is anything but user, when the body holds a secret, or when the rewritten content would not re-parse. It is idempotent and clobber-safe: re-promoting byte-identical content is a no-op, and a different existing user file is never overwritten.
An agent can do this through the snuffle-promote operating skill after snuffle doctor flags a reusable resource.
Onboard this repo too — reuse anything my user scope already covers and only import what's new.
Reused resources are referenced, not copied.
The api-pagination skill is reusable — promote it to my user scope so every project inherits it.
Agent runs the snuffle-promote skill; leaves a source: stub in the project.
Promote the api-pagination skill to my user scope by running `npx snuffle promote skills/api-pagination`.
--to must be "user"; governed kinds and decisions are refused.
Create a team, invite, and approve
A team in snuffle is your device's signing key pinned as the owner trust-root plus a shared store — there is no service to run. snuffle team create <name> --broker <store-url> pins this device's keychain-backed public key as the team's owner trust-root (written to .snuffle/config.json) and prints its fingerprint; publish that fingerprint out-of-band so a first joiner can confirm it. The private key never leaves the keychain.
snuffle team invite --allow <emails> --ttl-days <n> mints a signed redemption code that pre-approves the listed emails; --open makes it a bearer token anyone can redeem (defaulting to a shorter 1-day TTL). Only the pinned owner key can mint invites. The invite prints a npx snuffle join <code> line to hand to the joiner.
On the joiner's machine, snuffle join <code> verifies the code against the pinned owner key (a first join requires an out-of-band fingerprint confirmation via --pin-owner-key <fp>), then drops a signed pending request — their public key, never their secret — into the shared store. Nobody is admitted until the owner approves: snuffle team pending lists the queue and snuffle team approve <email> appends an owner-signed membership event that binds the joiner's device key. Self-asserted email is a label, not a control — membership is conferred only by the owner's signature, so holding the shared-store credential alone cannot mint a member.
Create our snuffle team and pin this device as owner by running `npx snuffle team create acme`.
Signing needs the OS keychain; publish the printed owner fingerprint out-of-band.
Create a snuffle team invite for alice@acme.com that expires in 7 days and give me the join line.
Owner-only. Prints `npx snuffle join <code>`; listed joiners queue for approval.
On the joiner's machine, join the team by running `npx snuffle join <code>`.
First join needs --pin-owner-key <fingerprint> confirmed out-of-band; then it queues, unverified.
Approve alice@acme.com into our snuffle team by running `npx snuffle team approve alice@acme.com`.
Approve binds the joiner's device key; owner-signed membership event (seq N).
Sync the same canon across the team
snuffle sync reconciles team-scope canon (.snuffle/team/**.md) with the configured backend (fs or Scaleway) using a hash-based 3-way merge against a manifest in .snuffle/state/sync/. When an owner trust-root is pinned, every team file is signed on push and verified on pull against the owner-signed member set: an unsigned file, or one signed by an unknown or unapproved signer, is held back before it touches your repo. Egress is fail-closed — every local team file is secret-scanned before any push, and one hit aborts the whole sync.
Both-sides-changed cases surface as conflicts and exit non-zero unless you force a winner with local-wins or remote-wins. --watch runs it as a daemon (with --interval N), and --git-snapshot records a snapshot. When a project needs a different take on a team skill, a project-scope copy overrides it for that repo alone while team rules and hooks stay enforced.
An agent runs this through the snuffle-share-with-team operating skill and reports what it pushed, pulled, and held back.
Sync our team canon with snuffle and tell me what got pushed, pulled, or held back.
Agent uses the snuffle-share-with-team skill; unsigned/unknown-signer files are rejected but non-fatal.
Sync our team canon and, on a conflict, let the remote copy win by running `npx snuffle sync`.
Use local-wins / remote-wins only to break a both-changed conflict.
Keep our team's snuffle canon continuously in sync in the background by running `npx snuffle sync`.
Daemon mode; re-syncs on change / interval.
Leave clean (detach and offboard)
Offboarding never holds your markdown hostage. snuffle detach --team <name> removes the team from this project's config and keeps its local store frozen (history and attribution preserved) — --purge wipes that local store instead. snuffle detach --project unwires every agent (each package tears down its own MCP entry), strips projected rule blocks, removes the advisory Claude gate hook, and deletes derived state, leaving .snuffle/project/** and .snuffle/team/** markdown intact. snuffle detach --device drops this device's signing key from the keychain. snuffle uninstall does the full local teardown at once (project wiring + derived state + device key), still keeping markdown unless you pass --purge.
Detach is honest about revocation: dropping local config or creds cannot revoke server-side grants. To actually cut off a leaver's or a leaked credential's access to shared canon, the owner rotates the shared store credential — snuffle team rotate-credential prints the manual out-of-band checklist (mint a new key, distribute to current members, delete the old one). Self-detach is reversible: re-run snuffle <agent> init to re-wire.
Detach this project from the acme team, keeping the local store, by running `npx snuffle detach`.
Removes team from config; rotate the credential to actually cut off access.
Unwire snuffle from this project but keep the markdown by running `npx snuffle detach`.
Removes agent wiring + gate hook + derived state; .snuffle markdown stays. Re-run `snuffle claude init` to re-wire.
Show me how to rotate our team's shared store credential to cut off access by running `npx snuffle team rotate-credential`.
Prints the manual out-of-band rotation checklist; snuffle cannot rotate the IAM key for you.
// concepts
What is a playbook / the .snuffle layout
snuffle is a shared "how-we-build" knowledge base for AI coding agents. The playbook itself is nothing exotic: it is plain markdown files with YAML frontmatter, one file per resource, living under a .snuffle/ folder. Those files are the single source of truth ("L0 canonical"). You can open, grep, diff, and commit them; if you deleted snuffle entirely you would still have a readable folder of markdown. Nothing needs snuffle to make sense.
The playbook lives at three scope directories that are just subfolders: project canon in .snuffle/ inside your repo (versioned in git); user canon under ~/.snuffle/user/ (your promoted resources, following you across projects); and team canon mirrored from a shared store under ~/.snuffle/teams/<name>/. Within a scope, resources are organized by kind (e.g. user/workflows/feature-delivery.md, team/decisions/0046-*.md, user/skills/*.md).
The platform's job is to move these files between machines (git in your repo, plus the team store), index them, and serve the ranked, task-scoped slice to agents and humans over a local MCP server, with inheritance/override across user then project then team scopes. The architecture is layered and each layer is pluggable: L0 canonical files, L1 sync (files to/from a bucket), L1.5 reconcile (git 3-way, union-merge the append log), L2 index (Postgres + pgvector + FTS), L3 access (the MCP toolset with a file-only fallback), L4 resolution (inheritance/override/drift), and L5 governance (declarative workflows, gates, decision log). Set up a project with snuffle init.
One-time project scaffolding has no agent equivalent
Creates the .snuffle/ store; snuffle init gitignores only .snuffle/state/.
"Give me an overview of our snuffle playbook — how many skills, rules, decisions, and workflows do we have?"
The agent calls the overview MCP tool, the cheap entry point.
Resources, kinds, frontmatter fields & lifecycle
Every resource has a kind, from a fixed vocabulary of eight: skill, command, rule, hook, workflow, decision, spec, note (RESOURCE_KINDS in packages/core/src/domain/kinds.ts). workflow and decision are the hero kinds — the differentiator — capturing repeatable multi-step processes and the append-only reasoning log.
Frontmatter is validated by a zod schema (packages/core/src/canonical/frontmatter.ts). Core fields: id (stable logical id; derived from the file path minus the scope prefix if absent), kind, title, description, scope (inferred from a leading user/ project/ team/ path segment if absent), tags, status (free-form, e.g. a decision's status: accepted), and lifecycle. Kind-specific and feature fields include: phases (workflow steps, each with resources refs and gates); command (a hook's executable command — a hook whose command is not an allowlisted-safe snuffle … invocation is high-risk and gets quarantined until approved); applies_to (repo-root-relative posix globs the resource governs — an agent editing a matching file pulls it as the relevant "why"); owners (CODEOWNERS-style approvers); supersededBy (successor for the "use X instead" nudge); overrides (target + baseHash, drives drift); source (by-reference stub pointing at the canonical copy); promotedFrom; and decision provenance (deciders, supersedes, backfilledFrom, draftedBy).
Lifecycle is an explicit lifecycle: field (absent → active), deliberately separate from the free-form status:. There are five states (packages/core/src/domain/lifecycle.ts): draft (hidden from consumers, not pushed, not surfaced), active (normal), deprecated (still findable but de-ranked to 0.5x and warned with a "use X instead"), archived (removed from the index and the active sync set, but history kept — still directly fetchable), and quarantine (behaves like draft — hidden, not synced, not indexed — but is assigned by admission for a high-risk hook, not authored, and stays inert until snuffle approve clears it). Only active + deprecated are indexed, surfaced, and synced.
"Record a workflow for our feature-delivery loop: phases brainstorm, spec, plan, work, review, merge — and put a hard gate on merge requiring a decision entry."
Agent calls record_workflow with phases (each phase can carry gates of type soft|hard).
"Show me all our team-scope decisions tagged 'delivery'."
structured_filter matches kind/scope/tags (ALL) + free text; only active+deprecated are surfaced.
Scopes & resolution (user / project / team)
A single logical id can be defined at more than one scope. Resolution collapses each id to exactly one effective winner, and which scope wins is kind-dependent (packages/core/src/resolution/resolve.ts). This is deliberate and deterministic — no LLM involved.
Governed kinds (rule and hook) are the team's enforceable canon, so the shared base wins: precedence is team > project > user. A user can refine a rule within a project, but cannot quietly override a team mandate from their own user scope. Everything else (skills, commands, workflows, decisions, specs, notes) is personalizable, so the most-specific scope wins: user > project > team. Decision 0012 records that the original uniform user > project > team ordering — which let a user silently shadow a team rule/hook — was a correctness bug.
Cross-scope reuse without copying is done by-reference. snuffle promote <ref> lifts a project resource's canonical bytes into your user home store (~/.snuffle/user/<kind>/) and leaves a body-less source: reference stub in the project; the loader materializes the stub's body from the target so the bytes live in exactly one place, and the promoted resource follows you into every project. Promote is fail-closed: it refuses non-project-scope refs, already-references, governed kinds (promoting a rule/hook to user would invert team precedence), decisions (append-only project provenance), a --to other than user, a body holding a secret, or content that would not re-parse.
"Promote the skill skills/rrf-fusion to my user scope so I have it in every project."
Leaves a source: stub in the project; refuses governed kinds and decisions.
"Fetch rules/no-secrets and tell me which scope's version is the effective one."
The serve path resolves team-wins for governed rule/hook, user-wins otherwise.
Files vs MCP: the two-layer access model
Two questions get conflated constantly, and the whole model turns on keeping them apart (docs/design/access-model.md). First: how does the playbook get to a machine/project/team? Answer: files. git in your repo, plus the team store. Not MCP. Second: how does an agent read and write the playbook while it codes? Answer: MCP — local, on-demand, scoped and ranked.
L0/L1 is files. In a repo the .snuffle/ folder travels with git like any other tracked files. Across projects, snuffle promote lifts a resource into your user scope (by reference). Across a team, snuffle sync pushes/pulls signed canon through a store you own (S3-style bucket) — signed on push, verified against the owner-approved member set on pull. MCP is never the sync transport; a teammate's changes reach you as files in the store.
L3 is MCP. The server is local and stdio, launched on demand by the agent's host (packages/core/src/mcp/start.ts, StdioServerTransport) — there is no resident daemon and no remote endpoint. At serve time it does the work raw file reads can't: it merges user + project + team scopes, materializes source: references, and resolves the effective kind-aware canon before anything reaches the agent, then builds the search index over that same effective set; fail-closed secret scanning runs on the serve path. The result is the right resolved slice for the task, not the whole corpus and not an unresolved pile of per-scope files.
What is always materialized into agent-native folders is narrow: the MCP config (.mcp.json, .cursor/mcp.json, ~/.codex/config.toml, generated not symlinked) and the eleven snuffle-* operating skills that teach each agent how to use snuffle. The rest of your domain canon (skills, workflows, decisions, specs, notes) is NOT re-projected back to native folders — it is served over MCP. The one opt-in exception is rule projection (below).
(wired via .mcp.json) — the agent launches the server on demand; the snuffle tools appear
File-only with no index, so it works offline; KB dir via TEAMSHARING_KB, first CLI arg, or cwd.
Writes the MCP config + operating-skill pack; no agent equivalent
link re-creates the generated .mcp.json / .cursor/mcp.json projections (Claude + Cursor).
Capture as you work
The knowledge base is writable over MCP. When capture is enabled, the server advertises four record tools and emits capture instructions to the agent (packages/core/src/mcp/server-factory.ts): record_skill (a reusable how-to, the moment you learn a repeatable technique), record_decision (a consequential choice — the what, the why, the trade-offs — which also satisfies the decision-before-merge gate proactively; omit the body to get a MADR scaffold), record_rule (a must/never the team should always follow, with optional appliesTo code-path globs so the rule code-anchors to the files it governs), and record_workflow (a repeatable multi-step process, phases required, each phase optionally carrying gates).
All four write plain markdown into the project store at project scope. The sink is fail-closed: it validates and secret-scans before the write lands — so you never paste credentials, you reference them. There is deliberately no record_note tool (notes are freeform/low-signal for agents; the four kinds cover the authoring surface), though note capture still works programmatically.
Capture is gated by write authorization: the tools are registered only when a capture sink is injected and the principal has write on the project (with the default AllowAll backend this is permissive). The equivalent CLI surface for humans is snuffle decision (scaffold a decision-log entry).
"Record a decision that we're adopting reciprocal rank fusion to combine vector and lexical search, and note the trade-offs we discussed."
Agent calls record_decision; omitting the body yields a MADR scaffold to fill in.
"Record a rule that all monetary amounts must be integer cents, applying to packages/billing/**."
record_rule takes appliesTo globs; body is secret-scanned before it is written.
Governance: the decision gate + nudge
The governance layer (L5) is a decision-before-merge gate plus an advisory nudge. snuffle gate diffs the changeset against a base ref (--base, else $GATE_BASE, else origin/main) and passes if and only if the changeset adds or modifies a kind: decision resource — exit 0 when satisfied, 1 otherwise (packages/cli/src/commands/gate.ts). This is the same check the GitHub required status check runs (authoritative) and that lefthook runs at pre-push (advisory shift-left). There is also snuffle gate --hook, a Claude Code PreToolUse mode: it reads the tool payload on stdin and, for a publish command (git push / gh pr create|merge) with no decision recorded on the branch, returns a deny nudging you to record one — advisory only, it never hard-blocks (the forge required check is the authority).
Hard gates can also be declared on workflow phases: a phase's gates entry of type: hard (with an optional machine check, e.g. decision-exists) is compiled to a forge check; type: soft gates are agent-followed.
The nudge (snuffle nudge) is purely advisory and always exits 0: when significant work is about to be pushed with no decision recorded, it prints the exact snuffle decision command to run while it's fresh (packages/cli/src/commands/nudge.ts). It is wired as an advisory pre-push lefthook command and surfaced in snuffle doctor; nudge.enabled: false fully silences it. Related surfaces: snuffle coverage reports undocumented reasoning (changes with no decision, thin-coverage areas) and snuffle backfill proposes decision entries for un-recorded architectural history.
"Run the snuffle decision gate against origin/main and tell me if I still need to record a decision before merging."
Exit 0 iff the diff adds/modifies a kind: decision resource; same check the forge enforces.
"Do I have significant uncaptured work on this branch that should have a decision recorded?"
Always exits 0; never blocks. nudge.enabled:false silences it.
Team sharing (signed store, owner-approved membership, no server)
A teammate can join a team and receive its canon without the owner standing up any external service — no operated broker, no GitHub OAuth app, no Postgres, no OpenFGA (docs/design/team-distribution.md). The only requirement is a shared storage backend (the existing Storage seam — local fs for solo, S3/Scaleway for a real team). Identity and credential-minting are pluggable seams with a zero-config local default and OAuth/OIDC/STS as later opt-ins.
The trust model is the Discord/Slack "invite link + admin approval" pattern. An invite is a signed (ed25519) bearer join-request token carrying an allow list of pre-approved emails or the literal "any", inside the signed payload. Identity is self-asserted (read from the joiner's local gh/git). In the zero-config build every join QUEUES — auto-join is enabled only behind a verified IdP. The security gate is the approval queue plus the owner: a leaked code lets someone request to join under a claimed identity; the owner seeing an unexpected identity and denying is what stops the leak.
Membership state lives in the shared store, not a server: membership and the pending queue are an append-only log of immutable, content-addressed objects (key = hash of the event), folded by mergeAppendLog so distinct keys never collide. Every membership-mutating event (approve/deny/revoke, member record) is ed25519-signed by a key chained to the team's pinned owner trust anchor and verified on read; unsigned/untrusted events are ignored. snuffle sync push signs each file into a content-addressed sidecar; pull builds the canon trust root (owner key + approved members' bound device keys) and rejects unsigned, unknown-signer, or secret-bearing content.
The honest caveat, stated plainly in the design: v1 closes forgery, not confidentiality. Byte-level read of the store is gated by the storage credential, not by approval — any shared-cred holder can read/write/delete all canon regardless of membership, and deny/revoke cannot rescind it; the only v1 revocation is rotating the shared credential (a documented team rotate-credential checklist, since the CLI reads SCW_* from env and cannot rotate the IAM key). Scoped per-user credentials (the STS/minter seam, decision 0099) are the prerequisite for sensitive canon and per-user revocation.
Team membership + credential handling has no agent equivalent
join verifies signature+expiry locally then queues; approve is an owner-signed transition.
Sync moves files, never MCP; no agent capability
Pull rejects unsigned/unknown-signer/secret-bearing content; scan runs on every pull.
Rule projection (opt-in, always-on native rule files)
MCP retrieval depends on the agent choosing to call a tool. Rule projection is the opt-in stronger guarantee: it materializes the resolved always-on rule set into each wired agent's native always-on rule surface, so a must-follow rule is in context with zero tool calls (docs/design/specs/rule-projection.md). It is off by default (projection.enabled), keeping the MCP-first model intact for everything else. Only kind: rule is projected — skills, commands, workflows, decisions, specs, and notes stay MCP-only.
The projected set is kind === "rule" from the effective, authz-filtered KB, filtered to surfaced lifecycle (draft/archived/quarantine excluded; deprecated labeled). Because it is the resolved set, a project or user scope cannot shadow a team rule via projection — governed rules are team-wins (decision 0012) and only the winner per id is emitted. applies_to is the always-on axis: a rule with no applies_to is projected everywhere; a rule with applies_to globs becomes Cursor-only (one .mdc per rule with native globs: + alwaysApply: false), since Claude/Codex have no glob-gated always-on surface.
Targets: Claude's project-root CLAUDE.md and Codex/generic AGENTS.md get a delimited managed block (<!-- BEGIN SNUFFLE RULES (managed; do not edit; checksum=…; created-by-snuffle=…) --> … <!-- END SNUFFLE RULES -->), mirroring the Supbuddy convention and co-existing with it; Cursor gets .cursor/rules/snuffle-rules/<id>.mdc. The merge is surgical: content outside the block (including a co-resident Supbuddy block) is preserved byte-for-byte. The set is secret-scanned on egress (fail-closed, all-or-nothing) before any write. The honest trade-off: a projected rule is a checksummed COPY of canon in a committed file — drift is detected (via the checksum), not auto-healed; project-rules/link/sync regenerate (canon wins), a hand-edit inside the block is reported then replaced, and hand-edits outside the block always survive.
Projection writes committed rule files; no agent capability
Off by default; needs projection.enabled. Emits the resolved winner per id, secret-scanned first.
Exits 0 by default (advisory); exits non-zero only under projection.checkGatesCi.
Drift detection
There are two distinct drift axes. The first is override drift (L4, packages/core/src/resolution/drift.ts). An override resource records the upstream content hash it was based on (overrides.baseHash); the tripwire compares that against the upstream's current hash. It is a high-recall tripwire, not a verdict — a status of upstream-changed just means "look closer" (the semantic classifier + conversational HITL of a later phase decide whether the override is actually stale). Statuses are ok, upstream-changed, upstream-missing, and no-base. Detection has two surfaces: a pull-time full scan (every override checked against current upstreams, scanDrift) and a push-time preflight that only re-checks overrides referencing the upstreams that just changed (driftAffecting). It is wired into the sync pipeline.
The second axis is projection drift (from rule projection): each projected managed block carries a checksum, and snuffle doctor (plus snuffle project-rules --check) recomputes the on-disk block's inner checksum and compares it both to the recorded marker checksum (hand-edit drift) and to freshly-resolved canon (staleness). This readout is advisory — it prints a line but never flips doctor's exit code — and staleness is computed through the identical resolver + projectRules path that a real projection would use, so doctor's verdict cannot disagree with what project-rules would actually write.
"Run snuffle doctor and tell me if any overrides drifted from upstream or any projected rules are stale."
Reports override drift (via sync) and projection staleness advisory lines; exit is on doctor's own criteria.
Pull-time full drift scan runs; upstream-changed flags overrides to look at, it is not a verdict.
Secret scanning (fail-closed)
Secret scanning is a fail-closed gate on every write/egress surface (packages/secrets/src/scan.ts). scanText runs the default rule set over the content and returns findings that never carry the secret value — only rule id, description, 1-based line, column, and the matched length. A scanner that echoed secrets to logs/JSON would itself be a leak, so this is a deliberate invariant; formatHits output is therefore safe to display.
The scan runs before capture writes land (the MCP record tools), on sync push and — after review hardening — symmetrically on every pull (including solo/dev, no key needed), on snuffle promote, and on the rule-projection egress (the final on-disk bytes of every artifact, aborting the whole projection on any finding before any write). Because resolution materializes source:/override bodies, the scan runs on the resolved bytes, not just the on-disk source.
The one escape hatch is a # snuffle:allow directive on the matched line (or a standalone comment line directly above it), and it is honored only when the caller passes allowDirectives — which the code does solely for user-authored, user-scope content. A team or project resource can never whitelist its own secret, and a projected file (a shared artifact) is scanned like team scope. The honest limit: scanText is a credential detector, not a prompt-injection or display sanitizer — that is separate, harder work.
"Record a skill for calling our billing API." (agent must reference the key, not paste it)
The record_* sink secret-scans before the write lands; findings never echo the value.
(edit the user-scope markdown yourself)
allowDirectives is honored only for user-authored user-scope content, never team/project.
Search & the index (hybrid, learned embedder opt-in)
The MCP toolset is a progressive search surface (R21): start cheap and broad, drill only where it pays. overview (totals + counts by kind/scope + flat summaries) is the entry point; structured_filter filters by kind/scope/tags (must match ALL)/free text; graph_traverse walks [[wikilink]] / phase / override edges from a ref to a given depth and direction; and fetch_resource is the deep-fetch step (full body + frontmatter) used last. context_for_file returns the rules/specs/skills that declare a path via applies_to globs, ranked most-specific first — deterministic, no search needed, meant to be called before editing a file (its human twin is snuffle why). assemble_context seeds via hybrid search, expands one hop over the reference graph, ranks, then packs to a token budget (full body, else summary, else dropped) — extractive, no synthesis (the human twin is snuffle pack). All of these except semantic search work file-only with no index, so the server is useful offline.
Semantic search is the hybrid layer. semantic_search is registered only when an index is available (otherwise callers gracefully fall back to the file-only tools). It fuses a vector ranking and a lexical ranking via Reciprocal Rank Fusion (packages/core/src/index/rrf.ts) — RRF combines ranked lists without needing comparable scores (score = sum of 1/(k+rank), k=60). The index is built with snuffle index (Postgres + pgvector + FTS).
The embedder is pluggable and the default is free and offline: a HashingEmbedder. snuffle embed setup opts into a learned embedder — local (Ollama, free) or remote (OpenAI / Voyage) — storing non-secret config only (provider/model/base URL/dimensions and the env-var NAME of the key, never the key). embed test embeds a probe and reports observed dimensions; embed show prints resolved settings without the key. Because switching providers changes vector dimensions/identity, a persisted index must be rebuilt with snuffle index afterward. snuffle ask answers from your own canon with ranked citations.
"Search our playbook for how we handle monetary amounts and cite the resources."
Agent calls semantic_search (vector+lexical fused via RRF) when an index exists; else file-only fallback.
Embedder/index config has no agent equivalent
Default is the free offline HashingEmbedder; switching dimensions requires rebuilding the index.
"Before I edit packages/billing/charge.ts, load the rules and specs that govern it."
Agent calls context_for_file (deterministic applies_to glob match, most-specific first).
// your agent, over MCP
How an agent connects to and uses snuffle over MCP
snuffle serves your .snuffle/ canon to a coding agent over the Model Context Protocol on stdio. The server is stood up by snuffle mcp (defined in packages/cli/src/commands/mcp.ts): stdout is the MCP protocol channel and all logs/readiness go to stderr. Under the hood it loads the project store (merging your user-scope canon and any resolved team references), builds a KbService (packages/core/src/mcp/service.ts), and registers the toolset from createKbMcpServer (packages/core/src/mcp/server-factory.ts). Before it will start it runs a fail-closed secret scan of the store and refuses to serve if any credential is found.
You don't launch snuffle mcp by hand day-to-day. You wire it into an agent once with snuffle claude init, snuffle cursor init, or snuffle codex init. Each writes a server entry (npx -y snuffle mcp) into the agent's MCP config file (.mcp.json for Claude Code, .cursor/mcp.json for Cursor, Codex's TOML for Codex). From then on the agent boots snuffle automatically and the tools appear namespaced (e.g. mcp__snuffle__semantic_search).
The toolset degrades gracefully by capability. The four progressive-search tools (overview, structured_filter, graph_traverse, fetch_resource) plus context_for_file and assemble_context are always registered. semantic_search is registered only when a semantic index is available (service.hasSemanticSearch()). The capture/write tools (record_skill, record_decision, record_rule, record_workflow) are registered only when a capture sink is injected AND the principal has project write — a read-only serve simply omits them.
When capture is enabled the server also emits MCP instructions (the CAPTURE_INSTRUCTIONS string). This is guidance the agent reads on connect: it tells the agent the KB is writable, to call record_skill when it learns a reusable technique, record_decision for consequential choices (which also satisfies the decision-before-merge gate proactively), record_rule for a must/never, and record_workflow for a repeatable process — and that everything written is secret-scanned, so reference credentials, never paste them.
Wire snuffle into Claude Code by running `npx snuffle claude init`.
One-time wiring. Also: snuffle cursor init / snuffle codex init. Remove with `snuffle claude remove`.
(agent launches this automatically once wired via `snuffle claude init`)
stdout is the MCP channel; refuses to start if the store contains secrets.
Are the snuffle MCP tools connected? List what snuffle knowledge tools you can call.
semantic_search only appears when an index exists; the record_* tools only when the serve is writable.
overview — cheap entry point (totals + counts + summaries)
overview is the cheapest first call in progressive search. It returns totals, counts broken down by kind and scope, and a flat list of lightweight resource summaries. The intent is: start here to see the shape of the canon, then narrow with structured_filter or graph_traverse before you ever fetch a full body. It takes no arguments and calls service.overview().
$ (agent-only, over MCP)
Triggers overview. No inputs; returns counts by kind/scope plus summaries.
structured_filter — filter by kind / scope / tags / text
structured_filter narrows the canon by metadata: kind (one or many), scope (one or many), tags (must match ALL supplied), and/or free text matched across id, title, and description. It returns lightweight summaries only — not full bodies. It is the deterministic narrowing step after overview and is stronger for precise metadata than free-form search. Backed by service.filter().
$ (agent-only, over MCP)
Triggers structured_filter with kind=rule, scope=team.
$ (agent-only, over MCP)
tags must ALL match; free text also matches id/title/description.
graph_traverse — walk the reference graph
graph_traverse starts from a resource ref (a bare id like skills/foo or a scoped team:rules/base) and walks the reference graph — [[wikilink]], phase, and override edges — out to a given depth (1–5, direction outgoing/incoming/both). It returns the reached resource summaries each tagged with its distance from the start. It's how an agent discovers what a given piece of canon connects to (or what points at it). Note: quarantined resources are not discoverable by traversal — they're only reachable by an explicit direct ref. Backed by service.graphTraverse().
$ (agent-only, over MCP)
Triggers graph_traverse with ref, depth=2 (direction defaults). Reached items carry a distance.
$ (agent-only, over MCP)
direction=incoming.
fetch_resource — the deep-fetch step (full body + frontmatter)
fetch_resource is the only tool that returns full content: the complete body plus frontmatter for a resource ref (id or scope:id). It's deliberately the last step — you narrow with the cheap tools first, then deep-fetch the one or few resources you actually need. Backed by service.fetch(); returns whether it was found and the resolved full resource(s).
$ (agent-only, over MCP)
Triggers fetch_resource with that ref; returns body + frontmatter.
semantic_search — hybrid vector + lexical search (CLI cousin: ask)
semantic_search runs a hybrid search — vector plus lexical, fused via Reciprocal Rank Fusion — over resource text, returning ranked summaries with scores. Use it when you don't know exactly where to look; then narrow with structured_filter / graph_traverse and fetch_resource. Deprecated resources are de-ranked (still findable, sort below equivalents); draft/archived never reach the index. This tool is only registered when a semantic index is available.
The CLI cousin is snuffle ask "<question>", which builds the effective KB (project + your user scope + references) and runs the same hybrid RRF search, returning the top resources as citations plus the leading snippet of the best match. It is extractive — no LLM prose synthesis.
Search snuffle for how we handle database migrations.
Agent triggers semantic_search; `ask` is the terminal equivalent (same hybrid RRF search).
Search snuffle for our retry strategy for flaky tests, top 5 results.
k is 1–50.
context_for_file — why does this file have rules? (CLI cousin: why)
context_for_file takes one or more file paths and returns the rules/specs/skills that govern them via their applies_to globs — ranked most-specific first, each with a snippet of the why. It's deterministic (pure metadata + glob, no search or LLM). The guidance is to call it BEFORE editing a file so the relevant policy and reasoning load into context. Decisions also surface when the project's coverage-map (.snuffle/coverage-map.json) anchors them to the path. Backed by service.contextForFile().
The CLI cousin is snuffle why <path> [<path>...], the human side of the same tool — same deterministic glob-anchored lookup, including coverage-map-anchored decisions.
Before I edit src/auth/session.ts, use snuffle to show the rules and decisions that govern this file.
Agent triggers context_for_file; `why` is the terminal equivalent. Ranked most-specific first.
What snuffle rules govern the files I'm changing: src/db/migrate.ts and src/db/schema.ts?
Accepts multiple paths; k caps the count (1–50).
assemble_context — token-budgeted context pack (CLI cousin: pack)
assemble_context takes a task or area description and assembles a curated, token-budgeted bundle of the most relevant canon. It seeds via hybrid search, expands one hop over the reference graph, ranks, then packs to the token budget — full body where it fits, else summary, else dropped — returning ordered resources, citations, and a drop list. It is extractive: it selects and packs canon, it does not synthesize prose. Inputs: task (required), tokenBudget (256–200000), and optional kind/scope filters, seedK, and expandDepth. Backed by assembleContextPack().
The CLI cousin is snuffle pack "<task>" [--budget N] [--kind K] [--scope S]. It reuses the ask load path; a semantic index sharpens the seeds but isn't required (it degrades to a lexical fallback).
Assemble a snuffle context pack for the task: add rate limiting to the public API.
Agent triggers assemble_context; `pack` is the terminal equivalent. Returns ordered resources + citations + a drop list.
Pack snuffle context for onboarding a new service, budget about 4000 tokens, workflows only.
tokenBudget 256–200000; optional kind/scope/seedK/expandDepth.
record_skill — capture a reusable how-to
record_skill captures a reusable technique as a plain-markdown skill in the project knowledge base (project scope). The MCP instructions tell the agent to call it the moment it learns a repeatable technique. Inputs: title (required), body (required markdown), optional description, optional tags. What lands on disk: a file at project/skills/<slug-of-title>.md with id skills/<slug>, frontmatter (kind: skill, id, title, scope: project, plus description/tags when given), and the body. Before it's written the content is validated to round-trip through the loader's parser and is secret-scanned (fail-closed) — a captured file can't poison the next load, and credentials are rejected. Registered only on a writable serve.
$ (agent-only, over MCP)
Writes project/skills/reset-the-local-supabase-db.md (frontmatter + body). Secret-scanned first.
record_decision — log a consequential choice (satisfies the merge gate)
record_decision captures a consequential decision — the what, the why, the trade-offs — as an append-only decision-log entry, and proactively satisfies the decision-before-merge gate. Inputs: title (required), optional body, optional deciders. If you omit the body, snuffle writes a MADR scaffold (Context / Decision / Consequences) for you to fill in. What lands on disk: an auto-numbered file project/decisions/NNNN-<slug>.md (append-only sequence) with id decisions/NNNN-<slug>, frontmatter including status: proposed and any deciders. Secret-scanned and parser-validated before write. Registered only on a writable serve.
$ (agent-only, over MCP)
Writes an auto-numbered project/decisions/NNNN-*.md with status: proposed. Also satisfies the decision-before-merge gate.
Scaffold a snuffle decision titled 'Adopt trunk-based development' — leave the body as a template for me.
Omitting the body writes a MADR Context/Decision/Consequences scaffold. `snuffle decision` is the CLI scaffolder.
record_rule — capture a must / never
record_rule captures a rule — a must or never the team should always follow — as plain markdown at project scope. Inputs: title (required), body (required), optional description, optional tags, and optional appliesTo code-path globs. When you supply appliesTo, the rule code-anchors to the files it governs (which is exactly what context_for_file / snuffle why later surface for those paths). What lands on disk: project/rules/<slug>.md with id rules/<slug>, frontmatter (kind: rule, scope: project, plus applies_to globs when given). Secret-scanned and parser-validated before write. Registered only on a writable serve.
$ (agent-only, over MCP)
Writes project/rules/*.md with applies_to: ['src/payments/**']; those globs power `snuffle why`.
record_workflow — capture a repeatable multi-step process
record_workflow captures a repeatable multi-step process as a workflow (a hero kind) at project scope. The one required structural input is phases — at least one — each with a name and optional description, referenced resource ids, and optional gates (each gate has id, type soft|hard, description, and an optional check). Also accepts title (required), optional body, description, and tags. What lands on disk: project/workflows/<slug>.md with id workflows/<slug>, a rendered phases: YAML block (including any gates), and frontmatter. The parser rejects a phase-less workflow, and content is secret-scanned and parser-validated before write. Registered only on a writable serve.
$ (agent-only, over MCP)
Writes project/workflows/ship-a-feature.md with a phases: block + gates. At least one phase is required.
// cli reference
Every command
The full snuffle command surface. Flip to Agent mode to see the prompt that drives the same outcome through your coding agent. Setup and wiring commands are terminal-only and say so.
// setup
snuffle setupHost-level bootstrap: scans your HOME agent config dirs (Claude/Cursor/Codex) for skills, subagents, and MCP servers, previews them, and (on --apply or -y) records each as canon at USER scope (~/.snuffle/user/), writing ~/.snuffle/config.json if absent. Dry-run first; each rendered resource is secret-scanned before write. --project then delegates to `onboard` for the cwd.
snuffle setup [--apply] [--project] [-y]- --apply
- Actually import into user scope (else dry-run that writes nothing; -y also applies)
- --project
- After user-scope setup, also onboard the current project (delegates to `onboard`)
- -y, --yes
- Assume yes; in non-TTY/CI this is the way to apply without a prompt
This scans your machine's agent config dirs and installs snuffle wiring; it has no in-agent equivalent. Run `npx @snuffle/cli setup` in your agent's terminal.
snuffle link(Re)create the project's agent-to-KB projections — regenerate the MCP config for each agent the project uses: .mcp.json (Claude) and .cursor/mcp.json (Cursor). Also re-projects opt-in always-on rules for the linked agents. Idempotent; generates config files rather than symlinks.
snuffle linkRe-create the snuffle MCP wiring for this project's agents by running `npx snuffle link`, then reload me to pick it up.
snuffle mcpServe the project's .snuffle knowledge base to an agent over stdio (MCP). This is the runtime the agent adapters register (npx -y snuffle mcp). stdout is the MCP protocol channel; readiness/logs go to stderr; it merges your user scope on serve. Fail-closed: refuses to start if any served resource carries a secret.
snuffle mcp [dir]- [dir]
- Positional: serve a store dir relative to cwd instead of the project's .snuffle/
This is how your agent connects to snuffle — the wired MCP entry runs `npx -y snuffle mcp`. You don't invoke it by hand; `snuffle claude|cursor|codex init` wires it for you.
snuffle claudeWire snuffle into Claude Code. `init`/`refresh`/`sync` register the snuffle MCP server into the project's .mcp.json (idempotent, preserves other servers), install the snuffle operating skills into .claude/skills/, and re-project opt-in always-on rules. `remove` unwires the server, removes the skills, and un-projects the rules.
snuffle claude <init|refresh|sync|remove>- init
- Wire the MCP server + install operating skills + project rules
- refresh
- Re-apply the wiring/skills to the current version (alias of init's projection)
- sync
- Same as refresh — re-project the wiring (the MCP server serves live canon)
- remove
- Unwire the server, remove operating skills, un-project rules
Wire snuffle into Claude Code by running `npx snuffle claude init`, then reload the project so I pick up the MCP server and operating skills.
snuffle cursorWire snuffle into Cursor. `init`/`refresh`/`sync` register the snuffle MCP server into .cursor/mcp.json (idempotent), install the snuffle operating skills, and re-project opt-in always-on rules; `remove` unwires it.
snuffle cursor <init|refresh|sync|remove>- init
- Wire the MCP server + install operating skills + project rules
- refresh
- Re-apply the wiring/skills to the current version
- sync
- Same as refresh — re-project the wiring
- remove
- Unwire the server, remove operating skills, un-project rules
Wire snuffle into Cursor by running `npx snuffle cursor init`, then reload Cursor to pick up the MCP server.
snuffle codexWire snuffle into Codex. Codex's MCP config is global (~/.codex/config.toml), so this is not project-scoped; the server resolves .snuffle from the cwd Codex launches it in. `init`/`refresh`/`sync` register the server, install operating skills into ~/.codex/prompts/, and project always-on rules into AGENTS.md; `remove` unwires it.
snuffle codex <init|refresh|sync|remove>- init
- Register the global MCP server + install prompts + project rules into AGENTS.md
- refresh
- Re-apply the wiring/prompts to the current version
- sync
- Same as refresh — re-project the wiring
- remove
- Unregister the server, remove prompts, un-project rules
Wire snuffle into Codex by running `npx snuffle codex init`, then restart Codex to pick it up.
// in a project
snuffle initScaffold a .snuffle/ knowledge base in the current project: creates user/, project/, team/ scope dirs, a welcome note, config.json, and .gitignore for derived state. Idempotent — existing files are left untouched.
snuffle initScaffold a snuffle knowledge base in this project by running `npx snuffle init` — then I can read/write canon over MCP.
snuffle onboardIntegrate an existing project in one command: scaffold .snuffle/, import existing agent config as canon (.claude/skills, CLAUDE.md, docs/adr, .cursor/rules), wire the MCP server into .mcp.json, install the decision-before-merge gate (GitHub Action + lefthook), wire the advisory Claude gate hook, and optionally configure AI drafting + learned embedding. Idempotent.
snuffle onboardOnboard this existing project into snuffle by running `npx snuffle onboard` — it will import my existing docs/skills as canon, wire the MCP server, and install the decision gate.
// knowledge
snuffle importBootstrap project canon from existing docs: discovers docs/, ADRs, specs, .claude/skills, .cursor/rules, and AGENTS.md/CLAUDE.md, then copies each into .snuffle/project/<kind>/ as canon with provenance. Copy, not relocate — originals untouched. Each candidate is secret-scanned before import; a file with a secret is skipped. Idempotent; candidates already covered byte-for-byte by user scope are reused by reference.
snuffle importImport my existing docs, ADRs, and specs into snuffle canon (run `npx snuffle import` in the terminal) so you can search them over MCP.
snuffle discoverDry-run the import scan: list the docs/specs/ADRs/skills/rules `import` would bring in as canon, grouped by kind, without copying anything. Read-only preview.
snuffle discover [dir]- [dir]
- Positional: scan a directory other than the cwd
Show me what `snuffle import` would bring in as canon for this project — run `npx snuffle discover` and summarize the candidates by kind.
snuffle indexBuild/refresh the semantic index from the project KB (merging user scope). The index is derived and rebuildable. The in-memory default is transient (built on serve), so this only persists for pglite/postgres. --pg <url> forces the remote postgres backend with that connection string. A remote index is secret-scanned first and refuses on any finding.
snuffle index [--pg <url>]- --pg <url>
- Force the postgres backend with this connection string (overrides the configured driver)
Rebuild the snuffle semantic index by running `npx snuffle index`.
snuffle promoteRelocate a project resource's bytes to the user home store (~/.snuffle/user/<kind>/), leaving a body-less source: reference stub in the project. The promoted resource then follows you into every project and is reused by reference, not copied. Fail-closed: refuses when the ref is not project-scoped, is already a reference, is a governed kind (rule/hook), is a decision, --to is not user, holds a secret, or would not re-parse.
snuffle promote <ref> [--to user]- <ref>
- The project resource to promote (positional)
- --to <user>
- Target scope — only "user" is supported (the default)
Promote the project skill `project:my-git-workflow` to my user scope so it follows me across projects — run `npx snuffle promote project:my-git-workflow`.
snuffle askAnswer a question from your own canon on the CLI without leaving the terminal. Extractive (no LLM): builds the effective KB (project + user scope + references) and runs the same hybrid lexical+vector RRF search the MCP semantic_search tool uses, returning the top-ranked resources as citations (scope:id + relpath) plus the leading snippet of the best match.
snuffle ask "<question>" [--k N]- "<question>"
- The question (positional; joined from all positionals)
- --k <N>
- Number of ranked citations to return
Search our snuffle canon for how we handle database migrations and cite the top sources. (Use the snuffle MCP semantic_search / assemble_context tools.)
snuffle packAssemble a token-budgeted context bundle for a task: the most relevant canon, ranked, graph-expanded, and packed to a budget, with citations and a drop list. Extractive and deterministic. A semantic index sharpens seeds but isn't required (degrades to lexical fallback).
snuffle pack "<task>" [--budget N] [--kind K] [--scope S]- "<task>"
- The task to build context for (positional)
- --budget <N>
- Token budget for the bundle (defaults to DEFAULT_PACK_BUDGET)
- --kind <K>
- Filter to one resource kind (e.g. skill, rule, decision, spec, note)
- --scope <S>
- Filter to one scope (user | project | team)
Assemble a context bundle from snuffle for the task 'add rate limiting to the API', budget ~4000 tokens. (Use the snuffle MCP assemble_context tool.)
snuffle whyShow the knowledge that governs a file: the rules, specs, and skills whose applies_to globs match it, ranked most-specific first, each with the leading snippet of the why. Decisions surface too when the coverage-map anchors them to the path. Deterministic, extractive — the human side of the context_for_file MCP tool.
snuffle why <path> [<path>...]- <path>...
- One or more file paths to explain (positional)
Which snuffle rules, specs, and skills govern src/auth/session.ts? (Use the snuffle MCP context_for_file tool.)
snuffle embedConfigure the embedder for semantic search. The free offline hashing embedder is the default; this opts in a local (Ollama) or remote (OpenAI/Voyage) learned embedder — non-secret config only (provider/model/base URL/dimensions and the env-var NAME of the key). `test` embeds a probe and reports observed dimensions; `show` reports resolved settings. Switching providers changes vector identity, so re-run `snuffle index` afterward.
snuffle embed <setup|test|show> [--provider P] [--model M] [--dimensions N] [--base-url URL] [--api-key-env NAME]- setup
- Configure the embedder (interactive, or via flags; `hashing` clears back to the free default)
- test
- Embed a probe and report the observed dimensions
- show
- Show resolved embed settings (never prints the key)
- --provider <P>
- Embedding provider (e.g. hashing, ollama, openai, voyage)
- --model <M>
- Embedding model id
- --dimensions <N>
- Vector dimensions
- --base-url <URL>
- Provider base URL
- --api-key-env <NAME>
- Env-var NAME holding the key (never the key itself)
Configure a learned embedder for snuffle semantic search by running `npx snuffle embed setup`, then `npx snuffle index` to rebuild.
// governance
snuffle project-rulesMaterialize the resolved always-on rule set into each wired agent's native rule surface (Claude/Cursor/Codex). Additive to MCP: only kind:rule projects; the searchable corpus stays on MCP. Projection is opt-in (config projection.enabled) unless --agent forces it. --check reports drift without writing.
snuffle project-rules [--agent claude|cursor|codex|all] [--check] [--json]- --agent <claude|cursor|codex|all>
- Target agents (comma-separated); forces projection even if disabled in config. Omit for every wired agent
- --check
- Report projection drift (hand-edited/stale native rule files) without writing
- --json
- Machine-readable report
Project the always-on rules from snuffle canon into my native rule files for Claude and Cursor by running `npx snuffle project-rules`, then reload the rules.
snuffle decisionScaffold an append-only MADR decision-log entry: computes the next sequence number in .snuffle/<scope>/decisions/ and writes a kind:decision markdown file (refuses to overwrite). This is the resource the decision-gate looks for in a changeset before merge. Default scope is team.
snuffle decision "<title>" [--scope team|project|user] [--decider <name>]...- "<title>"
- Decision title (positional, required)
- --scope <team|project|user>
- Target scope's decisions/ dir (default team)
- --decider <name>
- A decider name; repeatable for multiple deciders
Record a decision in snuffle titled 'Adopt Postgres for the index' at project scope with deciders criss and alice, then fill in Context/Decision/Consequences. (Use the snuffle MCP record_decision tool.)
snuffle gateRun the decision-before-merge gate locally: diffs against a base ref (--base, else $GATE_BASE, else origin/main) and passes iff the changeset adds/modifies a kind:decision resource; also enforces decision immutability. Exit 0 when satisfied, 1 otherwise — the same check the GitHub required status runs. `--hook` is the Claude Code PreToolUse mode (agent shift-left): reads the tool payload on stdin and nudges (never hard-blocks) on a publish command with no decision.
snuffle gate [--base <ref>] [--hook]- --base <ref>
- Base ref to diff against (else $GATE_BASE, else origin/main)
- --hook
- Claude Code PreToolUse mode: read the tool payload on stdin, advise on publish commands (never blocks)
Check whether this branch satisfies the decision-before-merge gate by running `npx snuffle gate` before I open the PR.
snuffle coverageReport undocumented reasoning: first-parent changesets that landed with no decision, plus high-churn code areas with no decision in the window. Advisory — always exits 0 (the gate stays the only hard gate). Read-only, local-first, no LLM. Commit subjects are secret-scanned and masked before printing.
snuffle coverage [--since <ref>] [--base <ref>] [--limit N] [--min-churn N] [--area-depth N]- --since <ref>
- Window start ref
- --base <ref>
- Base ref for the diff window
- --limit <N>
- Cap the number of reported changesets
- --min-churn <N>
- Minimum churn for a high-churn area to be flagged
- --area-depth <N>
- Directory depth used to group high-churn areas
Show me where our reasoning went uncaptured since v1.0.0 and summarize the thin-coverage areas.
snuffle backfillPropose decision entries for un-recorded architectural history. Dry-run by default (writes nothing). --accept scaffolds selected proposals as draft decisions through the fail-closed capture sink (you review + commit); --only limits which, --decline records groups so they never resurface. Deterministic/heuristic by default; --draft <heuristic|llm> chooses the prose drafter. Never auto-commits.
snuffle backfill [--base <ref>] [--since <ref>] [--threshold N] [--accept [--only <ids>]] [--decline <ids>] [--scope <s>] [--draft heuristic|llm]- --base <ref>
- Base ref for the git walk window
- --since <ref>
- Since ref for the git walk window
- --threshold <N>
- Significance threshold for proposing a group
- --accept
- Scaffold the selected proposals as draft decisions (else dry-run)
- --only <ids>
- Comma-separated group ids to accept (with --accept)
- --decline <ids>
- Group ids to record as declined so they never resurface
- --scope <user|project|team>
- Scope for accepted entries (default team)
- --draft <heuristic|llm>
- Prose drafter for accepted entries (heuristic default; llm uses the configured provider)
Propose decision entries for our un-recorded architectural history since v1.0.0, then scaffold the ones I confirm.
snuffle nudgeAdvisory: when significant work is about to be pushed with no decision recorded, print the exact `snuffle decision` command to run while it's fresh. Never blocks and always exits 0. Wired as an advisory pre-push lefthook and surfaced in doctor. nudge.enabled:false silences it.
snuffle nudge [--base <ref>]- --base <ref>
- Base ref to diff against (else $GATE_BASE, else origin/main)
Check whether I should record a decision for the work on this branch by running `npx snuffle nudge`.
snuffle draftConfigure the AI provider that drafts decision prose for `backfill --draft llm`. `setup` picks the strategy (keyless heuristic/agent modes, or an llm provider) — non-secret config only (provider/model/base URL and the env-var NAME of the key). `test` pings the provider; `show` reports the resolved settings without printing the key.
snuffle draft <setup|test|show> [--mode heuristic|agent|llm] [--provider P] [--model M] [--base-url URL] [--api-key-env NAME]- setup
- Configure the drafting strategy (interactive, or via flags)
- test
- Ping the configured provider
- show
- Show resolved draft settings (never prints the key)
- --mode <heuristic|agent|llm>
- Default drafting strategy; heuristic/agent are keyless (drop provider flags)
- --provider <P>
- LLM provider (required with --mode llm)
- --model <M>
- Model id
- --base-url <URL>
- Provider base URL
- --api-key-env <NAME>
- Env-var NAME holding the key (never the key itself)
Configure snuffle's decision-prose drafter by running `npx snuffle draft setup`, then `npx snuffle draft test` to verify.
snuffle approveClear a quarantined high-risk hook so it activates. A hook that declares a non-snuffle command is quarantined by admission (inert: hidden, not synced, not indexed) until an admin approves it here. Approval is admin-gated, records the hook's current content hash, and is content-bound — any later edit re-quarantines it. Distinct from `team approve <email>`.
snuffle approve <hook-id>- <hook-id>
- The quarantined hook's id or scope:id (positional, required)
Approve the quarantined snuffle hook so it activates by running `npx snuffle approve project:pre-commit-lint` (admin-gated).
// teams
snuffle syncSync team-scope canon (.snuffle/team/**.md) with the configured backend (fs or Scaleway) via a hash-based 3-way reconcile against a manifest. Both sides changed → conflict (exits non-zero) unless a strategy forces a winner. Egress is fail-closed: every local team file is secret-scanned before any push. When an owner trust-root is pinned, canon is signed on push and verified on pull. --watch/--interval turn it into a daemon; --git-snapshot adds an additive git snapshot after a clean team sync; --classify forces the drift classifier.
snuffle sync [local-wins|remote-wins] [--watch] [--interval N] [--git-snapshot] [--classify heuristic|llm]- local-wins | remote-wins
- Conflict strategy: force a winner instead of surfacing the conflict (positional)
- --watch
- Run as a daemon: single-flight passes triggered by fs changes under team/ (and/or the interval timer) until SIGINT/SIGTERM
- --interval <N>
- Timer-driven daemon interval (e.g. 5s, 1m, 30000ms); also implies daemon mode
- --git-snapshot
- After a clean team-mode sync, take an opt-in additive git snapshot of verified canon
- --classify <heuristic|llm>
- Force the drift classifier (heuristic is free; else auto from config)
Keep our team's snuffle canon in sync by running `npx snuffle sync`.
snuffle team createCreate a team and pin THIS device's keychain-backed signing key as the team's owner trust-root (public SPKI written to config; private key never leaves the keychain). Prints the owner-key fingerprint to publish out-of-band so a first joiner can confirm it. Refuses if a team is already configured.
snuffle team create <name> [--broker <shared-store-url>]- <name>
- Team name (positional, required)
- --broker <url>
- Shared-store URL for the team (else set team.broker in config before inviting)
Create our snuffle team by running `npx snuffle team create acme-eng` — it pins this device's key as the owner trust-root.
snuffle team inviteMint a signed redemption code (owner-only) carrying a pre-approval allow-list (emails) or "any" (open). Open invites are bearer tokens and default to a shorter TTL (1 day) than listed invites (7 days). Every join is still queued for owner approval — a self-asserted email is a label, not a control. Prints the `npx snuffle join <code>` line and the invite nonce (for later revoke-invite).
snuffle team invite (--allow <emails> | --open | <github-login>) [--ttl-days N] [--team <name>] [--broker <url>]- --allow <emails>
- Comma-separated pre-approved emails (listed invite)
- --open
- Open invite anyone can redeem (bearer; defaults to a 1-day TTL)
- <github-login>
- Back-compat: a bare login becomes a single allow entry (positional)
- --ttl-days <N>
- Invite lifetime in days (default 7; open defaults to 1 unless set)
- --team <name>
- Override team identity instead of reading config.team
- --broker <url>
- Override the shared-store URL for this invite
Mint a snuffle team invite for alice@acme.com and bob@acme.com by running `npx snuffle team invite` — then share the printed join line.
snuffle team join(the `join` command) Verify an invite code against the pinned owner trust-root (no TOFU) and queue a self-signed pending join request into the shared store. Membership is conferred only by the owner's approval, never by the joiner. First join with no pinned key: shows the owner-key fingerprint and refuses until you confirm it out-of-band and re-run with --pin-owner-key <fp>. --as sets the self-asserted email label.
snuffle join <code> [--as <email>] [--pin-owner-key <fingerprint>]- <code>
- The invite redemption code (positional, required)
- --as <email>
- Self-asserted email label on the queued request (not a control)
- --pin-owner-key <fingerprint>
- First-join only: confirm the owner-key fingerprint (matched out-of-band) to pin the trust-root
Join the snuffle team with my invite code by running `npx snuffle join <code>` — the owner then approves my queued request.
snuffle team membersList the effective member set by folding the owner-signed membership log (read-only), showing each member's subject, capability, and bound device-key fingerprint.
snuffle team membersShow the approved members of our snuffle team by running `npx snuffle team members`.
snuffle team pendingList unresolved, self-verified, live join requests not yet approved/denied (read-only). All emails are UNVERIFIED by the owner; any GitHub-OAuth note is requester-submitted evidence, not owner-verified. Attacker-controlled display fields are sanitized; the queue has a scan ceiling with a truncation warning.
snuffle team pendingShow pending snuffle join requests I need to approve by running `npx snuffle team pending`.
snuffle team approveOwner-only: append an owner-signed membership event admitting an email, binding the member's device key from their live pending request (fails closed and requires --key if multiple distinct device keys claim the email). Binds the approval to the redeemed invite's allow-list. Requires a live pending request for that email.
snuffle team approve <email> [--key <fingerprint>]- <email>
- Email to approve (positional, required)
- --key <fingerprint>
- Disambiguate when multiple device keys claim the email (confirmed out-of-band)
Approve alice@acme.com into our snuffle team by running `npx snuffle team approve alice@acme.com`.
snuffle team denyOwner-only: append an owner-signed membership event denying a queued email (a deny tombstone). Does not rescind the shared storage credential.
snuffle team deny <email>- <email>
- Email to deny (positional, required)
Deny the pending join request from spammer@evil.com by running `npx snuffle team deny spammer@evil.com`.
snuffle team revokeOwner-only: append an owner-signed revoke tombstone for a member. When sync.credentials.provider is per-user-iam, it also chains a per-user credential revocation (disables the member's scoped IAM user) best-effort. Note: a revoke does NOT rescind a shared storage credential — rotate that separately.
snuffle team revoke <email>- <email>
- Email to revoke (positional, required)
Revoke bob@acme.com's membership by running `npx snuffle team revoke bob@acme.com` — if we use per-user IAM it also disables their scoped credential.
snuffle team revoke-inviteOwner-only: append an owner-signed revocation of an invite nonce so the invite can no longer be redeemed and pending requests citing it are dropped. Does NOT rescind the shared storage credential. The nonce is shown in `team invite` output and `join`.
snuffle team revoke-invite <nonce>- <nonce>
- The invite nonce to revoke (positional, required)
Kill the leaked invite by running `npx snuffle team revoke-invite <nonce>` (the nonce printed when the invite was minted).
snuffle team rotate-credentialPrint the MANUAL out-of-band rotation checklist for the shared storage credential. snuffle reads SCW_* from the environment and cannot rotate the IAM key itself — this is honest guidance, not an action. Rotating the shared credential is the only real revocation of canon read/write in v1.
snuffle team rotate-credentialShow me the checklist to rotate our team's shared storage credential by running `npx snuffle team rotate-credential`.
snuffle team provision-credentialOwner-only: create a member-scoped IAM user (read-all canon, write only their own inbox/<memberId>/), return its access key + secret for OUT-OF-BAND delivery (never persisted), and record an owner-signed provisioning marker (no secret). Requires a scaleway backend with per-user-iam configured and the member approved with a bound device key.
snuffle team provision-credential <email>- <email>
- Approved member to provision a scoped credential for (positional, required)
Provision a write-confined credential for alice@acme.com by running `npx snuffle team provision-credential alice@acme.com` — then deliver the printed keys out-of-band.
snuffle team revoke-credentialOwner-only: disable ALL of a member's active scoped IAM users (real per-user revocation) and record owner-signed revoke markers. Documents the residual access window (immediate for the static key; any already-minted STS token lives until its expiry). Requires per-user-iam configured.
snuffle team revoke-credential <email>- <email>
- Member whose scoped credential(s) to disable (positional, required)
Disable alice@acme.com's scoped cloud credential by running `npx snuffle team revoke-credential alice@acme.com`.
snuffle team promoteOwner-only: fold VERIFIED member inbox contributions (inbox/<memberId>/team/…) into the flat canonical team/ tree, owner-signed. Rejects anything unsigned, not signed by that member's bound key, path-unsafe, or secret-bearing. --from <email> restricts promotion to one member's inbox. Distinct from `team approve` (membership).
snuffle team promote [--from <email>]- --from <email>
- Restrict promotion to that approved member's inbox
Promote verified member inbox contributions into our team canon by running `npx snuffle team promote`.
snuffle backendConfigure the cloud backends: the semantic-index driver (memory/pglite/postgres) and the team-sync backend (fs/scaleway), plus which env-var NAMES hold the object-storage credential. Non-secret only — paths, bucket/region/prefix, and credential env NAMES live in config; keys stay in the environment. `show` prints the resolved config; `set` validates the whole config before an atomic write.
snuffle backend <show | set index <driver> | set sync <fs|scaleway> [--path DIR | --bucket B --region R [--prefix P]] [--credentials env|sts] [--access-key-env NAME] [--secret-key-env NAME] [--sts-endpoint URL]>- show
- Report the resolved index + sync backend config (credential env NAMES only, never values)
- set index <driver>
- Set the index driver (memory | pglite | postgres)
- set sync <fs|scaleway>
- Set the sync backend
- --path <DIR>
- fs backend directory
- --bucket <B> / --region <R> / --prefix <P>
- scaleway backend object-storage location
- --credentials <env|sts>
- Credential provider mode
- --access-key-env / --secret-key-env <NAME>
- Env-var NAMES for the storage credential (default SCW_ACCESS_KEY / SCW_SECRET_KEY)
- --sts-endpoint <URL>
- STS endpoint URL (required when --credentials sts)
Point snuffle's team sync at our Scaleway bucket by running `npx snuffle backend set sync scaleway` — check it with `npx snuffle backend show`.
// maintenance
snuffle doctorLoad the knowledge base and report its health: resource counts by kind/scope, parse errors, dangling references, detected secrets, decision-gate readiness for the branch, a proactive-capture nudge, stale code-anchor globs, promote-to-user suggestions, and rule-projection drift. Exit 0 iff clean-exit invariant holds. Defaults to the project's .snuffle/ store.
snuffle doctor [dir]- [dir]
- Positional: a store dir to check instead of the project's .snuffle/
Run a health check on this project's snuffle knowledge base and tell me about any parse errors, dangling references, or detected secrets.
snuffle migrateMove a legacy Phase-0 kb/ store to .snuffle/ (a directory rename, since kb/{user,project,team} maps 1:1). Idempotent — reports nothing to do when there is no kb/, and refuses to overwrite an existing .snuffle/.
snuffle migrateMigrate my legacy kb/ store to .snuffle/ by running `npx snuffle migrate`.
snuffle detachOffboard a scope while keeping usable markdown: removes only derived state (.snuffle/state/), generated agent wiring (each agent tears down its MCP entry + projected rules + gate hook), and credentials. Canon is never touched unless --purge. Default scope is --project; --device drops this device's signing key; --team <name> removes the team from config and keeps its local store frozen (or purges it). Always advises credential rotation (revocation is backend-enforced).
snuffle detach [--project | --team <name> | --device] [--purge]- --project
- Detach this project (default): unwire agents, remove derived state, keep markdown
- --team <name>
- Remove a team from this project's config; keep its local store frozen unless --purge
- --device
- Remove this device's signing key from the keychain
- --purge
- Also wipe the canon markdown (the explicit "wipe it" opt-in)
Offboard this project from snuffle (keeping the markdown) by running `npx snuffle detach`.
snuffle uninstallFull local teardown: project agent wiring + derived state + this device's signing key, keeping your markdown intact. --purge also wipes the .snuffle store. Reversible: re-run `snuffle <agent> init` to re-wire.
snuffle uninstall [--purge]- --purge
- Also wipe the .snuffle store (markdown included)
Remove all snuffle wiring and this device's key while keeping my markdown by running `npx snuffle uninstall`.
snuffle upgradeUpdate the CLI. Honest source-mode: the snuffle package is currently unpublished, so upgrade performs NO npm-registry query and NO package-manager command (a public unscoped `snuffle` could be an unrelated package). It reports the current version and the source-update steps. --check states self-update isn't available yet.
snuffle upgrade [--check]- --check
- Report whether an update is available without installing (source-mode: states self-update isn't wired yet)
Is there a newer snuffle CLI version available?