All docs

Plugin docs

Cuttle plugins

What plugins are, how to turn them on, install, enable and update them, and what v1 does not do yet.

Status: v1 (plugin API 1.0.0), on by default; CUTTLE_PLUGINS=0 turns it off (Turning plugins on and off). What v1 does not do yet is listed under Known limits in v1.

Cuttle plugins extend Cuttle the way VS Code extensions extend VS Code: you import a package, see what it adds, enable it per project, configure it from an auto-generated settings page, and your install list and settings follow you to your other devices. Unlike VS Code, every plugin declares permissions that are shown at install, chosen by you, and enforced by the server, the way browser extensions and Android apps work.

Always say "Cuttle plugin" in UI and docs. Cuttle separately manages Claude Code plugins for sessions; those are a different thing.

DocWhat it covers
TutorialBuild a plugin from cuttle plugins new to a shared package, step by step
Developing with CuttleThe author loop (develop → test locally → package → install → share), the Plugin Developer catalog plugin whose plugin-developer agent builds one for you, and its downloadable agent bundle
Manifestcuttle-plugin.json reference, contribution points, variables, package format and limits, validation error codes
PermissionsThe permission catalog: tiers, scopes, what each one opens, the session cap, what is never grantable
Security modelThreat model, sandbox choice, scoped credentials, integrity, residual risks
Sync and exportWhat follows you to your other devices, critical permissions asked per device, conflicts, offline; exporting and importing the plugin set
PublishingThe plugin index behind Browse: format, SHA-256 pinning, hosting your own, the signing roadmap

Author tooling and example plugins covers cuttle plugins new / dev and the three plugins in examples/plugins/; Developing with Cuttle covers having the Plugin Developer plugin's plugin-developer agent build one for you.

Turning plugins on and off

Plugins are on unless the server starts with CUTTLE_PLUGINS=0. Nothing runs until someone installs a plugin. The sandboxed plugin host, which runs plugin JavaScript, is a second switch and stays off by default.

VariableDefaultEffect
CUTTLE_PLUGINSon0 turns plugins off: the Plugins view, /api/plugins*, cuttle plugins, and every installed plugin's contributions.
CUTTLE_PLUGIN_HOSToff1 (with CUTTLE_PLUGINS=1) runs plugins' host code: Actions buttons and onWebhook filters. Without it, actions answer "plugin code is turned off" and a webhook automation with an onWebhook handler answers 503 without running.
CUTTLE_PLUGIN_INDEX_URLthe built-in catalogThe index Browse reads (Publishing). Unset, Browse lists the plugins this build ships (examples/plugins), installed from disk with no network.
SAFE_FETCH_ALLOWLISTemptyHost names or CIDRs the server may fetch from although they are private (an index or package on your LAN).

Where to set them, then restart the server:

  • Source checkout (pnpm dev, pnpm start): the repository-root .env. Turbo forwards all four.
  • Desktop app: <cccRoot>/ccc.env (by default ~/.ccc/ccc.env), one NAME=value per line; restart Cuttle from the tray.
  • Docker Compose: the server service passes only the variables it lists, so add the ones you change in a docker-compose.override.yml, for example to turn plugins off:

    services:
      server:
        environment:
          - CUTTLE_PLUGINS=0
  • Isolated dev instances (pnpm dev:instance) set CUTTLE_PLUGINS=1 explicitly in .env.instance.example, so QA and the e2e journeys exercise plugins whatever the default; the host stays off there too.

Check with GET /api/features (pluginsEnabled, pluginHostEnabled).

Turning it off again deletes nothing. Plugin routes answer 404 (the plugin pages say plugins are off), every plugin credential is refused, and every plugin's automations, skills, agents, tool servers and runners stop applying; installations, settings, grants and history stay. Items you kept as your own when uninstalling are ordinary items and keep working. Turning it back on restores everything as it was.

Concepts

  • Package — a .cuttleplugin zip with a cuttle-plugin.json manifest. Identified by the SHA-256 of its bytes. Unsigned in v1 ("Unverified publisher").
  • Plugin id — publisher.name, e.g. acme.suite. Stable across versions.
  • Contributions — what a plugin adds: skills, agents, automations (cron / interval / webhook), pipelines, MCP servers, ACP runners, a settings schema, and optional sandboxed host code for webhook transforms and action buttons. Contributions are read-only; Duplicate to customize makes an editable copy that is yours.
  • Permissions — declared by the plugin, required or optional, tiered low / medium / high / critical. Required ones must be granted to install.
  • Installation — a plugin installed for a user (or, by an admin, for the org). It has a status: active, disabled, blocked (org policy), incompatible (engine range not satisfied on this device), missing_package (synced from another device, package not present here). Separately it may have a pending update: a newer package waiting for permission approval while the installed version keeps running.
  • Enablement — per project, like VS Code's per-workspace enable. The set of projects a plugin is enabled in is also the project scope of its grants.
  • Settings — user (synced), machine (this device only) or project (per-project override); secret settings live in the encrypted secrets store.
  • Source — every item carries its provenance: user, builtin:<feature> or plugin:<id>. Automations, pipelines, sessions, agents, skills, runners and intents show a source chip and can be filtered by it. Plugin items are shown by default; built-in feature items stay hidden by default. See Provenance for what is stored and enforced today.

Provenance

  • Stored. triggers.source, pipelines.source and intents.source ('user' by default) and sessions.origin_source (NULL for a user's session). An automation with a feature is builtin:<feature>; a preset pipeline a feature provisioned is builtin:<feature>, while a user's --preset-key copy of a gallery template stays user. A session inherits the non-user source of the automation or pipeline that started it.
  • Stamped, never self-reported. A request made with a plugin's credential stamps plugin:<id> on the automations, pipelines and intents it creates; a source in the request body is ignored. The server resolves the manifest id from the credential's installation on every request.
  • Read-only. A plugin's automation or pipeline answers 409 plugin_managed to edits, deletes, claims and template updates from anyone but that plugin; it can still be run and duplicated into your own copy. Intents a plugin creates stay fully workable.
  • Archived, not deleted. triggers.archived_at / pipelines.archived_at keep run history when a plugin is uninstalled (executions and runs cascade on delete). Lists hide archived rows unless ?archived=1.
  • Filtered. GET /api/triggers, /api/pipelines and /api/intents take ?source=user|builtin|plugin|builtin:<feature>|plugin:<id>; the CLI's automations list, pipelines list and intents list take --source.
  • Shown. A source chip on automations, pipelines, intents and sessions (desktop and phone). The existing "Show feature automations" switch still governs built-in automations exactly as before, and Pipelines gains the same "Show feature pipelines" switch. A source dropdown (Mine / All plugins / each plugin) appears on Automations, Pipelines, Intents and the Sessions panel's Filters only once a plugin has contributed something there. The Sessions panel keeps its own rule that automation sessions are hidden until revealed; picking a plugin in the dropdown reveals that plugin's sessions.

Registry, import and install

With plugins off, every /api/plugins* route is 404 and every plugin credential is refused with 403 plugin_inactive; nothing installed is deleted. The UI is the Plugins view.

  • Import — POST /api/plugins/packages (multipart field file, ≤ 20 MB) runs the package reader (@ccc/types/plugin-package: every limit and rejected-entry rule in Manifest, then the manifest validator) entirely in memory. Only an accepted package is written: <cccRoot>/plugins/packages/<sha256>.cuttleplugin and the extracted tree <cccRoot>/plugins/x/<id>@<version>-<sha8>/ (files read-only), each via a temp name and a rename. Importing the same bytes again is a no-op. The answer is the install preview.
  • Preview — GET /api/plugins/packages/:sha/preview?scope=user|org: the package (publisher, "unverified", sha, engine range vs plugin API), every permission with tier, plain-language summary, reason, scope kind, device-local and unsandboxed flags (critical first), what it contributes, the policy refusals that already apply, and name conflicts.
  • Install — POST /api/plugins { packageSha, scope, projects, grants, cap? } (cap narrows the sessions.spawn limits in the same transaction; wider than the plugin asks is 400 cap_widened and nothing is installed). grants must include every required permission (400 required_denied lists the missing ones) and may add optional ones; anything the plugin did not declare is 400 undeclared_grant. Also refused: an unsatisfied engine range (400 incompatible), an existing install of that plugin id at that scope (409 already_installed: update it instead), a project the user cannot edit (403 project_forbidden), the org policy (403 policy_denied with reasons), an agent key / skill name another installed plugin contributes (409 name_conflict), and a stored package that no longer hashes to its sha (409 package_missing). The installation, every grant decision and the project set are written in one transaction.
  • Manage — /api/plugins/:id where :id is the installation id or the plugin id: GET, POST …/enable|disable, PUT …/projects { projects } (replaces the set; adding a project needs edit rights, removing never does), GET|PUT …/grants { grant, revoke } (optional permissions only; required ones are 400 required_permission: disable or uninstall instead), GET|PUT …/settings { projectId?, values }, POST …/uninstall { mode }.
  • Settings — every value is checked against the manifest schema before anything is written (400 bad_settings with per-key errors). Effective value: project override (only for scope: "project" settings, in an enabled project) → stored → manifest default; a stored value a newer schema rejects falls back to the default. secret settings go to the encrypted secrets store (user:<owner> or org:<org> scope, key plugin:<id>:<key>) and are only ever reported as isSet.
  • Status — active, disabled, and on this device missing_package (stored package missing or no longer matching its sha) or incompatible (engine range). Enabling re-checks policy, engine range and integrity, and the server re-checks every running install at boot, parking or restoring it. blocked follows the policy (below).
  • Uninstall — deletes the installation, its grants, project set, settings and secret settings; the grant log keeps the history and the package stays stored. mode decides what happens to its automations and pipelines first (contributedItems: { archived, keptAsYours }): remove archives them (run history kept, installing again revives them), keep makes them the user's own. See Manifest → automations.
  • Policy — GET|PUT /api/plugins/policy (PUT admin only) edits system_settings.plugins_policy { mode: open|allowlist|blocklist, ids, memberMaxTier }. Allow/block lists apply to everyone; members additionally cannot install org-wide, cannot install plugins that add runners, and cannot grant above memberMaxTier (default high). A corrupt stored policy fails closed (allowlist of nothing, members up to low). On AUTH_MODE=none the single user is an admin.
  • Audit — every grant, deny, revoke, project change, status change and uninstall is a plugin_grant_log row written in the same transaction as the change (no FK, exempt from retention), and a plugin.* event on the installation's server events timeline.
  • Credentials — a plugin's token (control token or PAT naming an installation) carries no grants of its own: its scopes are the installation's granted permissions × enabled projects, read from the registry on every request. A plugin credential can never reach /api/plugins*. Under session containment (CCC_EXEC_ISOLATION_CONTAIN=1) a session a plugin started is contained like any session and held to the plugin's grants; the plugin host's own token has no session, so its grants alone apply and routes never see a session id for it.
  • CLI — cuttle plugins validate <dir|file> and pack <dir> (offline, the server's own reader; dotfiles are left out, symlinks refused), install <file> [--yes-required] [--grant p]… [--project id]… [--org] (shows the permission list and installs nothing until --yes-required), list, enable, disable [--stop-sessions], projects <id> --project …, grants <id> [--grant p] [--revoke p] [--stop-sessions], update <file|id> [--approve] [--grant p]… [--confirm] [--dismiss], cap <id> [--runner …] [--model …] [--mode …] [--allow …] [--deny …] (show or narrow the session cap), run-here <id> (this device fires the scheduled automations), uninstall <id> --mode remove|keep; new and dev for authors (below).

Updates, revocation and policy changes

Contract: Permissions → Updates and → Revoking.

  • Update — importing another package of an installed plugin answers the preview with update (fromVersion, toVersion, kind upgrade|downgrade|reinstall, the permission diff, needsApproval, blockers). POST /api/plugins/:id/update { packageSha, approve?, grants?, cap?, confirm? }:

    • nothing new asked → applied at once, in place: the reconciler updates the same automation and pipeline rows (ids, webhook URLs and secrets kept), archives items the new version no longer has and adds new ones;
    • more asked and no approve → recorded as pending (pending_version/pending_sha, installation.pendingUpdate); the installed version keeps running and firing;
    • approve → grants what is newly required, grants switches on new optional permissions, cap narrows new session limits;
    • a downgrade or a same-version package with other contents needs confirm: true (409 confirm_version); an unsatisfied engine range, the policy or a name clash refuse it (409 update_blocked with reasons) and the installed version stays. Updating an incompatible or missing_package installation to a good package makes it active again;
    • without packageSha it approves the pending update; DELETE /api/plugins/:id/update dismisses it.
    • The user's own narrowing of sessions.spawn survives an update that does not widen the plugin's request (the stricter of the two applies).
  • Revoke and disable with running sessions — GET /api/plugins/:id/affected-sessions?change=disable or ?revoke=a,b lists the live sessions the change leaves with the plugin's access: the ones it started, plus — when disabling or revoking mcp.contribute / process.spawn of a plugin with tool servers — every session started in its projects since it was installed. POST …/disable { stopSessions } and PUT …/grants { revoke, stopSessions } stop them in the same call (stoppedSessions); only sessions the acting user may write are touched.
  • Policy changes — saving the policy re-judges every installation (as the user who installed it): newly refused ones become blocked (automations held; skills, agents, tools and runners gone from sessions; settings, grants and history kept) and their owner is notified; blocked ones the policy allows again return to the status they had (read from the grant log). The PUT answer lists blocked and restored installation ids; boot applies the stored policy too. GET /api/plugins/blocked (admins) lists every blocked installation on the server; an admin uninstalls one with the usual POST /api/plugins/<installationId>/uninstall.
  • Automations without sessions.spawn — a plugin automation or pipeline whose plugin does not hold sessions.spawn is held with the reason ("sessions.spawn is off") instead of failing each run.

The Plugins view and the plugin page

Plugins (/plugins) appears in the navigation unless CUTTLE_PLUGINS=0; with the flag off every plugin page says so instead of answering 404, and Settings → Plugin policy (admins) edits the policy above.

  • Installed lists every installation you can see with its status (Enabled, Disabled, Update pending, Blocked, Incompatible, Needs package), version, project count and automation count. Loading, empty and load-error (with Retry) are distinct states; empty leads to Browse plugins. Browse lists the plugin index (Publishing), by default the built-in catalog; Install there downloads the package on the server (a built-in package is read from disk), refuses it unless its SHA-256 equals the index's, and opens the same install sheet (or update review). When the index can't be used it shows the last copy or a banner with Retry: "You may be offline" for a failed connection, "isn't available" for an answer it can't use (a 404, a malformed index). Importing a file keeps working.
  • ⋯ → Export plugin set / Export with packages / Import plugin set… (Sync and export → Export and import).
  • Import plugin file… (or dropping a .cuttleplugin on the page) uploads it; a refused package shows the server's code and every manifest error with its path, and nothing is installed.
  • Install sheet — a bottom sheet on phones, a dialog on desktop: icon, publisher, version, size, "Unverified publisher", sha prefix; what it adds (automations split into scheduled and webhook, pipelines, skills, agents, tool servers, settings) and the runners its automations use; Where it can act (a project checklist, "All my current projects" ticks every current project — later projects are not added automatically); every permission grouped by tier, critical first. Required rows are locked; optional rows start off and have a switch (44px row). Details on each row shows the permission id and what it is limited to. Under sessions.spawn the session limits can be narrowed before installing (sent with the install, in one call). Granting any critical permission requires ticking an acknowledgement before Install is enabled. Policy refusals, name conflicts and an unsatisfied engine range are shown and block Install; the exact package already installed says so. Choices never carry over from one package to the next.
  • Updates — importing a newer package of an installed plugin that asks for nothing new updates it at once ("no new permissions"). One that asks for more is left pending and opens the update review: only what is new (newly required permissions locked on, looser session limits with the limits editor, new optional permissions off), a critical acknowledgement when needed, Approve and update, Not now (stays pending) and Dismiss update. Downgrades and same-version packages open the review with a warning and an explicit Downgrade / Replace button; blocked updates show why. A pending update shows as Update … needs review on the plugin's card and as a banner with Review update on its page.
  • Plugin page (/plugins/<plugin id>, ?tab= survives reloads): the header has the Enable switch and Uninstall; a paused status explains itself. Tabs:

    • Overview — description, the README (markdown without raw HTML; remote images are not loaded, only their alt text is shown), Enabled in (the project checklist, saved as you tick) and package details.
    • Permissions — the same rows; optional ones switch on and off at once (the server re-checks policy); required ones are locked. The session limits can only be narrowed (capWithin, the server's own rule); loosening them means reinstalling. Permission history is the grant log.
    • Contributions — each automation and pipeline with its project and state (On, Off in settings, Paused with the reason), linking to it; Open in Automations / Pipelines lands on the list filtered to this plugin (?source=plugin:<id>). Webhook automations show the URL (copy) and "Secret set · rotate to reveal a new one"; Rotate confirms that the old secret stops working immediately, then shows the new one once. When the scheduled automations run on another device, Run here instead moves them here (POST /api/plugins/:id/run-here). Skills, agents, tool servers and runners are listed.
    • Settings — a form generated from contributes.configuration: each field is labelled Synced, This device only, Per project or Secret, values are checked with the server's rules before saving (nothing is written if any is invalid), secrets are write-only (set, replace or delete; never shown), and per-project overrides have their own editor. Leaving with unsaved changes asks first. "Synced" values follow you to your other devices (Sync and export).
    • Changelog — CHANGELOG.md, rendered like the README.
  • Uninstall asks what happens to the plugin's automations and pipelines: Remove them (archived, run history kept) or Keep them as mine.
  • Disable and revoke ask first when sessions are running with the plugin's access, listing them with Also stop these sessions. Settings → Plugin policy reports what a save blocked or restored and lists blocked installations with Uninstall… for admins.
  • Provenance — source chips show the plugin's name (not its id), and the source filter lists plugins by name (refreshed after install, update and uninstall). Agent pickers (new session, automation form, pipeline node, intent phases, intent work session, command palette, computer-use launch, browser session, chat policies, Autopilot crew "adapt") offer the organization's and enabled plugins' agents too, tagged with where they come from (GET /api/projects/:id/agents?merged=1: the spawn-time merge plus sources); runner pickers mark plugin runners.
  • Routes for the page: GET /api/plugins/:id/docs (README and CHANGELOG, ≤ 256 KB each) and GET /api/plugins/:id/icon / GET /api/plugins/packages/:sha/icon (served with a sandboxing CSP and nosniff, so an SVG opened directly runs nothing).

MCP servers, runners and session caps

An active plugin's contributes.mcpServers attach to every session in its projects through the session's own --mcp-config, and its contributes.runners become approved runtime templates while it is active (Manifest → mcpServers / runners). Every session a plugin starts — including runs its credential starts — runs inside its sessions.spawn cap or is refused with the reason (Permissions → The session cap).

Plugin host

Behind its own switch, CUTTLE_PLUGIN_HOST=1 (on top of CUTTLE_PLUGINS=1). A plugin's contributes.host module runs in a QuickJS sandbox inside a separate, permission-restricted Node process, and reaches anything only through the cuttle bridge (settings, its own secrets, a 1 MB store, granted network hosts, the Cuttle API as its own credential, its timeline). Its onAction: handlers are the Actions buttons on the plugin page (POST /api/plugins/:id/actions/:actionId, cuttle plugins action); its onWebhook: handlers turn a contributed webhook automation's verified deliveries into parameters or drop them. Handler contract and the cuttle API: Manifest → host; sandbox, limits and bridge rules: Security model → Plugin host.

Author tooling and example plugins

  • cuttle plugins new <publisher.name> [--dir d] writes a plugin that passes validate: a manifest asking only for what it uses, a skill, a weekday automation that stays off until its boolean setting is on, a README and a CHANGELOG.
  • cuttle plugins dev <dir> [--yes-required] [--grant p]… [--project id]… [--once] loads an unpacked directory as a Development installation and reloads it on every save (dotfiles ignored; 400 ms of quiet first, so a burst of saves is one reload). Each reload packs the directory in memory, validates it offline, and goes through the ordinary import → update path (POST /api/plugins/packages?dev=1, then POST /api/plugins/:id/update with confirm): an edit that asks for nothing new applies in place (contributed rows keep their ids, the next session sees the new skill); one that adds a permission or loosens the session cap waits for approval on the plugin page or cuttle plugins update <id> --approve, and the previous load keeps running. An invalid save prints the validator's errors and keeps the last good load.

    • Only an admin may load a dev package (403 admin_required otherwise); on a no-login desktop the user is the admin.
    • The installation shows a Development badge (development: true in the API) and does not sync: its package exists only on this device. Exports carry it with origin dev, so it imports elsewhere only with --with-packages.
    • Each save stores a new content-addressed package; packages are not garbage-collected yet, so long dev sessions leave old ones under <cccRoot>/plugins/packages (small; delete by hand if needed).
  • Examples (examples/plugins/, listed in index.json, packaged in dist/):

    • Automation Suite (cuttle.automation-suite) — Nightly triage (cron from the Triage schedule setting, off until Nightly triage is on), Weekly dependency audit (Mondays 06:00, off until switched on) and PR review (an HMAC webhook for GitHub; its host code keeps only opened / reopened / ready-for-review, non-draft pull requests, fetches the diff and starts the PR review pipeline). Its sessions run on the Claude Code terminal with Bash, Write, Edit and NotebookEdit denied. Optional: network:api.github.com (+ a GitHub token secret for private repositories) and network:discord.com (+ a Discord webhook URL secret) for a message when a review starts. The webhook needs the plugin host.
    • Plugin Developer (cuttle.plugin) — the plugin-developer agent (orchestrates child sessions to build, validate, dev-load and pack a plugin) and the plugin-dev skill with its references/ files. Only agents.contribute and skills.contribute. The project page's Skills & agents panel offers it to any project it is not enabled in (Developing with Cuttle).
    • ComfyUI (cuttle.comfyui) — a dependency-free stdio MCP server (comfy_list_models, comfy_run_workflow, comfy_get_result, comfy_queue_status; images saved to comfyui-output/ in the session's directory), the comfyui-workflows skill, the comfyui-artist agent, a bundled txt2img workflow, the ComfyUI URL (this device only) and API token (secret) settings, and a Test connection action.
    • examples/plugins/fixtures/fake-comfyui.mjs stands in for ComfyUI (the endpoints and response shapes the plugin uses; it returns a gradient PNG instead of rendering) for tests and demos without a GPU.
    • Changing an example: re-pack it into dist/ and update its sha256 and size in index.json — services/plugins/examples.test.ts fails with the exact command otherwise.

Skills and agents in sessions

An active plugin's contributes.skills and contributes.agents join every session in the projects it is enabled in, as a layer of the context merge (skills: built-in → plugin → org → project; agents: plugin → org → project → repo), read from the plugin's read-only extracted tree and never written into project files. The project's own items shadow them; the Skills & agents panel (desktop and phone) shows them with a source chip and Duplicate to customize. Rules, API and the allowed agent fields: Manifest → skills / agents.

Known limits in v1

What shipped differs from the original plan in these ways; each is also noted where it applies.

  • Plugins that start sessions must declare providers.use for the runner (providers.use:__pty__ for the Claude Code terminal). Without a granted providers.use covering the runner, every session the plugin starts is refused. cuttle plugins new and both examples include it.
  • notifications.send opens nothing yet. It is in the catalog and can be declared, but no Discord or Slack route is mapped to it (Permissions → What v1 opens; files.* and git.write likewise). The Automation Suite posts to Discord from its host code instead, with cuttle.fetch and an optional network:discord.com grant plus a webhook-URL secret setting.
  • The Automation Suite's PR review needs the plugin host. Its webhook runs through an onWebhook filter, so without CUTTLE_PLUGIN_HOST=1 a delivery answers 503 and nothing runs. Its cron automations work without the host.
  • The built-in catalog is the two examples. It ships with each build, so it changes only with a Cuttle update. Its download URLs are raw GitHub files of this repository; they are the packages' recorded origin, but Cuttle serves them from disk (same SHA-256 check), so they work while the repository is private. Host your own index for anything else (Publishing).
  • Importing a plugin set never updates. A plugin the file lists that is already installed here is left as it is, whatever version the file has; update it by importing its package.
  • Secret settings do not sync on the no-login desktop. The ciphertext secrets lane does not yet map the no-login operator to the linked account; re-enter secrets on each device (Sync and export → Limits).
  • Sync needs the cloud table. The per-user lane writes public.user_plugin_state (cloud migration 0024_user_plugin_state.sql). Until it is deployed the lane waits: a linked runtime checks for the table (hourly), skips plugin pulls and the plugin realtime binding without disturbing the other lanes, and logs it once; pushes of plugin changes stay queued (cloud/accounts.md).
  • ComfyUI is verified only against a stand-in. The example plugin was tested against examples/plugins/fixtures/fake-comfyui.mjs, which mimics the endpoints it uses, not against a real ComfyUI.
  • Every publisher is unverified. Packages are pinned by SHA-256 but not signed (Publishing → Signing).
  • Scoped credentials are refused on the Socket.io connection, so plugins reach Cuttle over HTTP only.
  • Packages are not garbage-collected. Uninstalled and superseded packages stay under <cccRoot>/plugins/packages (small; safe to delete by hand when nothing installed uses them).
  • The plugin host is untested on Windows (the Node permission model's path handling) and in a packaged desktop build.