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.
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.
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. After uninstall there is no stale enable flag or saved option 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:
packages/android-emulator-plugin/
-> ~/.zcode/cli/plugins/cache/zcode-plugins-official/android-emulator/0.1.0/
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.
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.
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.
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.
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.
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.