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.
| Doc | What it covers |
|---|---|
| Tutorial | Build a plugin from cuttle plugins new to a shared package, step by step |
| Developing with Cuttle | The 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 |
| Manifest | cuttle-plugin.json reference, contribution points, variables, package format and limits, validation error codes |
| Permissions | The permission catalog: tiers, scopes, what each one opens, the session cap, what is never grantable |
| Security model | Threat model, sandbox choice, scoped credentials, integrity, residual risks |
| Sync and export | What follows you to your other devices, critical permissions asked per device, conflicts, offline; exporting and importing the plugin set |
| Publishing | The 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.
| Variable | Default | Effect |
|---|---|---|
CUTTLE_PLUGINS | on | 0 turns plugins off: the Plugins view, /api/plugins*, cuttle plugins, and every installed plugin's contributions. |
CUTTLE_PLUGIN_HOST | off | 1 (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_URL | the built-in catalog | The index Browse reads (Publishing). Unset, Browse lists the plugins this build ships (examples/plugins), installed from disk with no network. |
SAFE_FETCH_ALLOWLIST | empty | Host 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), oneNAME=valueper 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) setCUTTLE_PLUGINS=1explicitly 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
.cuttlepluginzip with acuttle-plugin.jsonmanifest. 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) orproject(per-project override);secretsettings live in the encrypted secrets store. - Source — every item carries its provenance:
user,builtin:<feature>orplugin:<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.sourceandintents.source('user'by default) andsessions.origin_source(NULL for a user's session). An automation with afeatureisbuiltin:<feature>; a preset pipeline a feature provisioned isbuiltin:<feature>, while a user's--preset-keycopy of a gallery template staysuser. 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; asourcein 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_managedto 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_atkeep 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/pipelinesand/api/intentstake?source=user|builtin|plugin|builtin:<feature>|plugin:<id>; the CLI'sautomations list,pipelines listandintents listtake--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 fieldfile, ≤ 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>.cuttlepluginand 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? }(capnarrows thesessions.spawnlimits in the same transaction; wider than the plugin asks is400 cap_widenedand nothing is installed).grantsmust include every required permission (400 required_deniedlists the missing ones) and may add optional ones; anything the plugin did not declare is400 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_deniedwithreasons), 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/:idwhere:idis 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 are400 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_settingswith per-keyerrors). Effective value: project override (only forscope: "project"settings, in an enabled project) → stored → manifest default; a stored value a newer schema rejects falls back to the default.secretsettings go to the encrypted secrets store (user:<owner>ororg:<org>scope, keyplugin:<id>:<key>) and are only ever reported asisSet. - Status —
active,disabled, and on this devicemissing_package(stored package missing or no longer matching its sha) orincompatible(engine range). Enabling re-checks policy, engine range and integrity, and the server re-checks every running install at boot, parking or restoring it.blockedfollows 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.
modedecides what happens to its automations and pipelines first (contributedItems: { archived, keptAsYours }):removearchives them (run history kept, installing again revives them),keepmakes them the user's own. See Manifest → automations. - Policy —
GET|PUT /api/plugins/policy(PUTadmin only) editssystem_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 abovememberMaxTier(defaulthigh). A corrupt stored policy fails closed (allowlist of nothing, members up tolow). OnAUTH_MODE=nonethe single user is an admin. - Audit — every grant, deny, revoke, project change, status change and uninstall is a
plugin_grant_logrow written in the same transaction as the change (no FK, exempt from retention), and aplugin.*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>andpack <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;newanddevfor 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,kindupgrade|downgrade|reinstall, the permissiondiff,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,grantsswitches on new optional permissions,capnarrows 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_blockedwithreasons) and the installed version stays. Updating anincompatibleormissing_packageinstallation to a good package makes it active again; - without
packageShait approves the pending update;DELETE /api/plugins/:id/updatedismisses it. - The user's own narrowing of
sessions.spawnsurvives 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=disableor?revoke=a,blists the live sessions the change leaves with the plugin's access: the ones it started, plus — when disabling or revokingmcp.contribute/process.spawnof a plugin with tool servers — every session started in its projects since it was installed.POST …/disable { stopSessions }andPUT …/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). ThePUTanswer listsblockedandrestoredinstallation 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 usualPOST /api/plugins/<installationId>/uninstall. - Automations without
sessions.spawn— a plugin automation or pipeline whose plugin does not holdsessions.spawnis 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
.cuttlepluginon 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.spawnthe 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 plussources); runner pickers mark plugin runners. - Routes for the page:
GET /api/plugins/:id/docs(README and CHANGELOG, ≤ 256 KB each) andGET /api/plugins/:id/icon/GET /api/plugins/packages/:sha/icon(served with a sandboxing CSP andnosniff, 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 passesvalidate: 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, thenPOST /api/plugins/:id/updatewithconfirm): 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 orcuttle 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_requiredotherwise); on a no-login desktop the user is the admin. - The installation shows a Development badge (
development: truein the API) and does not sync: its package exists only on this device. Exports carry it with origindev, 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).
- Only an admin may load a dev package (
Examples (
examples/plugins/, listed inindex.json, packaged indist/):- 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) andnetwork:discord.com(+ a Discord webhook URL secret) for a message when a review starts. The webhook needs the plugin host. - Plugin Developer (
cuttle.plugin) — theplugin-developeragent (orchestrates child sessions to build, validate, dev-load and pack a plugin) and theplugin-devskill with itsreferences/files. Onlyagents.contributeandskills.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 tocomfyui-output/in the session's directory), thecomfyui-workflowsskill, thecomfyui-artistagent, a bundledtxt2imgworkflow, the ComfyUI URL (this device only) and API token (secret) settings, and a Test connection action. examples/plugins/fixtures/fake-comfyui.mjsstands 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 itssha256andsizeinindex.json—services/plugins/examples.test.tsfails with the exact command otherwise.
- Automation Suite (
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.usefor the runner (providers.use:__pty__for the Claude Code terminal). Without a grantedproviders.usecovering the runner, every session the plugin starts is refused.cuttle plugins newand both examples include it. notifications.sendopens 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.*andgit.writelikewise). The Automation Suite posts to Discord from its host code instead, withcuttle.fetchand an optionalnetwork:discord.comgrant plus a webhook-URL secret setting.- The Automation Suite's PR review needs the plugin host. Its webhook runs through an
onWebhookfilter, so withoutCUTTLE_PLUGIN_HOST=1a delivery answers503and 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 migration0024_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.