ai.json Manifest
ai.json is the file you author and commit. It declares the target vendors and the skill dependencies for your project. spm validates it against an embedded JSON Schema on every load.
Example
{
"targets": ["claude", "copilot"],
"skills": {
"pdf-tools": { "git": "https://github.com/org/skills", "tag": "v1.2.0", "path": "skills/pdf" },
"reviewer": { "git": "https://github.com/me/reviewer", "branch": "main" },
"linter": { "git": "https://github.com/org/linter", "version": "^1.2" },
"pinned": { "git": "https://github.com/x/y", "commit": "a1b2c3d" }
}
}targets
A list of one or more vendors: amp, claude, cline, codex, copilot, cursor, gemini, windsurf. Skills resolve once and project into each target independently. See Targets & Vendors.
skills
A map of local skill name → skill spec. Each spec has:
| field | required | meaning |
|---|---|---|
git | yes | Repo URL — any URL the system git understands. |
tag | one-of | git tag (annotated tags deref to commit). |
branch | one-of | branch tip at install/update time. |
commit | one-of | exact commit. |
version | one-of | semver range; the highest matching tag (^1.2). |
path | optional | subdirectory to select (for monorepos of skills). |
Exactly one version selector (tag, branch, commit, or version) is required per skill. Each selector is locked to a resolved commit SHA in ai.lock:
| field | locked to |
|---|---|
tag | resolved SHA |
branch | resolved SHA |
commit | itself |
version | resolved tag + SHA |
version ranges
version is a semver requirement — ^1.2, ~1.4, >=1.2, <2, 1, =1.4.1 — resolved to the highest tag that satisfies it:
- a tag may carry one leading
v(v1.4.1and1.4.1both match); the requirement itself may not (^v1.2is rejected — write^1.2); - non-semver tags (
latest, floatingv1) are ignored; - pre-release tags only match a requirement that names a pre-release of the same version (
~1.5.0-rc.1); - if both
v1.2.0and1.2.0exist, thev-prefixed one wins.
The chosen tag and its commit are pinned in ai.lock. spm install keeps that pin; spm update moves it to the newest tag still in the range. A tag selector is always an exact tag, never a range.
resolveTransitive
An optional top-level boolean (default false). When true, spm reads each resolved skill's own co-located ai.json and recursively resolves the skills it declares, folding them into the same install alongside your direct skills:
{
"targets": ["claude", "copilot"],
"resolveTransitive": true,
"skills": {
"toolkit": { "git": "https://github.com/org/toolkit", "tag": "v2.0.0" }
}
}It is off by default because skills can carry executable agent instructions, so auto-fetching repos you never named is a larger supply-chain surface than a typical package manager. The flag is read only from your root project's manifest — a dependency's nested ai.json cannot re-enable it. See Transitive Skill Dependencies for the full model: naming, deduplication, cycles, the depth cap, and the version-conflict error.
allowedSources
An optional top-level array of source patterns. When present, every skill and plugin — direct, transitive, or re-installed from ai.lock — must come from a repository matching one of them, or the command fails before contacting it:
{
"targets": ["claude"],
"allowedSources": ["github.com/camunda/*", "github.com/my-org/approved-skill", "file://*"],
"skills": { "reviewer": { "git": "https://github.com/camunda/reviewer", "tag": "v1.0.0" } }
}A pattern is host/path with no scheme; * as the whole last segment matches any repository below that path, and file://* allows local sources. An empty list allows nothing. The project policy is intersected with the optional machine policy (SPM_POLICY / $SPM_HOME/policy.json), so it can narrow but never widen it. It is read only from your root manifest — a dependency's nested ai.json declaring allowedSources is rejected. See Source Policy for the full syntax and enforcement points.
plugins
A map of local plugin name → plugin spec. A plugin is a Claude Code plugin that bundles agents, MCP servers, hooks and scripts in addition to (or instead of) skills. The spec uses the same fields as skills (git, one version selector, optional path), but path should point at the plugin root — the directory holding .claude-plugin/plugin.json:
{
"targets": ["claude", "copilot"],
"skills": {},
"plugins": {
"design-system": {
"git": "https://github.com/camunda/design-system",
"branch": "main",
"path": "plugins/camunda-design-system"
}
}
}How each target consumes a plugin:
- Claude registers the whole plugin under a dedicated, project-local
spm-pluginsmarketplace (.spm/claude-plugins/) — its agents, MCP servers, hooks and scripts all load. - Every other target gets only the plugin's bundled skills, flattened into that target's normal skills location.
ai.lock pins the plugin's commit and records its bundled skill set. A bundled skill whose name collides with a standalone skills entry (or another plugin's skill) is a hard error, never a silent overwrite. Add or remove plugins from the CLI with spm add --plugin / spm remove --plugin.
Editor autocompletion
Add a $schema reference so your editor validates and autocompletes ai.json:
{ "$schema": "./schema/ai.schema.json", "targets": ["claude"], "skills": {} }Pulling in many skills at once
Instead of naming skills one by one, point --path at a container and pass --all to add every immediate subdirectory that has its own SKILL.md:
spm add https://github.com/org/repo --tag v1.0.0 --path skills --allEach sub-skill becomes its own ai.json entry, keyed by its directory name.