What a plugin can surface
Enabled plugin components fold into the agent namespaced as<plugin-id-last-segment>:<name>. Native components always shadow a plugin
component on a name clash.
Prompt macros expand
$ARGUMENTS, $1–$9, and @file references, then run as
a normal turn. Macros whose body is a shell (!bash) command are filtered out:
a command file is a prompt template, not an execution channel.
LSP servers
A plugin’slspServers (or .lsp.json) entry describes a language server process.
Ziro honours command, args, extensionToLanguage, env, workspaceFolder,
settings, initializationOptions, the timeouts (clamped), maxRestarts,
restartOnCrash and diagnostics. A server stays inert unless its plugin is
trusted at its current content, enabled and active in the thread, and has no
pending consent or missing option. It never runs in unattended (queue) runs or
under ZIRO_STRICT_SECURITY. It starts lazily, the first time a file of one of
its extensions is read or edited, in the workspace directory, with the same
scrubbed environment as plugin hooks. Language servers read project files and may
run project-defined code by design, so only trust a plugin whose server you accept
running on that repository.
- After a successful
write_fileoredit_file, the server’s errors and warnings for that file are appended to the result as untrusted plugin data. When the server cannot answer (failed, restarting, slow) the result says so instead of staying silent. - The
lsp:*tools (definition, references, hover, symbols, call hierarchy) are deferred: find them withsearch_toolsand activate them withload_tools. They exist only for an agent that can read files itself, and a call is never more permitted thanread_file. /lsplists the servers and their state. An MCP server namedlspis reserved and skipped.
Discovery roots
Ziro scans, in order:Trust and consent
This is the security spine of the whole system. Third-party plugin code can run shell commands and spawn subprocesses, so those capabilities are inert until you explicitly trust the plugin. During import, Ziro extracts a grant for each capability a plugin wants. Three grant kinds are treated as dangerous:
Network access and http/sse MCP servers are not dangerous grants. They
surface without a trust decision, because they cannot execute local code.
Before anything installs, Ziro renders a consent manifest listing exactly what
the plugin wants, and waits for your decision.
1
Stage
Clone or copy the plugin at a pinned ref, import it, and build the consent
manifest. Nothing is active yet.
2
Decide
install records trust and enables everything. safe_only installs with the
dangerous parts inert. cancel discards the staged copy.3
Commit
Write the ledger entry and reload the registry.
Trust is content-hashed
Trust is keyed on plugin id plus a hash of its contents. Edit any file in a trusted plugin (including via an update) and the hash drifts, which auto-revokes trust until you confirm again. A plugin cannot be trusted once and then quietly changed underneath you. Revoking is symmetric: remove the plugin, or disable it, and its hooks and stdio MCP servers stop being surfaced.Installing
Without a flag, consent is interactive on a TTY and fails closed otherwise.
Managing
Marketplaces
A marketplace is a registry of plugin listings, parsed from a Claude Codemarketplace.json. Registered marketplaces live in a JSON file under your user
root.
local listing points at a subdirectory
of the marketplace repo, a github or git listing at a URL. Installing then
uses the normal staged-consent flow.
preapproved_marketplaces in the plugins section is discovery only. It
auto-registers a fetch origin so ziro plugin install <market>:<plugin>
resolves without a manual marketplace add. It never skips the consent
manifest and never grants trust.In chat
/plugins (alias /plugin) is one command covering the whole surface.
Install uses a staged-then-confirm flow rather than a blocking prompt, so it
works identically in the TUI and the REPL.
Changes hot-load without a restart: skills reindex, MCP servers reconcile through
sync_servers(), and prompt macros re-register.
Thread-scoped activation
Installation and trust are global. Activation is per thread. A thread entry inthreads.json carries an optional per-plugin override map.
Activation resolves per plugin:
/plugins disable foo in one thread turns foo off for that conversation
only, leaving your other threads untouched. To change the global default, use the
CLI (ziro plugin enable|disable) or install the plugin.
Enforcement is hybrid, applied at each point of use rather than by filtering one
list:
- Skill hits owned by an inactive plugin are dropped from search results.
- A hook belonging to an inactive plugin no-ops when invoked.
- A prompt macro declines at dispatch.
- Spawning an inactive plugin’s subagent is rejected. A spawned child inherits the parent’s active set.
- MCP servers are re-synced on a toggle rather than filtered at call time.
The plugins policy gate
Everything above describes what is currently on. The plugins section of
settings.yaml (was plugin_config.yaml) describes what is permitted at all.
It is a ceiling, not a switch: a thread override can narrow within it, but can
never widen past it.
This applies to the absence of the section, not to an unset enabled: key
inside one. A section that only narrows with allow / deny still means the
author wanted plugins, and stays permissive within its allowlist.
plugins section resolves through the standard chain, most-specific first.
At each layer Ziro reads the settings.yaml section or, as a fallback, a
legacy split plugin_config.yaml:
default (the Generalist) ships this section, and it
ships permissive: discovering and using whatever a task needs is its whole job.
The other shipped agents ship no plugins section and are therefore plugin-free
until you add one.
A malformed
plugins section fails closed: no plugins, plus a loud
warning. The section existing means someone meant to constrain something, so a
parse error must not be read as “no constraints”.Project layers are trust-gated too
Theplugins section is dangerous config. A project-layer copy is ignored
wholesale until you trust the folder, and folder trust is content-hashed the same
way plugin trust is. A cloned untrusted repo cannot set plugin policy on you.
Try a plugin for one session
Point--plugin-dir at a plugin folder or a .zip that holds one (repeatable), or export
ZIRO_PLUGIN_DIRS (absolute paths, separated by :, ; on Windows):
inline/<hash>/<name>, where the hash comes from
where the plugin lives: config values, secrets, data and trust belong to that source, so
two plugins that happen to share a name never see each other’s settings, and moving the
folder (or rebuilding the zip) asks again. An installed plugin with the same name is replaced
for the session. Switch an inline plugin off with "<name>@inline": false in the
enabledPlugins of your Claude Code settings.
An inline plugin is never trusted by being named: hooks, stdio MCP servers and remote MCP
servers stay off until you run /plugins trust inline/<hash>/<name> (the id is shown in
/plugins and ziro plugin info). Because a folder loaded this way is not copied, a root
node_modules or .mcpb-cache that holds files (code the content hash cannot cover) makes the
plugin impossible to trust; remove it or install the plugin instead. A .zip is unpacked
into a Ziro-owned cache, never your working directory.
Check a plugin before you ship it
hooks/hooks.json, .mcp.json
or .lsp.json that is not valid JSON), warnings and notes, and exits non-zero on an error.
The SDK is hermetic
A code-firstcreate_agent(...) surfaces zero plugins by default, so the same
script behaves identically on two machines regardless of what each has installed.
Opt in explicitly:
TypeError, so wrap it in a list. An empty list
normalizes to “no plugins”, kept distinct from True.
Next steps
Plugin CLI reference
Every
ziro plugin subcommand and flag.Skills
How plugin skills join the skill index.
MCP servers
How plugin MCP servers fold into the live manager.