Cortex Skills and Plugins¶
Cortex uses skills for reusable agent instructions and plugins for installable capability bundles. A plugin can contribute skills alone or, when it follows the portable Agent Plugins format, skills together with Model Context Protocol (MCP) servers. Plugins are assigned to execution environments, so each spawned backend receives only the surface its environment selects.
Skills¶
A skill is a directory whose entry point is SKILL.md. Its YAML frontmatter describes when the skill applies, while the Markdown body provides the instructions expanded by the Skill tool. A minimal portable skill looks like this:
---
name: summarize
description: Use when a long artifact needs a concise technical summary
---
# Summarize
Read the complete artifact, preserve evidence links, and report the main result.
When the frontmatter has a name, it must match the directory name. Portable skill names use lowercase letters, digits, and single hyphens — the Settings editor enforces that form when it creates or moves a skill — and Cortex discovers only immediate children of the plugin's skills/ directory. A skill can keep supporting files such as scripts and references below its own directory. Cortex validates the frontmatter of portable skills only; a legacy package's skills are handed to the backend as they are.
The loader is deliberately lenient. A skill is dropped only when it cannot be used at all: no parseable frontmatter, a name that contradicts its directory, or an empty description. Everything else — an absent name, a non-canonical one, an over-long description, a malformed optional field, an allowed-tools list written as YAML, or a key outside the spec — is loaded and reported as an advisory issue on the plugin's detail page. Losing a working skill to a stray frontmatter key is a worse and quieter failure than tolerating the key.
Supported plugin formats¶
Cortex inventories its built-in plugins and each immediate child directory of $CORTEX_HOME/plugins/. It recognizes the portable Agent Plugins 1.0.0 Working Draft and the existing Claude-compatible legacy layout.
| Format | Manifest | Skills | Cortex portable MCP |
|---|---|---|---|
| Agent Plugins 1.0 | plugin.json at the plugin root |
Immediate skills/<name>/SKILL.md children |
Root mcp.json |
| Legacy | .claude-plugin/plugin.json |
Immediate skills/<name>/SKILL.md children |
Not cataloged or mapped cross-backend |
A root plugin.json takes precedence. If it exists but has a fatal validation error, Cortex reports the portable package as invalid rather than falling back to the legacy manifest. Unknown top-level fields and a non-object extensions value are reported and ignored as non-fatal exceptions. The legacy manifest is considered only when root plugin.json is absent. The portable format and its failure boundaries follow the Agent Plugins 1.0.0 specification, while Cortex retains the legacy layout for existing installations.
Legacy directories are passed through to the backend. Claude can therefore interpret Claude-native files such as a legacy package's root .mcp.json; those servers are not inventoried, summarized, or acknowledgment-gated by Cortex and are not mapped into PI. Use portable root mcp.json when MCP must be visible in Settings and consistent across Claude and PI.
Built-in plugins¶
Cortex ships three portable plugins in its install, under agent-server/defaults/plugins/: cortex-system (approvals, schedules, project setup, client management, learning user preferences), cortex-board (boards) and cortex-feishu (Feishu documents, loaded in Feishu sessions and, with settings.feishuSkillsInWeb, in web sessions). Cortex reads them from the install and never copies them into $CORTEX_HOME, so they always match the running version.
A directory $CORTEX_HOME/plugins/<id>/ whose name is a built-in plugin's id is that plugin's user layer, not a plugin of its own. The built-in manifest stays in force, and the layer changes the plugin piece by piece:
skills/<name>/with aSKILL.md, where the built-in plugin has a skill of that name, replaces that skill whole, support files included;skills/<name>/under any other name adds a skill to the plugin;mcp.jsonreplaces the plugin's MCP declarations whole.
Deleting a skill directory or mcp.json from the layer brings back the built-in one. The catalog reports where each skill comes from: built in, modified (the user's version in place of the built-in one), or local (the user's own).
Built-in plugins load in every environment that loads skills. An environment switches one off by naming it in disabledPlugins (see Assigning plugins). An environment with "skills": false loads only what its pluginDirs lists, and a sealed home (CORTEX_CONFIG_IMMUTABLE=1, as in a benchmark trial) has no built-in plugins.
Portable package layout¶
A portable package has this shape:
my-plugin/
├── plugin.json
├── skills/
│ └── summarize/
│ ├── SKILL.md
│ └── references/
└── mcp.json
The minimal manifest selects the exact supported schema:
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "my-plugin",
"version": "1.0.0",
"description": "Summarization skills and tools"
}
Cortex validates the manifest with a locally vendored schema and does not fetch schemas while loading a plugin. The package directory name is the Cortex assignment identity; the manifest name supplies the portable runtime namespace. Package files, manifests, skill entry points, plugin-relative commands, and plugin-root-based working directories must remain inside the resolved plugin root. A ${PLUGIN_DATA} working directory is contained under that plugin's data root instead. A symlink or equivalent path escape is rejected at the narrowest component boundary.
An invalid portable manifest makes the plugin unassignable. An invalid skill is skipped while valid sibling skills remain available. A top-level mcp.json error disables MCP for that plugin without disabling valid skills, and an invalid MCP server entry is skipped without removing valid sibling servers. The Plugins page shows these issues instead of silently hiding them.
Portable MCP servers¶
A portable mcp.json declares stdio, Streamable HTTP, or legacy HTTP+SSE servers. The schema version must match plugin.json.
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
"mcpServers": {
"local-tools": {
"type": "stdio",
"command": "./bin/server",
"args": ["--data", "${PLUGIN_DATA}"],
"env": {
"CONFIG": "${PLUGIN_ROOT}/config.json"
},
"cwd": "${PLUGIN_ROOT}"
},
"remote-tools": {
"type": "streamable-http",
"url": "https://tools.example.com/mcp",
"headers": {
"X-Tenant": "public"
}
}
}
}
A stdio command is one executable token, not a shell command. It can be a bare executable name or a contained ./ path. Cortex expands only ${PLUGIN_ROOT} and ${PLUGIN_DATA} in args, environment values, and cwd; it supplies both variables itself and reserves their names. The dedicated persistent data directory is $CORTEX_HOME/data/plugin-data/<plugin-id>/ and is created only when a selected stdio server is materialized.
Remote servers require HTTPS except for loopback HTTP. URLs containing credentials or fragments are rejected, and configured headers are never treated as a portable secret mechanism. Both backends reject every remote MCP redirect: PI applies the shared redirect-rejecting fetch directly, while Claude reaches each remote server through a local stdio proxy with the same policy. This prevents configured headers and request bodies from being replayed to a redirect target (agent-server/src/agent-adapter/mcp-remote-fetch.ts; agent-server/src/agent-adapter/claude/remote-mcp-proxy.ts). The Settings UI shows only the remote origin and header names; for stdio it shows only the executable basename, argument count, and environment key names. Environment and header values are not returned to the browser.
The detailed transport and isolation rules come from the Agent Plugins MCP runtime contract. Cortex implements all three declared transports for Claude and PI.
Inspecting a plugin¶
The desktop and browser workbench expose the catalog at Settings → Plugins. The page reads the connected Cortex server, so its entries belong to that server rather than to the local desktop installation. It lists every installed package and opens each one on three tabs: Overview, Skills and MCP. New plugin creates a local portable package — a plugin.json at version 0.1.0 and an empty skills/ directory — under a name that is not a built-in plugin's id.
Overview carries the package's identity and health: format, manifest source and metadata, on-disk location, validation issues, and any spawn-time scope restriction. It also lists which environments currently reference the plugin, so the relationship stays discoverable from the package side without the page owning it.
Managing a plugin's contents¶
The Skills tab lists what the package ships, each with its description and its path, and edits it. A skill opens into its SKILL.md, which saves against the hash it was opened with, so a file changed underneath the editor produces a conflict rather than a silent overwrite. New skills are created from a template with a name and a required description, existing ones can be renamed or moved to another plugin, and moving rewrites the frontmatter name (and only the frontmatter) so it keeps agreeing with the directory. A new or moved skill cannot take the name of a built-in skill. Deleting removes the skill directory and everything under it.
The MCP tab edits mcp.json as a form. Command, arguments, working directory and URL round-trip normally, but environment values and headers do not: this page is never told them. Each stored key shows as kept, and replacing one is an explicit action; keys you leave alone are merged back server-side from what is already on disk, and a key you remove from the form is removed from the file. Unlike the skill editor, the MCP form saves without a conflict check, so the last save wins. A legacy package cannot declare MCP servers at all, so its MCP tab offers the conversion instead — writing a root plugin.json while leaving .claude-plugin/plugin.json in place. Saving the MCP form of a built-in plugin writes mcp.json into its user layer.
Built-in plugins are marked in the list, and each skill shows whether it is built in, modified or local. A built-in skill opens and saves like any other: saving copies the skill directory to $CORTEX_HOME/plugins/<id>/skills/<name>/ with your edit, where it replaces the built-in skill, and the skill shows as modified with a Revert to built-in action that deletes your version after a confirmation. Built-in and modified skills cannot be moved, renamed or deleted; those buttons are disabled and say why. A built-in plugin cannot be deleted either: its delete dialog explains this and keeps the confirm button disabled. Skills you add to a built-in plugin are your own and can be moved and deleted. A local package can be deleted once no environment still references it.
Assigning plugins¶
Assignment is a field of an execution environment rather than a property of the plugin, so it is edited on the environment: Settings → Environments, select an environment, then the Plugins tab (which appears once a new environment has been saved). Each environment has a single plugin set, shown as one switch per catalog plugin. Built-in plugins are marked and on by default; switching one off records it in the environment's disabledPlugins, while local plugins that are switched on are listed in its pluginDirs.
Saving uses the environment file's content hash as an optimistic concurrency guard. If the environment changes on disk, Cortex refreshes the target instead of overwriting the newer file. Existing plugin paths that are outside the managed catalog remain preserved and are reported as unmanaged. An already selected invalid catalog entry can be removed, but a new invalid entry cannot be assigned.
Adding a portable plugin with at least one valid root mcp.json server opens a confirmation that explains the local-code and network trust surface. The acknowledgment is required for that portable MCP addition, but it is not a sandbox or an independent authorization boundary. Reset discards the local draft, while Save persists it through plugins.assign. The Settings modal blocks section navigation and closing while an assignment draft is dirty. If a background refetch reports a different assignment hash, the page preserves the user's draft, marks it stale, disables Save, and requires Reset instead of silently replacing the edits (web/src/features/settings/vm/plugin-assign-vm.ts).
The assignment tab and the JSON body editor write the same file, so only one of them may hold a draft at a time: the tab is frozen while the body has unsaved edits. Assignment is also not the last word on whether a plugin loads. Scoped plugins are filtered again at spawn time, so a channel-scoped package shows an inline note saying it will not load outside that scope however it is assigned.
Assignments are stored in the environment's JSON file under $CORTEX_HOME/config/environments/, and can be written there directly:
{
"name": "researcher",
"pluginDirs": [
"plugins/my-plugin"
],
"disabledPlugins": [
"cortex-feishu"
]
}
The environment's sessions load the plugins pluginDirs lists, in order, followed by every built-in plugin that disabledPlugins does not name. A built-in plugin written into pluginDirs by hand is placed earlier in that order; saving the Plugins tab rewrites the file with built-in plugins left out of pluginDirs. With "skills": false only pluginDirs counts. There is no inheritance between environments.
Relative paths resolve against $CORTEX_HOME; absolute paths remain supported as unmanaged legacy configuration. The Settings UI canonicalizes managed entries to plugins/<plugin-id> while preserving unmanaged entries.
Backend loading¶
Cortex reloads the selected catalog at spawn time. Legacy plugin directories are handed to the backend at their own path. Portable skill support trees are containment-checked, content-hashed, and copied into private snapshots. Claude receives a compatibility projection of each portable plugin through --plugin-dir; PI receives one snapshot directory per skill as additional skill paths on its in-process session, followed by any legacy directories. Neither backend receives a portable skill path that points directly into the mutable installed package (agent-server/src/domain/plugins/skill-projection.ts; agent-server/src/domain/plugins/runtime.ts). Each skill is projected from the layer it comes from: a built-in skill from the install, a modified or local one from $CORTEX_HOME/plugins/<id>/.
Portable MCP declarations pass through one backend-neutral normalization layer. Claude receives a private supplemental configuration; remote entries in that file are local stdio proxies whose separate private files hold the remote URL and headers. PI's MCP bridge receives the normalized server entries on the session request and opens each connection itself (stdio, Streamable HTTP, or SSE). Components fail at their declared dependency boundary: an invalid or unmaterializable skill is omitted without removing sibling skills or valid MCP; unavailable plugin-scoped PLUGIN_DATA omits the stdio servers that share it while preserving remote MCP and skills; and remote/stdio processes connect independently from sibling processes and bundled tools (agent-server/src/domain/plugins/runtime.ts; agent-server/src/agent-adapter/claude/mcp-config.ts; agent-server/src/agent-adapter/pi/mcp-bridge.ts). When a selected skill or MCP server cannot be materialized, a web session shows one warning notice for the run naming what is missing and why.
Projections, snapshots and the private MCP files live under $CORTEX_HOME/data/plugin-runtime/. $CORTEX_HOME itself may be reached through a symlink (a home relocated to another disk, for instance); a symlink anywhere below it is refused. Every projection and snapshot is content-addressed: one that a spawn reuses is marked live, and one that no spawn has used for a week is removed and rebuilt at the same path when it is next needed. The sweep runs during a spawn that selects plugins, at most once an hour per backend.
A capability fingerprint covers the effective plugin paths, portable skill content, projected manifest, and normalized MCP declarations. Claude pooled sessions reuse a process only when that capability matches, so assignment changes and changes to those covered inputs select a compatible process boundary. Legacy package contents and auxiliary portable files such as executable bytes are not content-hashed; changing only those files does not promise process replacement. A selected portable plugin whose plugin.json is invalid is left out of the spawn, and the run's notice names it; its issues are listed on the Plugins page. When two selected portable plugins offer the same skill name, the one that comes first in the environment's plugin order keeps it and the other plugin loads without that skill. When two selected portable plugins share a manifest name (their namespace), the first keeps it and the later plugin is skipped as a whole; the Plugins page flags a shared name on every package that carries it. These two collisions are recorded in the daemon log rather than in the run's notice. A portable and a legacy plugin that ship the same skill name both reach Claude, while PI keeps the portable one.
Portable MCP is suppressed for the explicit none composition, and subagents load no plugins at all — neither skills nor MCP. This keeps plugin capabilities out of execution modes that intentionally expose a narrower tool set.
Trust and administration¶
Everything below $CORTEX_HOME/plugins/ is administrator-installed code. Assign only packages whose source and contents you trust: a stdio MCP server can execute local code with the backend process's account, and a remote MCP server can receive tool data over the network. Portable skills are copied into private snapshots, but stdio commands, arguments, working directories, and package data remain trusted administrator-controlled runtime inputs rather than a sandbox boundary. Manifest validation, path containment, private runtime files, redirect rejection, and assignment confirmation reduce configuration mistakes and secret exposure; they do not sandbox plugin behavior.
The Plugins page manages inventory and package contents, and an environment's Plugins tab manages assignment. Its MCP inventory and acknowledgment apply only to portable root mcp.json; a legacy Claude-native .mcp.json remains outside this managed view. Installation, marketplace search, update, OAuth, credential entry, per-tool permission policy, and live MCP health checks are outside this surface. Installing a package means placing its directory under $CORTEX_HOME/plugins/ through an administrator-controlled workflow.
Skill discovery and invocation¶
The !skills command lists the skills the current conversation would load, grouped by plugin: the plugins of its environment after scope filtering. Portable plugins show the skills the catalog accepts; a directory handed to the backend as it is (a legacy package, or a path outside $CORTEX_HOME/plugins/) shows its skills/<name>/SKILL.md entries, labelled by the configured path when it is not a catalog plugin. The Skills view lists every catalog plugin's skills the same way.
The agent invokes a skill itself through its Skill tool when the skill's description matches the task. You can also invoke one explicitly. In the Web UI and desktop app, typing / in the composer opens a palette with Cortex's commands and the skills the conversation loads; choosing a skill inserts its invocation in the backend's form — /<namespace>:<skill> on Claude, where the namespace is the plugin's manifest name, and /skill:<skill> on PI — and the message goes to the agent as typed. PI resolves skills by bare name, so when two plugins ship a skill of the same name only the first is reachable there.
Each successful Skill call is recorded by the built-in session-activity-tracker hook as a skill_use event in $CORTEX_HOME/logs/session-activity/<session_id>.jsonl (see hooks.md).
Implementation references¶
| Behavior | Source |
|---|---|
| Catalog precedence, built-in and user layers, and narrow validation | agent-server/src/domain/plugins/catalog.ts |
| Built-in plugin set and the plugins an environment loads | agent-server/src/domain/plugins/builtin.ts |
| Portable MCP validation and sanitized summaries | agent-server/src/domain/plugins/mcp.ts |
| Spawn-time projection, normalization, and fingerprinting | agent-server/src/domain/plugins/runtime.ts |
| Portable skill frontmatter validation | agent-server/src/domain/plugins/skill.ts |
| Runtime snapshot sweep | agent-server/src/domain/plugins/runtime-gc.ts |
Skills a conversation loads (!skills, the slash palette) |
agent-server/src/domain/plugins/conversation-skills.ts |
| Environment plugin assignment persistence | agent-server/src/domain/ui-service/mutate/plugins.ts |
| Connected-server catalog and environment inventory | agent-server/src/domain/ui-service/query/plugins.ts |
| Settings plugin package manager | web/src/features/settings/panels/PluginsPanel.tsx |
| Skill and plugin writes to the user layer, containment-checked | agent-server/src/domain/plugins/authoring.ts |
| Authoring ops and MCP secret merging | agent-server/src/domain/ui-service/mutate/plugin-packages.ts |
| Settings skill and MCP editors | web/src/features/settings/panels/PluginSkillsTab.tsx, web/src/features/settings/panels/PluginMcpTab.tsx |
| Settings assignment and MCP acknowledgment | web/src/features/settings/panels/PluginAssignPanel.tsx |