Skip to main content
A plugin is a folder of components that fold into your running agent. Ziro reads the Claude Code plugin format directly, so plugins written for Claude Code work here without conversion. The system has three independent layers, and confusing them is the main source of “why is my plugin not showing up”:

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’s lspServers (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_file or edit_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 with search_tools and activate them with load_tools. They exist only for an agent that can read files itself, and a call is never more permitted than read_file.
  • /lsp lists the servers and their state. An MCP server named lsp is reserved and skipped.
Hook events are mapped from Claude Code’s vocabulary onto Ziro’s. An event with no Ziro equivalent is skipped rather than guessed at.

Discovery roots

Ziro scans, in order:
The Claude Code import is read-only: Ziro reads your existing installation and surfaces those plugins, it does not rewrite anything Claude Code owns. Every parse and fetch failure is non-fatal. A malformed plugin is skipped with a warning; it never blocks startup. 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.
Dangerous grants are force-inert under ZIRO_STRICT_SECURITY, and in every background job: an unattended job runs with nobody watching, so it fails closed. A -p one-shot behaves like the TUI: it honors the trust you already recorded for the plugin’s exact content, and an untrusted plugin’s dangerous parts stay inert there as they do in the TUI.
Revoking is symmetric: remove the plugin, or disable it, and its hooks and stdio MCP servers stop being surfaced.

Installing

Two flags short-circuit the consent prompt, and both are decisions you are making in advance: 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 Code marketplace.json. Registered marketplaces live in a JSON file under your user root.
Listings resolve to a concrete source: a 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 in threads.json carries an optional per-plugin override map. Activation resolves per plugin:
So /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 TUI sidebar has an Active plugins section reflecting the thread-effective set.

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.
An agent that no config layer supplies a plugins section for surfaces ZERO plugins. Plugins are per-agent opt-in. A newly scaffolded agent does not silently inherit whatever happens to be installed on the machine.
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.
The 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:
Of the shipped agents, only 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

The plugins 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):
The plugin loads for that session only, with nothing written to the installed, enabled or trust records. It gets an id of its own, 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

The validator only reads: it extracts nothing, hashes nothing and writes no file. It reports errors (the plugin or a component would not load, including a 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-first create_agent(...) surfaces zero plugins by default, so the same script behaves identically on two machines regardless of what each has installed. Opt in explicitly:
Passing a bare string raises 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.