Skip to main content

Plugin System

Install, manage, and understand Daintree plugins: the .dntr package format, the Plugin Manager view, hot enable/disable, updates, the blocklist, and what a plugin is and is not allowed to do.

Reviewed

A plugin extends Daintree's habitat with things the app does not ship itself: panels and the React views that fill them, commands and keybindings, menu and context-menu entries, generated settings forms, MCP servers, agent registrations, skills, file decorations, and forge providers. Plugins live in ~/.daintree/plugins/, one directory per plugin, each holding a plugin.json manifest that declares everything it contributes.

The plugin contract reached 1.0 and is frozen. The manifest schema is stable, and the two contribution points that used to carry an experimental_ prefix (experimental_views and experimental_mcpServers) were promoted to plain views and mcpServers. The old keys still parse as deprecated aliases and log a one-time warning naming their replacement, so an older manifest keeps working while you migrate. Nothing on this page is provisional any more.

Installing a plugin

A packaged plugin is a .dntr file: a standard ZIP archive with plugin.json at its root. There is no marketplace and no central registry; authors publish .dntr files on GitHub Releases, their own site, or an internal artifact host. Daintree registers the .dntr extension with your OS at first launch, so a downloaded package is double-clickable.

PathHow
Drag and dropDrop one or more .dntr files anywhere on the Plugin Manager's body. A multi-file drop installs sequentially, one confirm per file.
Install from fileInstall from file… in the Plugin Manager, or double-click a .dntr in your file manager.
Install from URLInstall from URL… in the Plugin Manager. Paste a link to a .dntr.
SideloadPut an unpacked plugin directory at ~/.daintree/plugins/{publisher}.{name}/ and restart. This is the path for plugins you build yourself or share inside a team via a private repo.
Deep linkdaintree://plugin/install?url=… opens Daintree and pre-fills the URL install dialog. daintree://plugin/open?id=publisher.name opens the manager scrolled to that plugin.
CLIdaintree-plugin install <path-or-url> installs into the running app. daintree-plugin uninstall <pluginId> removes it.

Every path converges on the same install flow: hash the archive with SHA-256, validate the manifest against the same Zod schema the loader uses, check engines.daintree against the running app version, extract into a temp directory, then atomically swap it into ~/.daintree/plugins/ and load it. No restart is involved.

Installing over a plugin that is already present replaces it unconditionally: there is no semver comparison, no downgrade gate, and no identical-version block. The swap keeps the original installedAt and records an updatedAt.

URL installs carry their own gate: a 30 MB size cap and a 30 s timeout, a content-type allowlist with a .dntr-suffix fallback, redirects followed manually to at most five hops with each hop re-checked for an https: scheme and for private, loopback, and link-local targets, and a confirmation prompt before Daintree will fetch over plaintext HTTP.

The Plugin Manager

Plugin management graduated out of a settings tab into a first-class view: a full-screen overlay in the same family as the theme browser, not a modal. It does not trap focus, Escape closes it, and the app chrome behind it goes inert while it is open.

Open it from Plugin Manager… in the app menu, from the command palette (Open Plugin Manager), from the plugin toolbar tray, or from Settings > Plugins. There is no default keyboard shortcut; bind one yourself from Keyboard Shortcuts if you open it often.

The Plugin Manager: master list on the left, detail or catalog on the right

The layout is master/detail. The left column lists everything installed, grouped by catalog category: Forge providers, AI & agents, Workspace, and Other. A plugin's category comes from its manifest category field when it declares one, and is otherwise derived from what it contributes. A disabled plugin stays in its category, dimmed in place with a badge: state is shown where the row already is, so toggling a plugin never teleports it out from under your cursor.

Each row carries the plugin's icon, name, version, and tagline, plus whichever badges apply: Blocked, Disabled, the install source (file, URL, sideload) when it differs from the default, Dev for a plugin linked by the development loop, Restart required, and Failed when activation threw. The enable switch sits at the right of the row as its own control.

Search and filter operators

The search field does fuzzy matching on name and description, and also accepts VS Code-style @ operators that narrow the list before scoring. A row of chips below the field advertises the category filters and @disabled so the syntax is discoverable rather than hidden.

OperatorMatches
@builtinPlugins that ship with Daintree.
@installedPlugins you installed yourself.
@enabled / @disabledCurrent enable state.
@cat:<id>Category: forge, ai, workspace, other.
@cap:<id>Plugins declaring a capability, e.g. @cap:shell:exec.

An unrecognized @foo token is left in the free text rather than silently dropped, so a half-remembered operator still gets you a name match instead of an empty list.

The detail pane

Selecting a plugin fills the right pane with its identity, version, provenance, lifecycle actions, and a tab bar. Tabs are earned by content. Overview is always present; Settings appears only when the plugin declares a settings schema, Permissions only when it declares capabilities, and MCP servers only when it contributes one. A minimal plugin gets one tab, not three with two of them empty.

With nothing selected, the pane shows the catalog home: a card grid of the categories with a one-line blurb each. A footer slot below the installed list is reserved for the online browse catalog, which ships separately and currently reads "Online plugin catalog coming soon."

While an install is running, a progress banner reports the phase it is in (Downloading the plugin, Unpacking the plugin, Checking the plugin, Finishing the install), names the archive entry being written, and offers a cancel. If a change cannot be reconciled in the running session, a bar across the header offers a restart behind a confirm dialog. That bar clears itself if the condition resolves while it is open.

Settings > Plugins still exists, but it is now a launcher: the installed count, a button that opens this view, and an opt-in toggle for background update checks.

Managing installed plugins

Enable and disable, without a restart

The switch on each row is live. Disabling unloads the plugin in place: disposers run, its contributions leave the palette and toolbar, and any MCP subprocess it started is killed. Enabling re-forks the worker and loads it again. The Restart required badge reads true only in the brief window where the persisted desired state and the running state have not yet reconciled; it is a transient cue, not the normal path.

Settings

A plugin that declares a settings schema gets a generated form on its Settings tab. Fields are user-scoped (global, in ~/.daintree/plugin-settings/) or project-scoped (per project, in that project's .daintree directory). A field declared type: "secret" is encrypted at rest through the OS keychain (macOS Keychain, Windows DPAPI, libsecret or kwallet on Linux) and the form tells you which storage tier is in use. On a host with no keychain backend, typically a headless Linux box, it falls back to plaintext JSON at mode 0600 and says so.

Updates

Nothing auto-updates. Right-click a plugin and choose Check for update: Daintree re-fetches the URL it was installed from and compares the SHA-256 of the archive against the installed one, so a new build is detected by content change regardless of what its version string says. Update all checks every URL-installed plugin and then drains the ones with a change through the same per-plugin confirm: it never installs anything silently. Sideloaded and file-installed plugins have no origin to re-fetch, so they are updated by reinstalling.

Uninstall

Uninstall runs a full cascade: unload the plugin and run its disposers, terminate any MCP subprocesses it spawned, revoke every stored consent pin so a later reinstall re-prompts rather than inheriting prior approvals, and delete its directory. User-scope settings are kept by default so an API token survives a reinstall; tick also remove stored settings (or pass --delete-settings to the CLI) to delete them too. Project-scope settings are never touched: they are tracked per repository and removing them is the project's business.

The blocklist

Daintree fetches a small remote blocklist at startup and refuses to load anything it names. The list is fetched once with an 8 s timeout, cached on disk for offline enforcement, and re-fetched when the cache passes six hours. It fails open: a network or parse failure never blocks your plugins. A blocked plugin still appears in the manager, dimmed, with a Blocked badge, a stated reason, and its switch disabled: the refusal is host policy, not something you toggled.

Where plugin contributions appear

Contributions are read from the manifest eagerly at startup, before any plugin code runs, so a plugin's commands and panels are in the palette even when the plugin itself has not activated yet. Plugins are lazy by default: the entry module is imported the first time one of its contributions is actually used.

  • Commands appear in the command palette under the plugin's own category, and can be bound to keys like any built-in action. See Palettes and Menus.
  • Panels appear in the new-panel palette and open into the grid alongside terminals and viewers. See Terminals and Panels.
  • Toolbar buttons collect into a single grouped plugin tray button rather than each claiming a top-level slot. You promote the ones you use to their own slot by hovering the tray row and clicking the pin, pressing P, or from Settings > Toolbar. Placement is your call, not the manifest's.
  • Menu and context-menu items are appended below Daintree's own entries on the surfaces they target.
  • Agents, MCP servers, and skills join the same rosters the built-ins use. See Agents and MCP Server.

What a plugin can and cannot do

User-installed plugins run out of process, in a forked utility process with its own module realm and OS-level crash isolation. Every host.* call is bridged over a MessagePort, which is why the host API is fully asynchronous. Built-in plugins are the exception: they are app-bundled, trusted, and never uninstalled, so they activate in-process.

The manifest's capabilities array is disclosure-first with real host-side policy effects. Declaring one of the seven high-risk tokens raises every action the plugin registers to a confirmation dialog; a compound lattice catches combinations that are individually benign but dangerous together, such as a sensitive read paired with an unconstrained network sink; scopes.network.allowedUrls attenuates that elevation by proving the sink is fixed; scopes.fs.allowedPaths is enforced at runtime for the host-mediated filesystem and git surface; and the tier a plugin-hosted MCP server's tools can reach is capped against the same set.

What it is not is a sandbox. A plugin's entry module is ordinary Node code and can call node:fs or spawn a process directly, whatever it declared. Nor is there signing, publisher identity, or a pre-install consent gate: capabilities are surfaced after install, in the detail pane. Those are deliberate 1.0 decisions, stated plainly rather than papered over. The full contract, including every capability token and every non-guarantee, is on Trust & Capabilities. See also Security for how this sits inside Daintree's wider model.

Writing a plugin

Four pages cover authoring, in the order you will want them:

  • Plugin Manifest: every field in plugin.json: identity and the plugin id format, version compatibility, capabilities and scopes, activation events, and how strict validation reports mistakes.
  • Contribution Points: the fourteen slots a plugin can fill, each with its schema and an example.
  • Host API: the activate(host) entry point, the runtime surface it hands you, and the view component contract.
  • Building & Distributing: the daintree-plugin CLI, the hot-reload development loop, .dntr packaging, and how to ship it.
Note
The plugin SDK and CLI packages (@daintreehq/plugin-sdk, @daintreehq/plugin-vite, @daintreehq/plugin-testing, and daintree-plugin) are not yet published to npm. Every command on these pages describes the shipped contract, but npm install and npx will not resolve them today. Sideloading a directory you built yourself is the working path until they land.

Built-in plugins

Daintree ships its GitHub integration as a built-in plugin rather than as app code. It registers a forge provider (issues, pull requests, reviews, CI roll-up, releases, and auth) and a file-decoration provider that badges review state on changed-file rows in the Review Hub. It is the reference implementation for what a real provider looks like.

Built-in plugins can be disabled but not uninstalled. The switch works exactly as it does for anything else, and disabling GitHub takes its forge provider and decorations out of the app; there is simply no directory to delete, because they live inside the application bundle. They also carry no source badge, since built-in is the default.

The daintree.* namespace is reserved. A user-installed plugin whose name starts with daintree. is rejected at manifest validation.