Skip to main content

Plugin Manifest

Complete reference for plugin.json: the plugin id format, semver and engines.daintree, tagline, authors and category, the fifteen capability tokens, scopes, activation events, contribution caps, and how strict validation reports mistakes.

Reviewed

Every plugin has a plugin.json at its root. It declares the plugin's identity, which Daintree versions it works with, what it contributes to the UI, and what capabilities it claims to need.

Daintree reads the manifest eagerly at startup. Contributions declared here populate the command palette, menus, and toolbars immediately, before any plugin code runs. The entry module is only imported when something actually triggers it. See Contribution Points for the shape of each contributes array and Plugin System for how manifests get installed.

A complete manifest

{
  "name": "acme.linear-planner",
  "version": "0.1.0",
  "displayName": "Linear Planner",
  "description": "Plan Linear issues as multi-step agent workflows.",
  "tagline": "Turn Linear issues into agent workflows.",
  "category": "ai",
  "authors": [
    { "name": "Ada Lovelace", "url": "https://ada.example.com", "role": "Maintainer" }
  ],
  "main": "dist/index.js",
  "engines": { "daintree": "^0.32.0" },
  "capabilities": ["fs:project-read", "network:fetch"],
  "scopes": {
    "network": { "allowedUrls": ["https://api.linear.app/graphql"] },
    "fs": { "allowedPaths": ["${project}/docs"] }
  },
  "activationEvents": [],
  "contributes": {
    "commands": [ /* … */ ],
    "panels": [ /* … */ ],
    "views": [ /* … */ ],
    "settings": [ /* … */ ]
  }
}

Required fields

name: the plugin id

A scoped identifier in publisher.plugin-name form, at most 64 characters, matching:

^[a-z0-9]+(?:-[a-z0-9]+)*\.[a-z0-9]+(?:-[a-z0-9]+)*$

Lowercase only, hyphens rather than underscores, exactly one period separating publisher from plugin name, no spaces. acme.linear-planner and gpriday.cost-management are valid; LinearPlanner, acme/linear, acme.linear.planner, and Acme.LinearPlanner are not.

The publisher segment should identify you: a GitHub handle, company name, or domain prefix. It is what prevents collisions between independently authored plugins. This id is also the namespace every contribution gets: a command declared as plan-from-issue becomes the action acme.linear-planner.plan-from-issue at runtime, and the same rule applies to panels, views, MCP servers, skills, agents, forge providers, and decoration providers.

The daintree.* namespace is reserved for first-party plugins. A user-installed manifest claiming it is rejected.

version

Must be valid semver: 0.1.0, 1.2.3-beta.1. Anything semver.valid() rejects fails manifest validation outright. Note that Daintree does not use this string for update detection; Check for update compares the SHA-256 hash of the archive. Version it honestly anyway, because it is what a user sees on the plugin row.

Identity and catalog metadata

FieldTypeNotes
displayNamestringHuman-readable name in UI listings. Falls back to name. Never used for runtime lookups.
descriptionstringOne-sentence blurb. Long values are truncated in listings.
taglinestringOne-line value proposition, trimmed and capped at 120 characters. Shown under the name on the plugin row. Distinct from description: the tagline is the hook, the description is the blurb.
authorsarrayUp to 10 attribution entries, rendered as a Contributors block in the detail pane.
categoryenumforge, ai, workspace, or other. Groups the plugin in the manager's list and catalog.
mainstringPath to the compiled ESM entry, relative to the plugin root. Optional: a plugin with only static contributions (a settings-driven MCP server config, a skill pack) needs no code at all.

authors

Each entry requires name and accepts optional url, email, and a free-form role. Unknown keys on an entry are rejected. Because url renders as a clickable button in the detail pane it carries the same discipline as a network scope: https:// only, no embedded credentials, no private or loopback hosts.

"authors": [
  { "name": "Ada Lovelace", "url": "https://ada.example.com", "role": "Maintainer" },
  { "name": "Grace Hopper", "email": "grace@example.com" }
]

category

Omit it and Daintree derives one from what you contribute, checking the most identity-defining contribution first: forge providers map to forge; agents, MCP servers, or skills to ai; panels or views to workspace; anything else to other. Declare it explicitly when a multi-contribution plugin would otherwise be misfiled: Daintree's own GitHub plugin contributes file decorations as well as a forge provider, and derivation alone would be a coin flip.

Version compatibility

engines.daintree is a semver range saying which Daintree versions the plugin supports.

"engines": { "daintree": "^0.32.0" }

If the running version does not satisfy the range, the plugin is rejected at load with a user-visible warning. If the field is omitted entirely, Daintree warns in the console and loads the plugin anyway.

Capabilities

The field is capabilities. The strict permissions enum this replaced is no longer accepted under that name.

The model is disclosure-first with host-side policy effects. There is no Node sandbox, so declaring nothing does not stop a plugin from doing anything, but the tokens are not merely advisory either. They are shown to the user in the detail pane, they drive danger classification on every action the plugin registers, and they cap the consent tier a plugin-hosted MCP server's tools can reach. Declare honestly: this is the part of the manifest users judge you by.

TokenIntentHigh-risk
fs:project-readRead files in the current project worktree.
fs:project-writeModify files in the current project worktree.Yes
fs:user-data-readRead from ~/.daintree/ or elsewhere in your home directory.
fs:user-data-writeWrite to ~/.daintree/ or elsewhere in your home directory.Yes
network:fetchMake outbound HTTP requests.
agent:invokeSend prompts to agents from plugin code.Yes
agent:readObserve agent state: lifecycle phase, and session cost and tokens on completion.
agent:registerRegister a launchable agent CLI as a selectable agent.Yes
agent:inputSend text to the active agent terminal.Yes
git:readRead git state: branches, status, log.
git:writeMake git changes: commits, branches.Yes
clipboard:readRead the system clipboard.
clipboard:writeWrite text or PNG images to the system clipboard.
shell:execSpawn subprocesses through the managed host.process surface.Yes
socket:connectConnect to local Unix-domain sockets or Windows named pipes, such as the Docker socket.

The seven marked high-risk raise every action the plugin registers to effectiveDanger: "confirm", regardless of what the action declared. The host may only raise, never lower. Read-only and trivially reversible tokens are excluded on purpose: promoting on those would over-confirm and train people to dismiss the dialog. socket:connect is excluded for a different reason: the host has no interception point for node:net, so elevating on a token it cannot enforce would buy friction without buying safety.

A command can narrow which capabilities that derivation consults with its requires field, so one shell:exec command does not put a confirmation on your "open the panel" command. The one runtime-enforced gate is host.process.spawn, which rejects unless shell:exec is declared, a gate on the supported managed surface, not on Node itself. The full contract, including the compound-capability lattice and the explicit non-guarantees, is on Trust & Capabilities.

Scopes

scopes is an optional top-level object that declares what a capability actually intends to reach. Its three buckets carry different runtime weight, and the page is explicit about which is which because pretending otherwise would be the same mistake the old permissions model made.

"scopes": {
  "network": { "allowedUrls": ["https://api.acme.com/v1"] },
  "fs":      { "allowedPaths": ["${project}/src", "/Users/me/.acme/data"] },
  "socket":  { "allowedPaths": ["/var/run/docker.sock"] }
}
BucketAcceptsRuntime weight
network.allowedUrlshttps:// URLs. Wildcards, embedded credentials, and private, loopback, or link-local targets are rejected at parse time.Live but advisory. A non-empty allowlist suppresses compound-capability elevation, because a fixed sink cannot be remote-controlled. It does not block requests to other URLs.
fs.allowedPathsAbsolute paths, or the dynamic tokens ${project} and ${worktree} with an optional /sub/path suffix. Relative paths, .. segments, globs, and unknown tokens are rejected.Enforced for the host API. Every path argument to host.fs.* and host.git.* is realpath-resolved and contained to a declared root; traversal and symlink escapes reject with a PATH_NOT_ALLOWED: prefix. It does not seal raw node:fs.
socket.allowedPathsAbsolute Unix-domain socket paths and Windows named pipes (\\.\pipe\name). Both forms parse on every platform so a cross-platform manifest validates everywhere.Purely advisory. Nothing enforces it. It exists so the Permissions tab can say "connects to /var/run/docker.sock" instead of showing a bare token.

Every plugin is granted an implicit per-plugin data root at ~/.daintree/plugin-data/{pluginId}/ regardless of what it declares, so fs.allowedPaths is only needed when you reach outside it. A misspelled bucket (networking rather than network) is a manifest error, not a silently dropped field.

Activation events

Plugins are lazy by default. Omit activationEvents, or pass an empty array, and Daintree defers importing main and calling activate() until one of the plugin's contributions is first used: a command dispatched, a forge provider or decoration provider queried, a contributed panel opened.

The only recognized value is "onStartupFinished", which opts the plugin into eager activation once the app has finished starting. List it only when the plugin genuinely has to run at boot.

"activationEvents": ["onStartupFinished"]

Either way, contributions are registered eagerly from the manifest. A lazy plugin's commands, panels, and keybindings are all present before a line of its code has run: activation governs the import and the activate() call, nothing else. Twenty manifest-declared commands cost zero activation time.

contributes

An object of arrays, one per contribution type. All are optional and default to empty. Each has an upper bound, generous relative to any real plugin: the caps exist to reject adversarial manifests that would otherwise exhaust the registration loops, not to constrain you.

KeyMaxKeyMax
commands200views50
keybindings200panels50
menuItems200skills50
contextMenus200agents50
settings200fileDecorationProviders50
toolbarButtons100mcpServers20
processTools100forgeProviders20

The authors array at the top level is capped at 10.

Deprecated aliases

Two keys were renamed in the 1.0 freeze:

DeprecatedStable
experimental_viewsviews
experimental_mcpServersmcpServers

An old manifest still parses and runs identically: the deprecated key is normalized to its canonical name before validation, and the plugin service logs a one-time deprecation warning naming the replacement. If both keys are present, the canonical one wins and the deprecated one is stripped. Rename them; the aliases may be removed in a future major.

Validation

The manifest is validated by Zod at load time, and validation is strict: unknown keys at the top level and unknown keys inside contributes are rejected rather than ignored. A typo cannot silently drop a contribution. Failures surface as a user-visible error naming the schema path that failed, and daintree-plugin validate runs the same schema locally before you package.

Beyond per-field shape, a cross-field pass rejects references that would dangle and produce a contribution that silently never fires. These are the errors that catch real mistakes:

RuleWhy it is an error rather than a runtime no-op
A views entry whose id matches no panels entry.A view renders into a panel of the same id. Without one it could never be shown.
A forgeProviders entry whose settingsScopeRef or viewRefs name nothing declared.The reference resolves to nothing and the wiring quietly does not happen.
A ${settings:id} token in an MCP server's command, args, or env naming an undeclared setting.The supervisor substitutes only declared ids, so an unknown one resolves to an empty string and the value vanishes at spawn.
An actionId on a toolbar button, menu item, keybinding, or context menu that resolves to nothing.The contribution paints a button or binding that does nothing when invoked. Built-in ids closed to plugin dispatch are rejected too, as is an id in another plugin's namespace, and an id in your own namespace that matches no declared command, when you declared any.
Duplicate id values within one contribution array.The registry is keyed on the bare id, so the second entry would silently first-lose at load.
contributes.agents without the agent:register capability.Registering a launchable CLI is a real side effect; the capability is what surfaces it to the user.
A plugin agent id, or a processTools command, that collides with a built-in.Built-ins always win at runtime, so the plugin's entry would register a detection that never fires.
A settings field of type: "enum" with no options, a min greater than its max, or extensions on any type other than file.Each would render a field that cannot be filled in correctly, or a filter that does nothing.
A panel declaring both hasPty: true and dockable: false.A PTY-backed kind renders as a terminal, which is always dockable, so the opt-out could never be honored.
Tip
The commonest validation failures are the dull ones: a name missing its period, uppercase in the id, an engines.daintree that is not a valid range, a capability token that is not in the list above, and contribute written for contributes. Run daintree-plugin validate before you package and none of them reach a user.