Skip to main content
A plugin is a package that adds something to the Cortex CLI: extra slash commands, extra tools the agent can call, configuration of its own, and hooks that observe or block what a session does. Plugins are compiled to WebAssembly and run in a sandbox with a memory cap, an instruction budget and a wall-clock timeout, so a plugin that misbehaves is stopped instead of taking your session with it. This page covers the plugin lifecycle end to end: where plugins live, how to review a package before you install it, how to enable, run, update and remove one, what the manifest declares, and the security model that decides what a plugin is allowed to do. The catalogue of hook events lives on Hooks.
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.
The failure is a clean one: nothing is written to the plugin root, an already-installed copy is left exactly as it was, and no trust decision is renewed. There is no flag that accepts whatever hash happens to be current, so a changed manifest always comes back to you. Accepted hashes are appended to the audit journal with the plugin, the hashes and the scope names, never prompt text or file contents.
Run the review step in CI too. A pinned hash in a repository script means a plugin that quietly grows a new command fails the build instead of shipping.
Organisations can require the pinned form and refuse unreviewed installs. See Permission policy.

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:
--trust-code trusts that exact package to execute native code. It is not a sandbox. A trusted package can read and write files, reach the network and start subprocesses with your user’s rights, and cortex plugin trust --yes is you acknowledging exactly that. There is no signing or content verification of plugin packages today, so trust the source or do not trust the package.

Enable, disable, remove

Disabling is the safe way to isolate a plugin while you debug something: the package stays installed and its hooks stop running. A plugin that is unloaded or disabled is skipped silently when hooks dispatch. A plugin in any other non-active state is an error rather than a silent skip, and the session reports 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:
A command name and --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 a plugin.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 with Plugins 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

Add --advanced for a template that includes interface hooks.
2

Validate the manifest

Validation checks the manifest and the project structure, and is the fastest way to catch a mistyped 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

The review and hash step is the same for a local path as for a registry package.
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).