The plugin surface is young. Installation, enablement, the local development workflow and the manifest format are real and stable enough to build against. The
search, browse and update subcommands talk to a plugin registry that is not documented publicly yet, and publish runs as a dry run by default, preparing the package locally and reporting what it would ship.Where plugins live
Plugins are read from two roots:
A project plugin wins over a personal plugin with the same id. See Data locations for the rest of the layout, including where the audit journal is written.
The plugin commands
cortex plugin (alias cortex plugins) groups everything. Every subcommand accepts --help.
Inside a session,
/plugins opens the same list without leaving the timeline: Enter toggles the selected plugin, i installs, u updates and Esc closes.
Install a plugin, after reading what it registers
Installing a plugin is a two-step review. The first step prints what the package would add and a hash of that surface. The second step installs only if the hash still matches.1
Print the review
--json prints the review and the hash without installing anything: the plugin id and version, every command with its aliases and arguments, every hook and tool it registers, and a command_hash over that surface.2
Read the command list
Treat the command list as the security boundary. A plugin that registers a hook on tool execution sees every tool call in the session, and a plugin that registers a tool adds something the agent can decide to call on its own. If a name or a scope surprises you, stop here.
3
Accept the exact hash
--accept-command takes the exact sha256 the review printed, all 64 characters of it: a shorter value is refused with Accept-command hash must be a 64-character SHA-256 value from --json. If the package changed in between, the install fails with Command hash mismatch. Manifest may have changed. Re-run with --json and accept the new hash.Packages that execute native code
Most plugins run in the WebAssembly sandbox and need no extra decision. A package that executes native code cannot be sandboxed, so it is refused until you say otherwise, once, in writing:Enable, disable, remove
Plugin is not active.
Run a command or a tool directly
cortex plugin run invokes a plugin outside a session, which is how you test one:
--tool are mutually exclusive, and one of the two is required. --input takes a JSON object, requires --tool, and defaults to {}. --json prints a machine-readable report of the invocation.
What the manifest declares
Every plugin has aplugin.toml at its root. It declares the capabilities the plugin uses, the permissions it asks for, and the commands, hooks, tools and configuration it registers.
capabilities names the broad areas a plugin touches. There are ten: commands, hooks, events, tools, formatters, themes, config, filesystem, shell and network. permissions is the scoped list: read_file, write_file, execute, network, environment and config each take the paths, commands, domains, variables or keys they apply to, while clipboard and notifications take no scope and are written as bare strings in the same array. A plugin that asks for read_file with paths = ["**/*"] is asking for your whole workspace; narrow scopes are easier to accept.
[[tools]] blocks add tools the agent can call, each with a JSON input schema. [config] keys become settings a user can set, with an optional [config.<key>.validation] table. [wasm] tunes the sandbox inside the hard ceilings below, and a package that uses the executable runtime declares it in [runtime].
Hooks in the manifest
Hooks reach a session through the manifest, not through a separate file. Each[[hooks]] block has exactly four fields, and an unknown field is a manifest error rather than a warning:
When several plugins subscribe to the same event, they run in a deterministic order: lowest priority number first, and plugins that share a priority keep the order they were registered in, so the same set of plugins always produces the same sequence. Hooks lists the events you can subscribe to and what each one receives.
What a hook can and cannot do
A hook can watch, annotate, and refuse. It cannot widen what a session is allowed to do, and that is enforced in two places rather than one. Every permission question starts at ask. A hook that answers “allow” does not get its way: the dispatch fails withPlugins cannot grant execution privileges, and a third-party plugin attempting it is refused with Third-party plugins cannot auto-grant permissions (security restriction). The engine re-checks the same rule after the hooks have run, so a plugin cannot slip an approval through by answering late. A hook can deny an action, and the first non-ask decision wins, so one plugin’s refusal is enough to stop a call.
Two further edits are rejected outright. A hook cannot change which tool is about to run or drop its arguments (A hook cannot change tool identity or remove arguments), and a hook cannot promote the role of a message it touches (Hooks cannot elevate message roles). The practical summary: a plugin can make your session stricter, never looser. Approvals stay with you and with your permission policy.
The sandbox ceilings
These limits are the outer bounds, whatever a manifest asks for:
A plugin that loops forever runs out of fuel or hits the timeout and is terminated. Your session keeps going.
Build your own
1
Scaffold the project
--advanced for a template that includes interface hooks.2
Validate the manifest
hook_type or an unknown field.3
Build and iterate
dev --watch rebuilds on change, with a debounce so a burst of saves is one rebuild. It rebuilds the artifact; it does not hot-reload a running session, so reload the plugin to pick up a new build.4
Install it locally
cortex plugin publish prepares the package and reports what it would ship. Dry-run is the default, as its own help says: Dry-run mode (default, no actual publishing).