Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD across contents and paths, upstream package/repo coordinates -> @golem15/msd-core and golem15com/msd-core. Deep links into upstream history, sibling upstream packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is. Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line, package/plugin identity, regenerated lockfile, install-tree fixtures, derived registries and benchmark baseline; migration checksum baseline re-locked (MSD keeps its own install state, so no install had applied the old sums); sort-order and regex-escaped expectations in tests adjusted.
10 KiB
Build Your First Capability
In this tutorial you will build a tiny, fully declarative MSD capability from scratch and watch it act inside your project's loop. By the end you will have a working capability installed, visible in msd capability list, and firing at the plan:pre extension point.
No code is required. Declarative capabilities — those that own only prompt fragments and hook declarations, with no executable hook scripts or MCP servers — require no trust prompt at install time.
We will build a capability called hello-note. It registers a contribution at the plan:pre extension point that injects a short greeting fragment into the planner's prompt and declares that it produces a file called HELLO.md.
Before you begin
You need:
- MSD 1.6.0 or later (
msd --version). - A throwaway project directory. Create one now:
mkdir ~/hello-demo && cd ~/hello-demo
msd init
You will work inside ~/hello-demo for the rest of this tutorial.
Step 1 — Scaffold the capability folder
Capabilities live in a capabilities/<id>/ folder. Create the folder structure:
mkdir -p capabilities/hello-note/fragments
Your project tree now looks like this:
~/hello-demo/
.msd/
capabilities/
hello-note/
fragments/ ← prompt fragments live here
Step 2 — Write the prompt fragment
The fragment is a short Markdown file that will be injected into the planner's prompt when the plan:pre hook fires. Create it:
cat > capabilities/hello-note/fragments/plan-pre.md << 'EOF'
## Hello from hello-note
This planning session was started with the hello-note capability active.
Record a brief note in HELLO.md summarising the plan goal in one sentence.
EOF
Notice that the fragment is plain prose. The capability system reads this file and inlines its text when the capability is loaded, then renders it into the planner's prompt when the loop reaches plan:pre.
Step 3 — Write capability.json
Create the manifest at capabilities/hello-note/capability.json:
{
"id": "hello-note",
"role": "feature",
"version": "0.1.0",
"title": "Hello Note",
"description": "Injects a greeting note at plan:pre and produces HELLO.md.",
"tier": "standard",
"requires": [],
"engines": { "msd": ">=1.6.0" },
"runtimeCompat": { "supported": ["*"], "unsupported": [] },
"skills": [],
"agents": [],
"config": {},
"steps": [],
"contributions": [
{
"point": "plan:pre",
"into": "planner",
"fragment": { "path": "fragments/plan-pre.md" },
"produces": ["HELLO.md"],
"consumes": [],
"onError": "skip"
}
],
"gates": []
}
A few things to notice:
versionis required in 1.6.0. Use semver.engines.msdis a hard gate: MSD will refuse to install or load this capability on any version older than 1.6.0.role: "feature"means this capability adds optional behaviour to the loop — it is not a runtime descriptor. Afeaturecapability must declareruntimeCompat;{ "supported": ["*"] }means "every runtime".- This is a contribution, not a step. A contribution injects a prompt fragment into a named agent role (
into) and needs no dispatch target. A step, by contrast, must carry arefwith exactly one ofskill,agent, orcommand— so a fragment-only injection is always a contribution. That is whystepsis left empty here. into: "planner"names the agent role that receives the fragment.planneris one of the roles published by theplan:preextension point (alongsideresearcherandchecker); the value must be a role that point publishes or the manifest fails validation.producestells the registry that this contribution writesHELLO.md, which lets the registry order hooks and detect unsatisfied dependencies in more complex setups.onError: "skip"means the loop continues even if this contribution fails. For a first capability that is the safe choice.
The fragment is referenced by path. At load time MSD reads the file and inlines its text into the registry, so the contribution carries the materialised content wherever the loop renders it. This keeps the capability completely declarative — no executable code is involved.
Step 4 — Install the capability into your project
Install from the local path with --scope project so it is scoped only to this demo project:
msd capability install ./capabilities/hello-note --scope project
The command emits a JSON result:
{
"status": "installed",
"id": "hello-note",
"version": "0.1.0",
"scope": "project",
"disclosure": [
"This capability ships no executable surfaces (declarative only)."
]
}
MSD copies the bundle into .msd/capabilities/hello-note/ and records it in the project ledger at .msd-capabilities.json. Because hello-note declares no executable surfaces (no hook scripts, no MCP servers, no command modules) it installs without a consent prompt — the disclosure line confirms there was no runnable code to review. That is intentional: declarative capabilities are safe to install without reviewing executable code.
Step 5 — Confirm the installation
msd capability list
list emits a JSON array of every capability MSD can see — the first-party ones that ship with MSD, plus any you have installed. Your hello-note entry appears at the end:
{
"id": "hello-note",
"role": "feature",
"version": "0.1.0",
"tier": "standard",
"source": "./capabilities/hello-note",
"scope": "project",
"status": "active",
"reason": null,
"title": "Hello Note"
}
status is active — the capability is installed, compatible with your MSD version, and will fire. (The other status values are incompatible, when the host MSD version is outside the capability's engines.msd range, and inactive, when a project-scoped capability has not been consented on this machine.)
You can also query the active hook set for the plan:pre point:
msd loop render-hooks plan:pre --raw
The envelope is { point, activeHooks, rendered }. Your contribution appears in activeHooks (alongside any first-party hooks active at this point):
{
"capId": "hello-note",
"kind": "contribution",
"into": "planner",
"fragment": {
"inline": "## Hello from hello-note\n\nThis planning session was started with the hello-note capability active.\nRecord a brief note in HELLO.md summarising the plan goal in one sentence.\n",
"path": "fragments/plan-pre.md"
},
"produces": ["HELLO.md"],
"onError": "skip"
}
Notice that fragment.inline now holds the materialised text from fragments/plan-pre.md — MSD inlined it at load time, while keeping the original path for reference. The top-level rendered field is an unfiltered diagnostic digest across kinds and roles; planner hosts do not paste it. They read activeHooks in registry order and inject only active kind: "contribution" entries whose into is planner, using each entry's fragment.inline and resolved configValues.
Step 6 — See the contribution reach the planner
Planning is driven by a slash command, not a msd subcommand. In your AI assistant, start a planning session for a phase with:
/msd-plan-phase
When the planner runs — whether from /msd-plan-phase or /msd-quick — the active planner-targeted plan:pre contributions are injected into its prompt, so it receives hello-note and, following the fragment's instruction, records a one-line note in HELLO.md. In /msd-quick --full and /msd-quick --validate, the same rendered hook snapshot is reused if the plan checker sends the plan back to the planner for revision.
The first-party security capability is enabled by default and also contributes at plan:pre into the planner role. Its guidance requires generated plans to include <threat_model> blocks and carries the configured ASVS level and blocking threshold. Disable workflow.security_enforcement if that behavior is not wanted. Contributions targeting any non-planner role are not inserted into Quick's planner prompts.
You do not need to run a full planning session to confirm the wiring, though. The loop render-hooks command shows exactly what the loop would hand the planner — the same output you saw in Step 5:
msd loop render-hooks plan:pre --raw
Find hello-note in activeHooks and inspect its fragment.inline, into, and optional configValues. Those are the fields the planner host filters and injects. The unfiltered top-level rendered digest is useful for inspection, but it is not the planner prompt. This confirms the capability is wired into the loop without dispatching an agent.
Step 7 — Remove the capability
When you want to stop the contribution from firing, remove the capability from the project:
msd capability remove hello-note --scope project
This emits a JSON result describing what was removed:
{
"status": "removed",
"id": "hello-note",
"scope": "project",
"removedFiles": [
".msd/capabilities/hello-note"
],
"strippedEdits": 0,
"dataPreserved": true
}
Run msd capability list again and hello-note is gone from the array. Run msd loop render-hooks plan:pre --raw and you will see it is absent from activeHooks: a removed capability contributes nothing to the loop.
Removing the installed bundle does not touch the source folder you authored under capabilities/hello-note/ — that is your copy. To reinstall, just run the Step 4 command again.
You have built your first capability
You scaffolded a capability folder, wrote a manifest with a single plan:pre contribution, installed it into a project-scoped ledger without a trust prompt, confirmed it in the active hook set, saw it reach the planning loop, and removed it cleanly.
The capability you built is fully declarative: it owns a prompt fragment and a hook declaration, and no executable code was involved at any point.
Where next
- Publish a capability — package and share your capability via a URL or registry.
- Import a capability from a URL — install a third-party capability from a git URL, tarball, or npm package.
- Capability manifest reference — all fields, types, and validation rules for
capability.json. - Capability trust model — why declarative capabilities need no consent prompt and how executable surfaces are disclosed.