Authoring a way
How to write a way — let an agent draft it, then the rules that make it installable.
A way is a directory with a way.yaml at its root, plus the files it declares. This page is about
writing one; the manifest itself — every section, who places or validates it — is documented in
The way.yaml.
Let an agent write it
Two authoring skills live in the ways repo, alongside the
schema and the conformance rules they document. They do this page's work for you — and they iterate
against validate and conformance until both are green, so what you get back is a way that
actually installs.
npx skills add incu-tech/ways --skill author-way
npx skills add incu-tech/ways --skill author-way-familyauthor-way— one flow,kind: Way. Works greenfield ("a way for our QA regression process"), but earns its keep as a converter: point it at a folder ofSKILL.mdfiles, Cursor rules, or steering docs and it maps them ontospec.skills[]andspec.knowledge[]with the rightapplicability, infersspec.requiresfrom the tools the process actually names, and asks you about the human gate instead of inventing one.author-way-family— several flows under one identity,kind: WayFamily. Adds what a family changes: which payload is shared versus what stays inside a member, and how member paths anchor.
Both target conformant, not merely schema-valid — the distinction that gates and conformance explains. If you'd rather write the manifest by hand, keep The way.yaml open and mind the rules below.
Rules that bite
- Namespace the name.
metadata.namemust bens/name(incu/dev) — it's the collision key for the lock, the cache, and the registry. Non-namespaced ways are not conformant and won't install. - Ship Binding profiles inside the bundle (e.g.
bindings/github.yaml). Profiles that live outside the way directory don't travel with the install, and bound slots will reportunknowninstead ofpass. - A Binding's identity is
{contract, impl}, not its filename — one impl may satisfy several contracts, so author one profile per contract (snyk-sast.yaml,snyk-sca.yaml). - Bundled paths must exist and stay inside the bundle (no
..) —addpre-flights this and refuses non-self-contained ways. Usesource/packfor third-party content. - Declare
checkcommands for anything that matters.git --version,gh auth status— they run only after the consumer approves your way, and they're what upgrade apassfrom "binary on PATH" to "actually works". - Never put credentials in
bindingConfig— or anywhere else in the bundle. Secrets belong inrequires.env(secret: true): the bind wizard refuses to prompt secret-shaped config keys, and doctor never echoes values. Ways Shield scans every file you ship and refuses the install for a well-known token shape, so a leaked key doesn't reach your consumers — it just means nobody can install your way. For a genuine false positive (quoting a vendor's own published example key in a doc), mark the lineshield:allow— orshield:allow:<rule-id>to scope it to one rule, so a real secret two lines down still blocks. - Pin what your
.mcp.jsonlaunches, and keep hook commands single. Annpx/dlxserver without an exact semver (@latest,@next, a range) and a hook command that chains or substitutes another (;,&,|,`,$(...)) are Shield warnings on every install of your way — visible in the plan, and a hard refusal for any consumer running--strict(which is what CI does). - At least one hard human gate.
approval: user+enforcement: blocking, or the way isn't conformant — by design. - Let slots derive from
skills[].uses. Declare what each skill uses and the slot appears automatically; add aneeds[]entry only to enrich it (adisplayName/descriptionsobind/doctorread human, suggestedoptions, a softenedrequired). Slot ids must be unique within the declaration. - Point contract namespaces at a catalog. A slot's
contract(acme.internal/scanner@1) must resolve — declare its namespace underspec.contracts(ways.devhas a built-in default), or bundle theSlotContract+Bindingyourself. Conformance fails an unknown reference and names the near-miss. See Resolving contracts.
Hooks
spec.hooks[] declares hook packs — auto-executing event handlers — the same way skills[] declares skills: { path } for a
bundled pack (a directory with a hooks.json + scripts) or { source } for an external one. ways
gives them first-class treatment:
- Full-clarity approval. The install plan expands each pack to its
hook <event>[matcher] → <command>lines under the executable surface — you see exactly what runs on which event before approving. A hook file the manifest doesn't declare is disclosed and markedundeclared — not installed. - Placement →
hooks.sh.adddelegates each pack tohooks.sh(the third installer next toskills.shandsteering; override withWAYS_HOOKS_CMD), with the captured agent selection fanned out. - Pinned + inventoried. External hook sources are
{sha, digest}-pinned in the lock (sibling ofskills);ways installreproduces them and refuses on drift, andways list --componentsshows a⚡N hooksmarker.
spec:
hooks:
- path: hooks/gate-guard # PreToolUse on Bash(git commit*) — blocks commits while a gate is unapprovedAuthoring a family
Shipping more than one flow under one identity? Don't fake a single flow — package them as a
WayFamily: spec.members[] references each member way (each keeps
its own flow), and shared skills/knowledge/needs/requires are declared once at the family
level. A family declares no flow of its own.
Validate while you author
npx ways.sh validate ./my-way/way.yaml # schema (any kind — Way, WayFamily, RulePack, …)
npx ways.sh conformance ./my-way/way.yaml # the rules above
npx ways.sh add ./my-way && npx ways.sh doctor # the full consumer experience, locallyThis is the loop the authoring skills run on your behalf — they don't hand back a manifest they haven't put through it.