Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Plugin model

TL;DR

  • A plugin is a directory. Every directory is a valid plugin — no manifest required.
  • An optional Symposium.toml provides explicit configuration. If absent, an empty one is synthesized.
  • Defaults apply to every plugin: skills/ and .agents/skills/ are discovered as skill directories.
  • Plugins can declare chained plugins (additional plugins to load when activated).
  • Predicates gate activation, not installation.

Motivation

The old plugin model was built around explicit manifests in “plugin source” directories. This made it hard for crate authors to ship skills without learning a new configuration system. The new model inverts the default: everything is a plugin, configuration is optional, and conventions do the heavy lifting.

Change in a nutshell

A plugin directory with nothing but a skills/ subdirectory:

my-plugin/
└── skills/
    └── usage-guide/
        └── SKILL.md

This is a valid, complete plugin. No Symposium.toml needed. Symposium synthesizes an empty manifest and applies defaults, which discovers the skill.

Adding a Symposium.toml lets you control behavior — add predicates, declare hooks, reference binaries, suppress defaults, or chain other plugins:

# Symposium.toml
depends-on = ["tokio>=1"]

[[hooks]]
event = "PreToolUse"
command = "my-linter"

[[plugins]]
source.cargo = "tokio-extras"

Detailed plans

What is a plugin?

A plugin is a directory. That’s it. The directory may contain:

  • Symposium.toml — optional manifest
  • skills/ — conventional skill directory (exposed to workspace and dependency consumers)
  • .agents/skills/ — conventional skill directory (workspace-only)
  • Any other files (scripts, assets, etc. referenced by hooks or MCP servers)

Synthesized manifest

When a directory has no Symposium.toml, Symposium behaves as if an empty one exists. This empty manifest still triggers default behavior (see below).

An empty manifest is enough because where the directory was found supplies the plugin’s activation root: a workspace member is rooted in workspace membership, a crate in the reference that reached it. A registry entry is not found anywhere in particular, being offered to every workspace equally, so it has to name its own root, which for a curated plugin means naming the dependencies it advises on (depends-on = ["*"] claims every workspace as one). An entry that names none is left with use as its only root: it loads and is reported, but contributes nothing until a [plugins] use entry names it.

Symposium.toml structure

# Predicates gating activation
predicates = ["workspace-member()", "path_exists(build.rs)"]

# Shorthand for the common dependency case
depends-on = ["tokio>=1", "serde>=1"]

# Suppress defaults
[defaults]
skills = false

# Skills (beyond those discovered by convention)
[[skills]]
source.path = "extra-skills/advanced"
predicates = ["env(ADVANCED_MODE=1)"]

# Hooks
[[hooks]]
event = "PreToolUse"
command = "my-linter"
args = ["--strict"]

[[hooks]]
event = "SessionStart"
command = "my-greeter"

# MCP servers
[[mcp]]
name = "my-server"
command = "my-mcp-binary"
args = ["serve"]

# Chained plugins — loaded when this plugin activates
[[plugins]]
source.cargo = "tokio-extras>=1"

# Installable content (binaries referenced by hooks/MCP servers)
[[installable]]
name = "my-linter"
source.cargo = { my-linter-crate = "1.0" }

Agentic extensions

Symposium.toml files contain the following kinds of content:

  • [[plugins]] defines a set of additional chained plugins. If a plugin X defines a chained plugin Y, then whenever X is loaded, Y will be loaded.
  • [[skills]] identifies directories where we should search for skills. Any skills found there will be installed into the user’s workspace in the appropriate place(s) for the agent(s) they’ve selected.
  • [[mcp]] identifies MCP servers.
  • [[hooks]] identifies hooks. Symposium allows you to define vendor-neutral hooks that work for any vendor or vendor-specific hooks that target a particular agent (e.g., Claude Code or Codex).
  • [[installable]] identifies installable content, which can be referenced by MCP servers or hooks (which need an executable). An easy option is to package your content as a cargo package that will be cargo-install’d and managed by Symposium, but there are other options.

Default content

Plugins have default content added automatically unless disabled via [defaults]. Currently we have one default, defaults.skills = (true|false). Assuming the default is not set to false, the following is added to the plugin:

[[skills]]
source.path = "skills"

[[skills]]
predicates = ["workspace()"]
source.path = ".agents/skills"

These defaults establish the skills conventions:

  • skills/ is exposed to anyone who depends on the crate (no predicate gate).
  • .agents/skills/ is only exposed when working directly in the workspace (gated by workspace()).

Predicates

The plugin itself and each of its subsections can be gated with a predicates = [...] field. When a plugin is installed, the content is only activated if the predicate matches.

The functions are listed in the parent RFD’s predicates section and specified in full in the predicates reference: depends-on(<atom>), workspace-member(), env(...), path_exists(...), shell(...), and the combinators not, any, all.

Explicit enablement is deliberately not a predicate. Enablement is a separate axis deciding whether a plugin may run at all, recorded in [plugins] and consulted before predicates are evaluated.

The depends-on shorthand covers the common dependency case:

depends-on = ["tokio>=1", "serde>=1"]

This is equivalent to predicates = ["any(depends-on(tokio>=1), depends-on(serde>=1))"].

Predicates can appear at any level (plugin, skill, hook, MCP server). A predicate on a plugin gates all its direct contents. Chained plugins have their own predicates and are evaluated independently.

Chained plugins

A plugin can declare additional plugins to be loaded when it activates:

[[plugins]]
source.cargo = "serde-extras>=1"

A chained edge names a package, which its package manager resolves. source.path and source.git are rejected with a hint: a path is not a package, and local content is reachable as a [[skills]] source.path group or as a workspace plugin.

Chaining is an activation-time relationship: when this plugin becomes active, also load these. Chained plugins:

  • Are fetched and cached transitively (installing A also fetches A’s chained plugins)
  • Have their own predicates (they may not activate even if the parent does)
  • Are independent after loading

Use chaining when a library crate wants agent support but ships it in a separate package for release-cycle independence.

Installed vs. active

StateMeaningWhere
InstalledContent is in cache, ready to activate~/.symposium/cache/
ActivePredicates pass, content wired into agent dirs.claude/skills/, etc.
InactiveInstalled but predicates don’t passCache only

A plugin transitions between active and inactive as workspace state changes (e.g., adding a dependency). No re-fetch needed.

Frequently asked questions

Why is every directory a plugin?

It makes the cargo PM simple: every crate is a plugin, no detection heuristic needed. Most crates won’t have any plugin content (no skills/, no Symposium.toml), so they result in empty plugins that are effectively no-ops.

What happened to “plugin sources”?

Gone. In the old model, [[plugin-source]] pointed at directories that contained plugins. Now there’s just plugins — and plugins can chain other plugins.

Can a plugin contain sub-directories that are also plugins?

Only via explicit [[plugins]] with source.path. We don’t recursively scan for nested Symposium.toml files.

What if skills/ exists but I don’t want it discovered?

[defaults]
skills = false

Implementation plan and status

All five steps landed. One follow-on remains: a crate-embedded plugin can define a custom predicate, but the definition isn’t registered, so it can’t be evaluated. See the parent RFD’s future work.

Step 1: Plugin struct and manifest parsing

Define the Plugin struct, parse Symposium.toml, synthesize empty manifests for directories without one.

  • PR: plugin struct + TOML parsing

Step 2: Default application

Implement skill discovery from skills/ and .agents/skills/. Suppression via [defaults].

  • PR: plugin defaults

Step 3: Predicates on plugins

Evaluate predicates at the plugin level and per-construct level. Gate activation. Implement the [depends-on] shorthand.

  • PR: predicate evaluation

Step 4: Chained plugins

Parse [[plugins]] entries, resolve via PMs, fetch transitively, evaluate independently.

  • PR: chained plugin loading

Step 5: Integration with sync

Wire the new plugin model into the sync pipeline: iterate installed plugins, evaluate predicates, sync active content to agent directories.

  • PR: sync integration