ZCode should load a bounded subset of Claude Code format plugins while keeping
ZCode's runtime boundaries. The plugin ecosystem remains file-compatible with
Claude Code where possible, but ZCode P0 only projects plugin-provided skills,
custom commands, MCP servers, hooks and agents into existing ZCode capability
domains. Hooks from every enabled plugin, including third-party Marketplace
plugins, are injected into the runtime. Per-plugin trust is not implemented yet;
the decision lives in canRunPluginHooks of the adapters plugin loader.
This feature started as first-party packaged capabilities such as the iOS Simulator and Android Emulator plugins. Claude Code Marketplace compatibility extends the installation and discovery model, but it is still not a new arbitrary extension runtime. High-risk Claude plugin features are recognized and diagnosed before ZCode grows a matching runtime.
See also docs/claude-code-marketplace-plugin-compat.md for the Marketplace
implementation plan and phased V1a/V1b/V1c scope.
Official plugins are built as workspace packages, including
packages/browser-use-plugin, packages/ios-simulator-plugin,
packages/android-emulator-plugin, and content-only packages such as
packages/document-skills-plugin, packages/skill-creator-plugin,
packages/zcode-guide-plugin, and packages/restore-legacy-sessions-plugin,
but runtime plugin loading does not execute from those workspace paths and does
not import plugin code into the ZCode app bundle. ZCode seeds first-party plugin
files into the official plugin cache under
~/.zcode/cli/plugins/cache/zcode-plugins-official/... and executes plugin MCP
servers from that cache. Users only enable or disable each capability.
The Android Emulator plugin P0 supports macOS and Windows only. Linux hosts are
intentionally unsupported for now; android_preflight must report the host OS as
unsupported instead of attempting a partial setup or emulator launch.
The Android Emulator plugin treats USB devices and emulators as Android targets.
Install, launch, screenshot, log, and UI automation tools accept a serial for
either target kind. android_start_emulator is emulator-only: it starts a new
GUI Android Emulator for the selected AVD and returns the new serial. It must not
silently reuse an existing target; follow-up operations reuse an existing USB
device or emulator by passing its serial directly to build, install, launch,
screenshot, log, or UI automation tools. Target tools without serial may pick a
ready Android target, then start a GUI Android Emulator only when no target is
ready. P0 does not expose a user-configurable UI backend; UI automation uses
Android SDK adb and UI Automator commands.
The catalog's optional publisherType (official or community) describes who
maintains a plugin within the ZCode marketplace. It does not grant trust or
change installation identity, paths, or user/workspace configuration. Personal
marketplaces remain personal regardless of their self-declared publisher type.
Catalog componentTypes may advertise ui before installation. Installed UI
capability comes from validated manifest ui.surfaces, including when disabled;
plugins/listUiSurfaces continues to return only enabled entries. Store listing
category remains the independent usage category. See the market interaction
spec in the root docs/plugins/ directory for presentation and scope semantics.
Contract: root docs/plugins/plugin-publishing.md §4.2–4.3. The CLI passes the
data through and never judges compatibility; only the App knows its plugin API
version.
- Catalog
componentTypesmay advertiseextension. The CLI passes it through toavailablePlugins[].componentTypesexactly likeui. Likeui, it is only a catalog claim; installed capability comes from the installed manifest. - A catalog entry may carry
extensionSummary: { engines: { zcode }, grants }. The CLI reads it from the raw entry on every overview (old manifest caches need no migration). It keeps the entry only whenengines.zcodeis a non-empty string, keeps only stringgrants(default[]), and drops every other key. It does not validate the range syntax. - Manifest synthesis for entries without
plugin.json(createManifestFromMarketplaceEntry) removesextensionSummary, the same as the other Store Listing keys. A partialextensionkey must never become manifest content. - Version skew:
zcodePluginStoreListingSchemaand the overview result are.strict(), and the calling services may be older or newer than the CLI.runtime/capabilitiesdeclarespluginExtensionSummary: true.plugins/overviewacceptsincludeExtensionSummary?: boolean. Services send it only after they see the capability, because old CLIs reject the unknown key.- Only when the request has
includeExtensionSummary: truedoes the CLI put the summary intoavailablePlugins[].listing.extensionSummaryandinstalledPlugins[].listing.extensionSummary. For an installed plugin it is the summary of the latest catalog entry. Install, uninstall, update andplugins/resolveSuggestedReferenceresults never carry it.
Tests: adapters (parseEntryExtensionSummary, manifest synthesis strips the
key), bootstrap (componentTypes passthrough, overview with and without the
flag, capability bit).
ZCode stores plugin state under the CLI storage root:
~/.zcode/cli/plugins/
installed_plugins.json
marketplaces/
zcode-plugins-official/
marketplace.json
cache/
zcode-plugins-official/
android-emulator/
0.1.0/
.zcode-plugin/plugin.json
.claude-plugin/plugin.json
.mcp.json
skills/
commands/
ios-simulator/
0.1.0/
.zcode-plugin/plugin.json
.claude-plugin/plugin.json
.mcp.json
skills/
commands/
browser-use/
0.3.0/
.zcode-plugin/plugin.json
skills/
docs/
document-skills/
0.1.4/
.zcode-plugin/plugin.json
agents/
skills/
skill-creator/
0.1.0/
.zcode-plugin/plugin.json
skills/
restore-legacy-sessions/
0.1.0/
.zcode-plugin/plugin.json
skills/
commands/
data/
android-emulator@zcode-plugins-official/
ios-simulator@zcode-plugins-official/
restore-legacy-sessions@zcode-plugins-official/
cache contains plugin code and static assets. It is version-scoped and can be
replaced when a plugin is updated. data is a persistent per-plugin directory
for MCP servers and tools. Plugin data survives cache replacement and is removed
only when the final installation is removed.
plugins/list reports this directory as the optional dataPath of each plugin
(the same PluginMetadata.dataPath that MCP servers and hooks receive as
${ZCODE_PLUGIN_DATA}). The App passes it to the plugin's extension as
ctx.extension.dataPath (docs/plugins/extension-host.md §18.1 H6) and never
derives the path itself. The CLI does not create the directory when listing;
the extension host creates it before activation.
Uninstalling a plugin is a thorough teardown so that a later reinstall starts
clean. Removing a plugin by id (<name>@<marketplace>) tears down every piece of
durable state ZCode created for it:
- The plugin record in
installed_plugins.json. - The version-scoped cache directory at the record's
installPath(cache/<marketplace>/<name>/<version>/). - The persistent per-plugin data directory
data/<sanitized-plugin-id>/, including anygenerated-commands/ZCode materialized for the plugin. This is the "final installation is removed" case from the storage layout: data is kept across cache replacement on update, but uninstall deletes it. - The plugin's user config entries
plugins.enabledPlugins[id]andplugins.options[id]in the userconfig.json, and the plugin's entries inplugin-secrets.json(see Sensitive Option Storage). After uninstall there is no stale enable flag, saved option or secret left behind.
Teardown is centralized so all entry points behave identically. The filesystem
deletions (record, cache, data) belong to the plugins adapter; the config-entry
deletion belongs to the config adapter. bootstrap orchestrates the two adapters
for one uninstall. No layer above the adapters touches the filesystem directly.
Component registration (skills, commands, hooks, MCP servers) is recomputed from
installed_plugins.json on every discovery pass, so removing the record plus its
directories de-registers the plugin's capabilities on the next session start.
There is no separate unregister step.
Uninstall is idempotent: removing a plugin id that is not installed is a no-op that reports nothing was removed, rather than an error.
The built-in marketplace name is zcode-plugins-official.
External Claude Marketplace sources use .claude-plugin/marketplace.json as
their canonical manifest location. ZCode may normalize the fetched marketplace
manifest into ~/.zcode/cli/plugins/marketplaces/{marketplace}/marketplace.json
for local bookkeeping, but add/update must read .claude-plugin/marketplace.json
first and only use a root marketplace.json fallback for local legacy data.
Startup must seed bundled official plugins before plugin discovery:
Since 2026-10-30, Protocol Agent processes do not enable the plugin startup
budget: seed and discovery run to completion, seed lock waits keep their shared
15-second budget, and the process never emits startup/pluginsDegraded. A cold
seed of the bundled official plugins measured about 3.0-3.1 seconds on Windows,
so a 3-second budget that includes seed degraded every first start after a
plugin version bump. The budget remains an opt-in owner option; the rest of this
section is its contract when an owner enables it. Re-enabling it in the
Protocol Agent requires moving seed out of the budget or another confirmed scope.
When enabled, the owner gives seed and discovery one shared soft budget per
in-flight batch, including admission waits. If any batch times out, the
process returns an empty plugin outcome with plugin_startup_timeout and stops
starting plugin scans until restart. It does not change enabled/suppressed
configuration or installed records, or scan an old cache for this fallback.
Restoring a bundled plugin after cancellation fails before changing suppression.
Existing Runtime snapshots retain their capabilities; new Runtime creation can
continue without plugin skills, commands, MCP servers or hooks.
The owner aborts further work, observes late results, and emits
startup/pluginsDegraded for a workspace-scoped UI warning. A directory
publication already in progress must finish or roll back before its lock is
released. The budget cannot cancel an in-flight kernel operation or guarantee
the entire App starts within the budget. Standalone management commands
without this process owner retain their existing contract.
The Android plugin keeps the project as its MCP working directory, but that
directory is never a source of SDK tools, Java, or the Windows command interpreter.
SDK installation paths take precedence over PATH. Lookup does not execute
where or which: it enumerates absolute PATH directories, skips empty and
relative entries, and rejects both lexical paths and real paths inside the
project (including symbolic links and Windows junctions). Candidates must be
regular executable files; SDK/JDK roots must be external directories. Child
PATH entries use the same directory checks. Missing tools report an unavailable
dependency rather than falling back to a bare command name.
Java runs only through the validated JDK's absolute bin/java path.
Java-based SDK scripts and system Gradle also require a valid JDK before lookup,
so their own Java fallback cannot reintroduce workspace execution. Windows
batch files use the validated system directory's absolute System32/cmd.exe.
An explicitly requested build may still run the project's Gradle wrapper;
project scripts and host dependency discovery remain separate operations.
SDK candidates → absolute PATH entries → external real path → execute
↓ invalid
skip / report missing
0.1.1 bundle → versioned cache → new MCP process
↓ seed failure
Android unavailable; no older-version fallback
Package, manifest, Bootstrap, SEA, and MCP server versions move together to 0.1.1. Android follows the existing fail-closed cache fallback branch: a failed update must not reactivate 0.1.0. Existing MCP processes need restarting to use the fix. iOS behavior is outside this change.
Regression evidence covers workspace fake tools, SDK priority, relative/empty PATH entries, external links into the workspace, SDK/JDK rejection, missing Java, Windows interpreter selection, explicit Gradle wrapper execution, and failed cache promotion with an older Android version present. Tests use harmless marker files to detect execution. Native Windows validation is recorded separately from portable path tests.
The normal seeding layout is:
packages/android-emulator-plugin/
-> ~/.zcode/cli/plugins/cache/zcode-plugins-official/android-emulator/0.1.1/
packages/ios-simulator-plugin/
-> ~/.zcode/cli/plugins/cache/zcode-plugins-official/ios-simulator/0.1.0/
packages/browser-use-plugin/
-> ~/.zcode/cli/plugins/cache/zcode-plugins-official/browser-use/0.3.0/
packages/document-skills-plugin/
-> ~/.zcode/cli/plugins/cache/zcode-plugins-official/document-skills/0.1.4/
packages/restore-legacy-sessions-plugin/
-> ~/.zcode/cli/plugins/cache/zcode-plugins-official/restore-legacy-sessions/0.1.0/
packages/skill-creator-plugin/
-> ~/.zcode/cli/plugins/cache/zcode-plugins-official/skill-creator/0.1.0/
packages/zcode-guide-plugin/
-> ~/.zcode/cli/plugins/cache/zcode-plugins-official/zcode-guide/0.3.0/
packages/bundled-skills/ is deliberately absent from this list: it is the CLI's
built-in skill pack, not a plugin (see skill.md, "bundled skill pack"). It is
never seeded into the official plugin cache, never listed in the marketplace
partition, and cannot be disabled or uninstalled.
Discovery shares Skill file scans within one query, keyed by root and symbolic link policy. Counts and component names retain their separate deduplication rules. Missing materials preserve ENOENT/ENOTDIR without another read, and are still revalidated before returning. Markdown reads run in batches of at most four; the material-cache owner also caps active reads across queries at four. Results keep their original order, and invalidation/disposal prevents queued stale reads from starting. This does not defer session reference-catalog data.
Discovery 的路径检查由本次查询的 material reader 统一持有。同一路径的并发检查 共享结果,Skill 扫描复用 reader 已取得的文件类型;符号链接拒绝规则保持不变。 插件根目录清单及已读取的子目录清单可排除缺失的标准入口,缺失结论依赖相应父目录, 不再为每个缺失子路径发起独立检查。仅在名称匹配规则确定时使用该优化;大小写等价 名称保留实际路径检查,目录枚举失败也回到原有检查,不把权限错误当成缺失。 返回前仍复核实际文件和父目录;目录新增入口、文件变化或目录替换使本次结果失效, 沿用现有有界重跑。缺失结论不跨查询独立保留,不新增持久化缓存或 watcher。
Filesystem releases compare the installed seed marker's pluginVersion with
the official plugin definition and check the cached manifest and required entry
paths. A matching usable version is reused without enumerating or hashing the
source tree, including after a process restart. Resource changes must bump the
plugin package, manifest, official definition and applicable packaging versions.
Same-version edits are not detected automatically, including in development;
remove the corresponding cache or bump the version to rebuild it. Invalidating
the process preparation state only rechecks this contract. Old marker hashes are
ignored for filesystem sources; new filesystem markers omit the hash.
Source files are enumerated only when rebuilding, then each file is read once
for copying. Validate the staged manifest and required paths before publication.
A definition may declare buildIdentityPath when its build output embeds a
per-build value that does not change the plugin version. node-repl-host uses
dist/build-identity.json, written by its build script, because
dist/mcp/server.js embeds the CUA Helper build id. Source preparation reads
only that small file and stores its contents in the marker as buildIdentity.
A different value, or a legacy marker without the field, rebuilds the cache like
a version change. A missing source file compares as no identity and does not
block seeding.
SEA releases embed the same files as SEA assets and extract them to the same official cache path on first startup or when the embedded plugin hash changes. The MCP command must point at the seeded cache directory, not the source workspace and not a module compiled into the ZCode SEA main bundle.
Seeding replaces one plugin version directory only while it holds the seed lock
for that version. It writes the new tree to a unique <version>.tmp-* sibling.
It moves the old target to a unique <version>.backup-* sibling, then renames
the temporary tree to the target, so a failed promotion can restore the old
target. A directory rename can return EXDEV inside one parent directory when
the old target is in a lower layer of an overlay filesystem (for example, a
container image that already contains the plugin cache). In that case, seeding
deletes the seed marker of the old target first, deletes the old target, and
then promotes the temporary tree. Because the marker goes first, an interrupted
deletion cannot leave a partial tree that looks current; the next startup seeds
it again.
A seed failure that ZCode cannot complete degrades only the affected plugin:
transient errors after the retry budget, a seed lock timeout, ENOENT after a
concurrent writer already promoted a current tree, and EXDEV. ZCode logs
Official plugin cache operation degraded, keeps a usable older cache when one
exists, and does not fail session startup or session resume. EXDEV is not
retried, because it does not go away on retry.
For bundled official plugins only, ZCode rewrites the seeded
.zcode-plugin/plugin.json MCP server command at seed time. The rewritten
command launches the current ZCode executable through the hidden
__zcode-plugin-host entrypoint and asks it to import the cached MCP server file
and call its exported main() function. In a SEA build this means the official
plugin MCP servers run on the SEA-embedded Node.js runtime, but each server
module is still loaded from
~/.zcode/cli/plugins/cache/zcode-plugins-official/....
The CLI main entry dispatches __zcode-plugin-host before importing the Agent
command runner or provider initialization. It retains environment sanitization,
SEA runtime tool preparation, product broker authorization, argument forwarding,
and the existing success/failure shutdown policy. Hosted server arguments are
not interpreted as Agent/provider options. Desktop Electron Node, ordinary Node
CLI and SEA use this same branch; no separate plugin execution engine is added.
This runtime host is not automatically projected to third-party plugins. Inline
and external plugins keep their manifest-provided command and args; if a
third-party plugin explicitly opts into the hidden host, it is treated like any
other local command in the existing plugin trust model.
A plugin id is:
<plugin-name>@<marketplace>
Session/local development plugin directories use the marketplace inline, for
example ios-simulator@inline. Official bundled plugins use
zcode-plugins-official.
The bundled official capabilities use these ids:
android-emulator@zcode-plugins-official
browser-use@zcode-plugins-official
document-skills@zcode-plugins-official
ios-simulator@zcode-plugins-official
restore-legacy-sessions@zcode-plugins-official
skill-creator@zcode-plugins-official
zcode-guide@zcode-plugins-official
P0 enablement is config-driven:
{
"plugins": {
"enabled": true,
"dirs": ["/absolute/path/to/plugin"],
"enabledPlugins": {
"android-emulator@zcode-plugins-official": true,
"ios-simulator@zcode-plugins-official": true,
"restore-legacy-sessions@zcode-plugins-official": true
},
"options": {
"android-emulator@zcode-plugins-official": {
"default_avd": "medium_phone",
"api_level": "35"
},
"ios-simulator@zcode-plugins-official": {
"default_device": "iPhone 16"
}
}
}
}dirs are explicit local plugin roots and default to enabled for the current
configuration unless enabledPlugins[id] is false. Official cache plugins
default to disabled unless enabledPlugins[id] is true.
内置 visualize 默认关闭,并在首次插件配置加载时将旧启用值关闭一次。 迁移完成标记与开关保存在同一配置文件;用户重新开启后不再重置。 只读配置或锁失败只输出告警,本次加载按该层关闭继续合并,不能阻断插件查询或会话装配。 用户级与项目级分别迁移,详见 Visualize 加载边界。
For a plugin root, ZCode reads the first manifest that exists:
.zcode-plugin/plugin.json.claude-plugin/plugin.json
.zcode-plugin/plugin.json intentionally uses the same field names as Claude
Code. P0 supports these fields:
nameversiondescriptionauthorlicenseskillscommandsmcpServersuserConfig
The selected manifest may contain MCP configuration directly in mcpServers.
This is the preferred override point when .zcode-plugin/plugin.json needs
ZCode-specific runtime wiring while .claude-plugin/plugin.json and .mcp.json
remain Claude-compatible.
Compatibility levels:
run: projected into an existing ZCode runtime capability.install-time only: used while installing or resolving plugin state, not as a runtime component.diagnostic only: recognized and reported, but not executed.unsupported: recognized when possible and reported as unsupported.
Unsupported or diagnostic-only fields are ignored with diagnostics:
outputStyleslspServerschannelssettings- marketplace-only metadata not needed at runtime
agents declarations without a discovered source file, such as object metadata
or a single-file path, do not produce runtime profiles; directory support is
defined under Agents.
dependencies is install-time only for Marketplace-installed plugins. It must
not be reported through the legacy plugin_unsupported_component warning once
the dependency resolver is enabled.
marketplace and source are separate model dimensions. marketplace is part
of plugin identity (<plugin-name>@<marketplace>). source describes the local
loading source (official, inline, cache, or a future more specific cache
source). Do not add a marketplace value to PluginSource.
Marketplace source compatibility:
url,github,git,file,directory, andsettingsare recognized.githubandgitsupportrefandpath; the default path is.claude-plugin/marketplace.json.npm,hostPattern, andpathPatternare recognized but diagnostic-only in P0.
Plugin entry source compatibility:
- Relative paths (
"./plugin") and local directory sources are installable. github,git,url, andgit-subdirsources are installable by cloning. They supportref,sha,commitas ashafallback, and entrypath/subdirectory installation because the official Claude marketplace uses all of those shapes.npmandpipare recognized and diagnosed until their installer paths are implemented.
Marketplace source validation is intentionally lighter than plugin validation. It parses the marketplace manifest, validates entry shape and dependencies, and deep-scans plugin roots that are already present in the marketplace source directory. It must not clone every remote entry while validating a marketplace source: the official Claude marketplace contains hundreds of entries that point at external git repositories. Remote entry roots are fetched and deep-scanned only during install or single-plugin validate, with marketplace source validate returning a deferred diagnostic for those entries.
manifest.skills may be a relative path or an array of relative paths. Paths
resolve inside the plugin root and become SkillRoot entries with
source: "plugin". The existing Skill tool is still responsible for loading
the full SKILL.md body on demand.
显式声明 skills 时,只使用声明路径,不补充默认 skills/;空数组表示不加载
技能。只有未声明该字段时,才使用存在的默认 skills/。命令等其他组件规则不变。
每个插件技能入口若自身有可加载的 SKILL.md,只加载该技能,不再枚举子目录;
否则按技能集合检查一层子目录。字符串和数组均支持这两种目录布局。需要加载单技能
入口下的其他技能时,必须另行声明其路径。路径越界、符号链接拒绝和缺失声明诊断
继续生效;计数、组件详情和运行时加载使用相同规则,用户级及项目级技能扫描不变。
有声明 → 仅声明路径;无声明 → 默认 skills/
入口自身有 SKILL.md → 只加载自身
入口自身无 SKILL.md → 扫描一层子目录
manifest.commands may be a relative path or an array of relative paths. Paths
resolve inside the plugin root and become CustomCommandRoot entries with
source: "plugin".
Claude command filenames may include _, -, : and nested directory
segments. ZCode keeps those names stable, for example
commands/clean_gone.md becomes /clean_gone.
P0 also supports a skills frontmatter field in markdown commands. When a
custom command declares skills: ios-dev or skills: android-dev, prompt
expansion tells the model to load that skill before performing the command.
ZCode 枚举默认 agents/*.md 与 manifest agents 字符串或数组声明的插件根内目录。
组件名称优先取 frontmatter name,缺失时取文件名;发现结果同时保存实际源文件 path。
加载器只读取该路径,不从名称重新拼接文件名,也不为缺少路径的描述性组件猜测文件。
文件读取失败保留 agent_read_failed 诊断,不查找替代文件。
已启用插件中的 profile 以 <plugin-name>:<agent-name> 加载到子代理运行时;名称无冲突时
同时提供裸名别名。组件仍按名称去重:manifest 对象声明先于文件,默认目录先于额外目录,
重复声明的目录只扫描一次;首次发现的条目保留其来源。对象声明仅提供描述,无源文件路径时
不加载。目录继续使用插件根内路径校验;组件名称不作为路径输入。源路径只用于 CLI 内部,
对外组件详情仍只投影名称与描述。
插件 profile 的 frontmatter permissionMode 只能收紧子会话权限,官方与第三方插件同一规则:
子会话取父会话模式与声明模式中更严格的一个(从严到宽为 plan、build、edit、guarded、yolo)。
bypassPermissions、dontAsk 因此不会生效;auto 尚未实现,不参与比较,按继承处理。
加载器保留声明值并给 profile 打 permissionModeRestrictOnly 标记,派生子会话时由 core 比较。
规则与威胁边界见 项目配置信任边界。
ZCode loads MCP server config from both:
- plugin
.mcp.json manifest.mcpServers
manifest.mcpServers may be either an inline object or a relative path to an
MCP config file. It is valid inside .zcode-plugin/plugin.json, for example:
{
"name": "ios-simulator",
"mcpServers": {
"ios-simulator": {
"command": "node",
"args": ["${ZCODE_PLUGIN_ROOT}/dist/mcp/server.js"],
"cwd": "${ZCODE_PROJECT_DIR}"
}
}
}Manifest mcpServers wins on server-name collisions. That means a plugin can
keep a Claude-compatible .mcp.json for Claude Code and put a ZCode-specific
MCP command in .zcode-plugin/plugin.json.
A .mcp.json file may use Claude's wrapper shape:
{
"mcpServers": {
"ios-simulator": {
"command": "node",
"args": ["${CLAUDE_PLUGIN_ROOT}/dist/mcp/server.js"]
}
}
}A plugin MCP server runs once per session by default. Official plugins
(zcode-plugins-official) may declare "isolation": "workspace" to share one
process among all sessions of a workspace; such a server must keep its own state
apart per session using the host's _meta.session_id (the document viewer
plugins do, see docs/plugins/2026-10-15-office-viewer-builtin-panels-spec.md
§8 in the repository root). ZCode ignores isolation on other plugins.
If a server has command but no type, ZCode infers type: "stdio".
Server names are normalized for model-visible tool names by replacing
non-identifier characters with underscores. A server named ios-simulator
therefore exposes tools such as mcp__ios_simulator__ios_preflight; a server
named android-emulator exposes tools such as
mcp__android_emulator__android_preflight.
The plugin loader normalizes external userConfig entries before they reach runtime
or protocol metadata. Unknown extension fields (for example options) are stripped
from the in-memory projection; source and installed manifest files are not rewritten.
Supported fields retain the shared protocol's type validation, including sensitive.
An invalid known field rejects only that plugin and emits plugin_manifest_invalid
with its plugin ID. Other plugins remain discoverable. App/Agent response schemas
remain strict.
Plugin component config may reference:
${CLAUDE_PLUGIN_ROOT}
${CLAUDE_PLUGIN_DATA}
${CLAUDE_PROJECT_DIR}
${ZCODE_PLUGIN_ROOT}
${ZCODE_PLUGIN_DATA}
${ZCODE_PROJECT_DIR}
${user_config.KEY}
${GITHUB_TOKEN}
${TOKEN_PLAN_API_TOKEN}
${ZCODE_SOME_ENV}
Claude and ZCode variable names are synonyms. MCP secret sinks (stdio.env,
HTTP/SSE headers, and OAuth clientSecret) expand conventional process
environment variable names such as GITHUB_TOKEN and TOKEN_PLAN_API_TOKEN.
Missing referenced values disable only the affected MCP server and produce a
plugin_variable_missing diagnostic instead of forwarding the unresolved
placeholder.
Arbitrary process environment variables are intentionally not expanded in
model-visible or process-list-visible fields such as command, args, cwd,
url, OAuth clientId, or OAuth scope. Existing ZCODE_* runtime wiring
variables remain available in those fields for backward compatibility.
userConfig defaults are used when no explicit plugin option exists. Sensitive
userConfig values follow the same sink boundary: options marked sensitive
may only be expanded in MCP secret sinks.
状态:已实现(扩展进程 P2d,用户决策)。总设计见 扩展进程 §14、§16.7。 代码:
adapters/src/config/plugin-secrets.adapter.ts(文件 I/O 与 schema)、bootstrap/src/plugin-secrets.ts(叠加、保存、清理规则)。
敏感值(userConfig[key].sensitive === true)和普通值一起明文写在
plugins.options[id] 里:user 层是 ~/.zcode/cli/config.json,workspace 层是
<workspace>/.zcode/config.json(bootstrap/src/plugins.ts 的
resolvePluginConfigPath)。
- workspace 配置可以进 Git,token 会被提交。
- user 配置常被复制、粘贴、同步,token 会被一起带出去。
plugins/list投影时已过滤敏感值(bootstrap/src/zcode-protocol/plugins.ts), 但文件本身仍是明文。
敏感值只存进一个私有文件,config.json 里不再写敏感值。
- 路径:
<cli storage root>/plugin-secrets.json,即默认的~/.zcode/cli/plugin-secrets.json。远程工作区的 CLI 在远端主机上,用它自己的文件。 文件始终与插件存储目录plugins/同级:调用方覆盖插件存储目录时(测试、嵌入), 文件跟着放在被覆盖目录的父目录里。 - 写入:
atomicWritePrivateTextFile(0600),在withFileLock内读改写。 Windows 上没有 POSIX 权限位,文件留在用户目录,权限沿用用户目录的 ACL。 - 形状(zod schema 校验):
{
"version": 1,
"user": { "<pluginId>": { "<key>": "<value>" } },
"workspaces": {
"<workspace root>": { "<pluginId>": { "<key>": "<value>" } }
}
}<workspace root>是 workspace 配置所在目录的绝对路径,即项目配置文件的 baseDir (<dir>/.zcode/config.json与<dir>/zcode.json都是<dir>)。比较规则与normalizePluginConfigPathForComparison相同;写入时键存规范化后的路径。- 读取层与
config.json一一对应:user 层随 user 配置读取(跳过 user 配置时不读); 每个被发现的项目配置文件是一个 workspace 层,用它的 baseDir 找私有文件里的条目。 workspace scope 的 configure 总会写<workspace>/.zcode/config.json(至少留一个空的options[id]),所以保存过的 workspace 敏感值一定有可被发现的层。
规则:
| 操作 | 敏感键 | 普通键 |
|---|---|---|
| configure(写值) | 写进私有文件的对应 scope;同一 scope 的 config.json 里若有同名键,同一次操作删掉(迁移) |
不变,写 config.json |
configure(clearOptionKeys) |
私有文件与 config.json 两处都删 |
不变 |
| 读取(运行时与投影) | 每个 scope 先读私有文件,再读 config.json 的旧明文值;私有文件优先。scope 优先级不变 |
不变 |
| reset | 与 config.json 同语义:删除该 scope 下这个插件的条目 |
不变 |
| uninstall | 删除 user 与全部 workspaces 下这个插件的条目 |
不变 |
- 不在启动时迁移。旧的明文值继续能读;用户下次保存这个插件的设置时,敏感键才迁进 私有文件。理由:启动时改写 workspace 配置会在用户仓库里产生意外的 diff。
- 键是否敏感以当前清单的
userConfig为准。清单把某个键改成敏感后,旧的明文值仍可读, 下次保存时迁移。 - 「下次保存」指这个插件在该 scope 的任意一次非
dryRunconfigure:该 scope 的config.json里所有按当前清单为敏感的旧明文键都迁走,包括这次请求没带的键。理由: UI 不回传敏感值,只保存普通键的请求里本来就没有已存的 secret;只迁请求里的键,旧明文会 永远留在文件里。clearOptionKeys里的键不迁,直接两处都删。 - 写入顺序:先写私有文件,再改
config.json。第二步失败时两处都有值,读取以私有文件为准, 下次保存再删;不会出现两处都没有的中间态。 - 投影:
plugins/list的optionSources也列出私有文件里的键(scope 标记照旧),设置页的 scope 标记与「清除」按钮因此不受迁移影响;configuredOptions仍过滤敏感值。 - 私有文件损坏(JSON 或 schema 不符):
backupCorruptFile备份后按空文件处理,发出诊断plugin_secrets_unreadable(severity: error,带文件路径,不含任何值)。受影响的 MCP server 按现有规则报plugin_variable_missing。其他读取错误(例如权限)在读取路径上 同样按空处理并发出同一诊断,但不备份;写入路径上向上抛,不覆盖读不出的文件。 损坏文件在下一次写入时被替换成有效内容(备份留着)。 - 日志、诊断、错误信息里只出现插件 id 与键名,不出现值。
扩展进程的 settings 能力要拿到本插件的敏感值(用户决策:只给本插件)。
plugins/list 继续过滤敏感值,另加一个只读方法:
plugins/readUserOptions,参数{ pluginId },返回{ values: Record<string, string | number | boolean> }。- 值是 user 层的有效值:清单默认值,再叠加 user 层选项(含敏感值)。不含 workspace 层: 扩展只认用户级启用(扩展进程 spec §13)。
- 插件未安装返回
-32602(invalid params),data.code为plugin_not_found;旧 CLI 返回 method-not-found,App 当作「不支持」,扩展只拿到非敏感值。 - 「已安装」指插件出现在 user 视图的插件发现结果里(inline、official、cache 都算)。
已安装但被禁用的插件照常返回值。参数只有
pluginId(strict),不带 workspace: 扩展是全局的,user 层与工作区无关。 - 只给 App 用,不进模型可见的工具,也不进 debug 日志。
- App 侧只在 Window Host 进程内持有:不挂在
IZCodeAgentService上(该服务整个频道经 RPC 暴露给 桌面 Renderer 与手机远控),由packages/services/src/node.ts的getPluginUserOptionsReader(services)取出,作为依赖交给扩展网关;网关只把调用方自己插件的值交给它。任何 RPC 频道都调不到这个读取器。 - 收到即处理,不排在串行请求队列之后(与
plugins/list同组)。
- configure 敏感键:值进私有文件,文件权限 0600(非 Windows),
config.json里没有该键。 - 旧明文:
config.json里已有的敏感值能被运行时读到;下次 configure 后移进私有文件并从config.json删除。 - 运行时展开:
${user_config.KEY}在 MCP secret sink 里读到私有文件的值;非 sink 字段 仍拒绝。 - workspace scope:敏感值按 workspace root 存进私有文件,
<workspace>/.zcode/config.json里没有;嵌套 workspace 仍按最内层优先。 plugins/list仍过滤敏感值;plugins/readUserOptions返回含敏感值的 user 层有效值。- uninstall 与 reset 清掉私有文件里的对应条目。
- 私有文件损坏:备份、按空处理、发出不含值的诊断。
状态:已实现(扩展进程 P2c)。总设计见 扩展进程 §16.6。 代码:
bootstrap/src/zcode-protocol/plugin-mention-items.ts;线格式在packages/shared/src/zcode-protocol/plugin-host-queries/contract.ts。
清单 slots["composer.mention"] 的 tool target 由 CLI 执行。Window Host 读清单、校验条目,
CLI 只负责在会话里调工具、校验结果。CLI 不解析 slots。
- 方法:
plugins/mentionItems,参数{ sessionId, pluginId, tool, query }(strict), 返回{ items }。tool是插件 MCP server 上的原始工具名(不是模型可见的mcp__…名)。query可以是空串,最长 1000 字符。 - 只在内存里的会话上执行:不启动、不恢复会话,也不新建 MCP 连接。会话不在内存 →
-32004(zcodeProtocolErrorCodes.sessionUnavailable)。草稿用 App 的预热会话。 - 插件身份取会话创建时冻结的 plugin catalog(
getPluginReferenceCatalog())。插件不在 catalog、未启用、或与同名插件冲突(conflictingPluginIds非空)→plugin_mention_plugin_unavailable。 - 找工具:按 catalog 里这个插件的 MCP server 名(已排序)依次查会话 tools/list,取第一个有
tool的 server。都没有 →plugin_mention_tool_not_found。工具可见性不含app(只给模型的工具)→plugin_mention_tool_not_app_visible。 - 调用:与 MCP App 页面的
mcp/uiCallTool走同一条路径——session facade 的callMcpToolForUi(server, tool, { query }):工具 schema 校验 → PreToolUse hook → 权限规则 / 审批(ToolExecutor.authorizeAppTool)→ 连接代际复验 → 会话现有 MCP 端口的callTool(runtimeScope: "main",带会话的 trace)。不另开绕过审批的宿主调用路径。 参数固定为{ query };服务器还没连上时按调用失败处理。超时沿用该 server 的 MCP 设置, CLI 不另加。不产生 transcript row。 - 审批:MCP 工具一律
needsApproval。会话第一次调这个工具时,可能在该会话里弹出审批, 请求一直等到用户回应(不另设超时);用户选「始终允许」后,同一会话后续调用不再询问。 deny 规则、用户拒绝、hook 拒绝(授权链抛permission_denied,或permission_escalation)→plugin_mention_tool_denied。每次查询都会跑 PreToolUse hook。 - 结果:
isError: true或调用抛出其他错误 →plugin_mention_tool_failed。structuredContent.items按严格 schema 校验,不符 →plugin_mention_result_invalid:
| 字段 | 规则 |
|---|---|
items |
数组,最多 50 项 |
id |
必填,1–200 字符 |
label |
必填,1–120 字符 |
description |
可选,最多 500 字符 |
icon |
可选,1–100 字符 |
insertText |
必填,1–4000 字符 |
| 其他字段 | 拒绝(条目对象是 strict) |
- 错误形态:
ProtocolRequestError,data.code是上面的稳定码。引用类错误 (plugin_unavailable、tool_not_found、tool_not_app_visible)用-32602;执行类错误 (tool_denied、tool_failed、result_invalid)用-32000。错误信息只含插件 id、server 名、 工具名与 schema 路径,不含 query、拒绝原因与工具返回的内容。 - 不设计取消。App 每次查询带递增的
seq,只采用最新一次的结果;过期的请求照常跑完 (等待审批的请求也照常等待,回应后按结果返回)。 - 收到即处理,不排在串行请求队列之后(与
plugins/list同组)。 - 日志:每次调用一条
debug(插件 id、工具名、server 名、耗时、项数或错误码),不记 query 与结果。理由:按键级频率,按根规则走debug。 - 旧 CLI 返回 method-not-found,App 不显示这一组。
测试(先写测试再实现):
- 会话不在内存 →
-32004,不调任何工具。 - 插件不在 catalog、未启用、名字冲突 →
plugin_mention_plugin_unavailable。 - 工具不存在 →
plugin_mention_tool_not_found;工具只给模型 →plugin_mention_tool_not_app_visible;两种情况都不调用工具。 - 正常:在插件自己的 server 上以
{ query }调用,返回校验后的items。 isError、调用抛错 →plugin_mention_tool_failed。- 超过 50 项、
insertText超长、未知字段、缺structuredContent→plugin_mention_result_invalid,错误里不含工具返回的内容。 - 授权链拒绝 →
plugin_mention_tool_denied,错误里不含拒绝原因。 - 审批等待中请求不返回;批准后按结果返回。
- facade
callMcpToolForUi:先跑授权链再以批准后的输入调工具;授权链拒绝时不调工具。
Plugin loading itself only reads local files. Starting an MCP server remains a side effect and goes through the existing MCP trust and permission system.
P0 policy:
- Explicit local plugin dirs are trusted for the current configuration.
- Official cache plugins are loaded only when enabled in config.
- Only bundled official cache plugins are automatically rewritten to the internal ZCode/SEA plugin host.
- Project-discovered plugin dirs are out of scope.
- Hook files from every enabled cache plugin, including non-official
third-party Marketplace plugins, are injected into the runtime. Per-plugin
trust is not implemented; see
canRunPluginHooks.
P0 CLI commands:
zcode plugins list
zcode plugins enable <plugin-id-or-name>
zcode plugins disable <plugin-id-or-name>
zcode plugins uninstall <plugin-id-or-name> [--force]
If a bare name matches exactly one discovered plugin, enable/disable/uninstall
resolves it to that plugin id. Ambiguous names require the full
<name>@<marketplace> id.
uninstall performs a thorough teardown (see "Uninstall And Teardown"). It is
destructive, so it confirms before acting:
- On an interactive TTY without
--force, it prints the plugin id and promptsUninstall <plugin-id>? [y/N]. Only an affirmative answer proceeds. - When stdin is not a TTY (pipes, CI) and
--forceis absent, it refuses with a non-zero exit and a message telling the caller to pass--force. It never uninstalls silently in a non-interactive context. --forceskips the prompt in every context, which is the scriptable path.
Uninstall removes only the explicitly named plugin. It does not cascade into dependency plugins that were installed as part of that plugin's dependency closure; an orphaned dependency stays installed until it is uninstalled by id.
The TUI exposes plugin enablement through the local slash command /plugins.
Submitting /plugins or /plugins list opens a composer selection panel with
one row per discovered plugin. Rows show the current enabled state, plugin id,
source, version, and projected skill/command/MCP counts.
Pressing Enter on a row toggles that plugin by dispatching either
/plugins enable <plugin-id> or /plugins disable <plugin-id>. The handler
writes only plugins.enabledPlugins in the user config and returns a refreshed
selection panel. The current session does not hot-reload plugin-provided skills,
commands, or MCP servers; the changed capability set is picked up by the next
session startup.
/plugins uninstall <plugin-id-or-name> performs the same thorough teardown as
the CLI. Because the slash handler is stateless and cannot run an interactive
prompt, it gates the destructive action on an explicit --force token:
/plugins uninstall <plugin> --force. Without --force it returns a
confirmation message describing what will be removed and does not mutate
anything. The marketplace-installed list in the settings UI exposes the same
operation through an Uninstall control with a confirmation dialog.
contractsdefines plugin metadata, diagnostics, config, and resolver output.adaptersowns filesystem discovery, manifest parsing, path validation, variable expansion, and plugin data directory creation.bootstrapresolves plugins once per app/session startup and merges component roots into existing skill, command, and MCP configuration. It also exposes a plugin management facade for local CLI/TUI commands to list plugins and update user-configured enablement.- Official plugin seeding is data-driven from the bundled plugin definition
list. Adding another official plugin requires a workspace package with a
.zcode-plugin/plugin.json, SEA asset inclusion through the same official plugin manifest, Electron/remote asset staging entries, and service-side default-enabled mirrors when the plugin is content-only and default-enabled. Runtime-backed plugins must also include a Node-builtdist/mcp/server.js. Content-only plugins may omit the MCP runtime.browser-useis runtime-backed and must keepdist/mcp/server.js,scripts/browser-client.mjs, both shipped skills, and effective docs in all official plugin asset allowlists. Filesystem/SEA seed, Desktop prepare, and production/development remote deployment treat these as required assets rather than optional copied files, so an incomplete Browser Use package is rejected or redeployed before a session can load it. coredoes not import plugin loader code and continues to see normal skills, custom commands, and MCP tools.
Minimum P0 coverage:
- Prefers
.zcode-plugin/plugin.jsonover.claude-plugin/plugin.json. - Loads local plugin
skills,commands, and.mcp.json. - Infers stdio MCP type when
commandis present. - Expands Claude and ZCode plugin variables.
- Expands arbitrary process environment variables only in MCP secret sinks.
- Keeps arbitrary process environment placeholders unresolved in command, argument, working-directory, URL, and non-secret OAuth fields.
- Uses
userConfigdefaults and explicit config options. - Disables only the affected MCP server when variable expansion fails.
- Ignores hooks with diagnostics.
- Seeds iOS Simulator and Android Emulator as disabled official plugins, then projects their skills, commands, MCP servers, user config defaults, and hidden plugin host command only after each id is enabled.
- Seeds default-enabled official plugins into CLI SEA, Electron desktop bundle, and remote agent resource layouts.
Browser Use is runtime-backed and must include and validate
dist/mcp/server.js,scripts/browser-client.mjs,docs/, and bothskills/; Document Skills, Skill Creator, and ZCode Guide remain content-only. - CLI list/enable/disable preserves unrelated config.
- CLI uninstall removes the installed record, cache directory, per-plugin data
directory, and the plugin's
enabledPlugins/optionsconfig entries while preserving unrelated config; it refuses without--forceon a non-TTY and is a no-op for an unknown plugin id. - TUI
/pluginsopens a selection panel and toggles plugin enablement. - TUI
/plugins uninstallrequires--forceand otherwise returns a confirmation message without mutating state.
The bundled plugin-creator follows the local development lifecycle in the creator spec. It prepares source files and a dev marketplace catalog in the selected workspace, then guides the user to add the market, install/update the plugin and try it through the existing UI. A global CLI or Node installation is not a prerequisite. Registration and installation remain owned by the existing plugin services and Host User inventory; project scope is not a separate installation namespace. No new plugin tool, protocol or installation registry is introduced.