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 definitions

A symposium plugin collects together all the extensions offered for a particular crate. Plugins are directories containing a SYMPOSIUM.toml manifest file that references skills, hooks, MCP servers, and other resources relevant to your crate. These extensions can be packaged within the plugin directory or the plugin can contain pointers to external repositories.

Plugins enable capabilities beyond standalone skills — they’re needed when you want to add hooks or MCP servers. For simple skill publishing, see Authoring a plugin instead.

Example: a plugin definition with inline skills

You could define a plugin definition with inline skills by having a directory struct like this:

myplugin/
  SYMPOSIUM.toml
  skills/
    skill-a/
      SKILL.md
    skill-b/
      SKILL.md

where myplugin/SYMPOSIUM.toml is as follows:

name = "example"
depends-on = ["*"]

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

Top-level fields

FieldTypeRequiredDescription
namestringyesPlugin name. Used in logs and CLI output.
depends-onstring or arraynoWhich crates this plugin applies to. Use ["*"] for all crates. See Plugin-level filtering.
predicatesarray of stringsnoPredicates (depends-on, shell, path_exists, env, workspace-member, not, any, all) that must all hold for the plugin to apply. See Predicates.
installationsarray of tablesnoNamed installation declarations ([[installations]]). Hooks reference these by name. See Installations.
skillsarray of tablesnoSkill groups ([[skills]]).
hooksarray of tablesnoHooks ([[hooks]]).
predicatearray of tablesnoCustom predicate definitions ([[predicate]]). See Custom predicates.
mcp_serversarray of tablesnoMCP server registrations ([[mcp_servers]]).

Note: A plugin that references no dependency anywhere — at the plugin level, in [[skills]] groups, [[mcp_servers]] entries, or [[plugins]] entries — via a depends-on list or a depends-on(...) predicate is dormant: it loads, but it never activates until the user enables it by name in the [plugins] use config. Use depends-on = ["*"] for a plugin that should always be active. (Workspace plugins are unaffected: membership in the active workspace is itself the gate.)

Plugin-level filtering

The top-level depends-on field controls when the entire plugin is active:

name = "my-plugin"
depends-on = ["serde", "tokio"]  # Only active in projects using serde OR tokio

# OR use wildcard to always apply
depends-on = ["*"]

Plugin-level filtering is combined with skill group filtering using AND logic — both must match for skills to be available.

[[skills]] groups

Each [[skills]] entry declares a group of skills.

FieldTypeDescription
depends-onstring or arrayWhich crates this group advises on. Accepts a single string ("serde") or array (["serde", "tokio>=1.0"]). See Crate predicates for syntax.
predicatesarray of stringsPredicates (depends-on, shell, path_exists, env, workspace-member, not, any, all) that must all hold for the group to install. See Predicates.
source.pathstringLocal directory containing skill subdirectories. Resolved relative to the manifest file.
source.gitstringGitHub URL pointing to a directory in a repository (e.g., https://github.com/org/repo/tree/main/skills). Symposium downloads the tarball, extracts the subdirectory, and caches it.

A skill group must have exactly one of source.path or source.git. A crate is no longer a skill-group source; to load a crate’s own skills, name it in a chained plugin.

Chained plugins

A [[plugins]] entry names another plugin that loads whenever this plugin is active — the “a package is a plugin” edge. Today the referenced plugin is a crate, which always loads as a first-class plugin built from its manifest sources (see Crate-embedded manifest below). This is the recommended path for crate authors to ship skills alongside their crate — see Supporting your crate.

FieldTypeDescription
source.cargostring or tableThe crate carrying the plugin. A dependency-atom string ("serde", "serde>=1") or a { name = "...", version = "..." } table.
depends-onstring or arrayGate for this edge — the referenced plugin loads only when these hold (in addition to the owning plugin’s own gate).
predicatesarray of stringsAdditional gate for this edge. See Predicates.
name = "serde-plugin"

# When serde is a dependency, load serde's plugin (its skills).
[[plugins]]
depends-on = ["serde"]
source.cargo = "serde"

The edge’s depends-on decides whether to load the referenced crate; the crate name in source.cargo decides which crate. (This replaces the retired source = "crate" skill-group form, where one depends-on predicate did both jobs.) List several [[plugins]] entries to load several crates.

Only source.cargo is supported today; source.git / source.path chained plugins are reserved and rejected with a clear error.

Crate-embedded manifest

A referenced crate describes its plugin with the ordinary plugin-manifest schema, from two interchangeable sources: a SYMPOSIUM.toml at its source root, and/or a [package.metadata.symposium] table in its Cargo.toml. Both are honored the same as a registry manifest — named [[skills]] groups, per-group predicates, source.path / source.git sources, and further [[plugins]] chained references. The crate’s effective manifest is the two sources merged over the crate defaults (merge order defaults → [package.metadata.symposium]SYMPOSIUM.toml): list entries from both are kept; where the two set the same scalar, the file wins. Each source is parsed leniently — a malformed layer is logged and dropped, and the crate still resolves through the remaining layers (at minimum the default skills/ group).

Because the chained reference is already the gate, a crate manifest may omit name (defaults to the crate) and a top-level depends-on; the default skills/ group is appended unless [defaults] skills = false. A crate with no manifest sources at all still resolves as a plugin whose only content is that default skills/ group.

The opt-out belongs to the referenced crate, not the referrer. The edge decides only whether to load the crate (via its depends-on / predicates); it cannot toggle the crate’s defaults. So for an active edge to crate foo:

  • foo ships nothing → its skills/ directory loads.
  • foo declares [[skills]] source.path = "guidance" → both guidance/ and skills/ load (combined with defaults).
  • foo declares [defaults] skills = false plus a custom group → only the custom group loads.
  • foo declares [defaults] skills = false and nothing else → nothing loads.
  • foo carries its own [[plugins]] source.cargo = "bar"bar resolves the same way, recursively.

Hooks, MCP servers, and subcommands declared in a crate manifest are parsed and validated but not yet dispatched — its skills and further chained references load today.

Delegating to another crate

A crate can delegate to another crate with a [[plugins]] chained reference of its own — the replacement for the retired crate = {..} metadata redirect:

# In the referenced crate's Cargo.toml (or its SYMPOSIUM.toml)
[[package.metadata.symposium.plugins]]
source.cargo = "companion-crate"

Chained references are expanded recursively, with cycle detection (hyphen/underscore-insensitive) and a depth limit of 10. When multiple crates delegate to the same target, its skills install once (dedup by crate name + version). See Supporting your crate for the full crate-author walkthrough.

Installations

An installation describes how to obtain (and optionally pre-configure) something a hook will run. Hooks then reference an installation as their command — either by name (command = "rtk") or inline at the use site (command = { script = "scripts/x.sh" }).

A [[installations]] entry has a name plus any of:

FieldTypeDescription
sourcestringOptional. How to acquire bits onto disk. One of cargo, github, binary (see below). When omitted, no acquisition step runs.
install_commandsarray of stringsOptional. Shell commands run (in order) after the source step. Useful for post-install setup such as aliasing, or when only have manual commands. Each command must exit zero.
requirementsarrayOptional. Other installations to acquire whenever this one is referenced. Strings name [[installations]] entries; tables are inline declarations.
executablestringOptional. Path to a binary to run. For cargo, the binary name (looked up in the install’s bin/ dir). For github / binary, a path inside the acquired tree. With no source, a path on disk.
scriptstringOptional. Same resolution rules as executable, but invoked as sh <path> <args>.
argsarray of stringsOptional. Default invocation arguments.

executable and script are mutually exclusive — pick one. The hook layer applies the same rule, and at most one of executable / script may be set across the hook AND the installation it references. An installation may have neither (then it’s pure setup — useful as a requirements entry). For a hook to run, the chosen layer pair must end up with exactly one runnable.

Inline installations (used as command or as a requirement entry) accept the same fields, including requirements.

Installation sources

cargo

[[installations]]
name = "rg"
source = "cargo"
crate = "ripgrep"
version = "13.0.0"     # optional; defaults to latest stable
executable = "rg"      # the binary to run; if omitted and the crate has a single binary, that one is used
args = ["--version"]   # optional default args

Symposium attempts cargo binstall first, falls back to cargo install, and caches the result under ~/.symposium/cache/binaries/<crate>/<version>/bin/ (passing --root so the install doesn’t pollute ~/.cargo/bin). The chosen executable resolves to <cache>/bin/<executable>. Hooks that depend on this installation get <cache>/bin/ prepended to $PATH, so scripts can invoke the binary by name.

To install from a git repo instead of crates.io, set git:

[[installations]]
name = "tool"
source = "cargo"
crate = "tool"
git = "https://github.com/example/tool"
executable = "tool"   # required for git sources (crates.io is not consulted)

To install into the user’s global cargo location (~/.cargo/bin) instead of a symposium-managed cache, set global = true. No --root is passed; $PATH is not augmented (the binary is expected to already be on $PATH). This can be useful if you are using scripts which require globally-installed programs, or if you want to use tools separately in a CLI.

[[installations]]
name = "rg"
source = "cargo"
crate = "ripgrep"
executable = "rg"
global = true

github

[[installations]]
name = "rtk-hooks"
source = "github"
url = "https://github.com/example/rtk-hooks"
script = "hooks/claude/rtk-rewrite.sh"   # optional; see below
args = ["--format"]

Acquires the repo (or a subtree, if url points at …/tree/<ref>/<path>) into a local cache. The chosen executable / script resolves to a file inside the cached tree.

executable/script may be set on the installation or on the hook (but not both, in any combination). Setting it on the installation pins this entry to a specific file; omitting it lets multiple hooks each pick a different file.

no source

Omit source entirely when you just need to point at a path on disk (or rely on install_commands to put one there):

[[installations]]
name = "tool"
executable = "/usr/local/bin/tool"

Or “shell-only” installations — useful as side-effect requirements:

[[installations]]
name = "setup"
install_commands = [
    "ln -sf $HOME/.cache/foo $HOME/.local/bin/foo",
]

[[hooks]]

Each [[hooks]] entry declares a hook that responds to agent events. For the JSON schemas that symposium-format hooks receive and produce, see Symposium hook events.

FieldTypeDescription
namestringDescriptive name for the hook (used in logs).
eventstringEvent type to match (e.g., PreToolUse).
matcherstring (optional)Which tool invocations to match (e.g., Bash). Omit to match all.
commandstring or tableWhat to run. A string names a [[installations]] entry; a table is an inline installation (promoted to a synthetic entry named after the hook).
executablestring (optional)Path to a binary inside (or relative to) the installation. At most one of executable/script set across hook + installation.
scriptstring (optional)Path to a shell script to run via sh. Same exclusivity rule as executable.
argsarray (optional)Invocation arguments. Forbidden when the installation also declares args.
requirementsarray (optional)Installations to acquire before running. Same shape as command (string name or inline declaration).
agentstring (optional)Restrict the hook to a specific agent (claude, copilot, gemini, kiro, …).
formatstringWire format the handler expects on stdin. symposium (default): symposium converts the agent’s event to its canonical format before delivering. Any agent name (claude, codex, copilot, gemini, kiro): the handler receives that agent’s native wire format. Symposium always intermediates — it never registers plugin hooks directly into agent configs. See Hooks.
predicatesarray (optional)Predicates (depends-on, shell, path_exists, env, workspace-member, not, any, all) that must all hold for the hook to dispatch. Evaluated per-dispatch. See Predicates.

Examples

Run a cargo-installed binary as the hook:

[[installations]]
name = "rg"
source = "cargo"
crate = "ripgrep"
executable = "rg"

[[hooks]]
name = "rg-version"
event = "PreToolUse"
command = "rg"
args = ["--version"]

Install rtk as a side requirement and run a hook script from a separate github source:

[[installations]]
name = "rtk"
source = "cargo"
crate = "rtk"

[[installations]]
name = "rtk-hooks"
source = "github"
url = "https://github.com/example/rtk-hooks"

[[hooks]]
name = "rewrite"
event = "PreToolUse"
requirements = ["rtk"]
command = "rtk-hooks"
script = "hooks/claude/rtk-rewrite.sh"
args = ["--format"]

Inline a one-off cargo install directly:

[[hooks]]
name = "rg-test"
event = "PreToolUse"
command = { source = "cargo", crate = "ripgrep", executable = "rg" }
args = ["--version"]

Run a script file on disk (no source):

[[hooks]]
name = "check"
event = "PreToolUse"
command = { script = "scripts/check.sh", args = ["--strict"] }

A cargo install with a post-install step (e.g. to symlink a wrapper script):

[[installations]]
name = "rtk"
source = "cargo"
crate = "rtk"
install_commands = [
    "ln -sf $HOME/.symposium/cache/binaries/rtk/*/bin/rtk $HOME/.local/bin/rtk",
]

[[hooks]]
name = "rtk-rewrite"
event = "PreToolUse"
command = "rtk"
args = ["rewrite"]

Agent-specific hooks

An agent-specific hook expects a particular agent’s native wire format on stdin. Use this when you need full access to an agent’s event schema. Symposium still intermediates — it delivers the input in the declared format (passing through unmodified when the current agent matches, or converting when it doesn’t).

A plugin with a Claude-specific hook and a symposium fallback:

[[hooks]]
name = "check-claude"
event = "PreToolUse"
format = "claude"
command = "my-hook-binary"

[[hooks]]
name = "check-portable"
event = "PreToolUse"
format = "symposium"
command = "my-hook-binary"
args = ["--symposium"]

On Claude, check-claude fires (receives Claude’s native JSON). On other agents, check-portable fires (receives symposium canonical JSON). Only one hook per plugin fires for a given event — symposium picks the best match by format priority.

Requirements

requirements ensures other installations are acquired before the hook runs. Useful when the hook’s command relies on something else being on disk (or eventually on $PATH).

[[hooks]]
name = "uses-rtk-via-script"
event = "PreToolUse"
requirements = ["rtk", { source = "cargo", crate = "ripgrep" }]
command = { script = "scripts/uses-rtk.sh" }

Requirements may also be declared on an [[installations]] entry. Whenever that installation is referenced — as a hook’s command or in another requirements list — its declared requirements are appended (one level, prerequisites first):

[[installations]]
name = "rtk"
source = "cargo"
crate = "rtk"

[[installations]]
name = "rtk-hooks"
source = "github"
url = "https://github.com/example/rtk-hooks"
requirements = ["rtk"]   # rtk gets installed whenever rtk-hooks is used

[[hooks]]
name = "rewrite"
event = "PreToolUse"
command = "rtk-hooks"
script = "hooks/claude/rtk-rewrite.sh"

Requirement installation is best-effort: failures are logged and dispatch continues.

Hook environment

Hooks are spawned with the following extras on top of the parent environment:

VariableWhen setValue
$SYMPOSIUM_DIR_<name>Installation has a symposium-managed cache (scoped cargo, github)Absolute path to the cache / clone directory.
$SYMPOSIUM_<name>Installation resolves to a runnable with an absolute pathAbsolute path to the resolved executable / script.
$PATHOne or more dependencies contribute a runnable with an absolute pathEach runnable’s parent dir is prepended, with the hook’s command first.

<name> is the installation name with non-alphanumeric characters replaced by _ (e.g. rtk-hooksSYMPOSIUM_DIR_rtk_hooks). Both the hook’s command installation and every requirement (recursively, one level via installation-level requirements) contribute.

Global cargo installs (global = true) don’t set $SYMPOSIUM_DIR_<name> or augment $PATH — the binary is expected to already be on the user’s $PATH via ~/.cargo/bin.

install_commands runs before env vars are set. The $SYMPOSIUM_* vars and the augmented $PATH are only available to the hook’s spawned process. install_commands runs earlier, inside the symposium dispatch process, so it cannot reference its own (or any other) installation’s env vars. Use absolute paths in install_commands instead.

Supported hook events

Hook eventDescriptionCLI usage
PreToolUseBefore a tool (e.g., Bash) is invoked by the agent.pre-tool-use
PostToolUseAfter a tool completes.post-tool-use
UserPromptSubmitWhen the user submits a prompt.user-prompt-submit
SessionStartWhen an agent session starts.session-start

Agent → hook name mapping

Tool / EventClaude (claude)Copilot (copilot)Gemini (gemini)
PreToolUsePreToolUsePreToolUseBeforeTool

Hook semantics

  • Exit codes:

    • 0 — success: the hook’s stdout is parsed as JSON and merged into the overall hook result.
    • 2 (or no reported exit code) — treated as a failure: dispatch stops immediately and the hook’s stderr is returned to the caller.
    • any other non-zero code — treated as success for dispatching purposes; stdout is still parsed and merged when possible.
  • Stdout handling: Hooks should write a JSON object to stdout to contribute structured data back to the caller. Valid JSON objects are merged together across successful hooks; keys from later hooks overwrite earlier keys.

  • Stderr handling: If a hook exits with code 2 (or no exit code), dispatch returns immediately with the hook’s stderr as the error message. Otherwise stderr is captured but not returned on success.

Testing hooks

Use the CLI to test a hook with sample input:

echo '{"tool": "Bash", "input": "cargo test"}' | cargo agents hook claude pre-tool-use

You can also use copilot, gemini, codex, or kiro as the agent name.

[[predicate]]

Each [[predicate]] entry defines a custom predicate function that can be used in predicates expressions anywhere a predicate is accepted. Custom predicates extend the built-in predicate language with plugin-specific checks.

FieldTypeDescription
namestringThe predicate name. Must be a valid identifier ([a-zA-Z][a-zA-Z0-9_]*) and must not collide with builtins (depends-on, crate, shell, path_exists, env, workspace-member, not, any, all).
commandstring or tableThe installation to run. Same shape as hook command (a string naming a [[installations]] entry or an inline table).
argsarray of stringsOptional. Static arguments passed to the command before the dynamic argument.

How custom predicates work

Custom predicates are registered globally — a predicate defined in one plugin can be used by any other plugin’s predicates expressions. Registration is unconditional: even if the defining plugin’s own crate predicates don’t match the current workspace, its [[predicate]] entries are still available.

When a predicate expression uses a function name that isn’t a builtin, Symposium looks it up in the custom predicate registry. If found, it spawns the declared command with the static args followed by the raw argument text from the expression.

[[installations]]
name = "cargo-bp-install"
source = "cargo"
crate = "cargo-bp"
executable = "cargo-bp"

[[predicate]]
name = "battery_pack"
command = "cargo-bp-install"
args = ["bp", "status", "--check"]

Usage in a predicates expression:

predicates = ["battery_pack(cli>=0.3)"]

This evaluates as:

cargo-bp bp status --check cli>=0.3

Exit 0 means the predicate passes; non-zero means it fails.

The argument is trimmed of leading/trailing whitespace before being passed. An empty argument — battery_pack() or battery_pack( ) — does not append anything to the command (only the static args are passed).

A custom predicate is a boolean gate only: it passes iff the command exits 0. Its stdout is ignored (the former selectedCrates witness output is retired along with source = "crate").

Collisions

If two plugins define the same predicate name, both definitions are skipped and a warning is emitted. Skills referencing the collided name evaluate as false.

Caching

Results are cached by (predicate_name, raw_arg) for the duration of a single sync run. The same predicate called with the same argument is only spawned once.

[[mcp_servers]]

Each [[mcp_servers]] entry declares an MCP server that Symposium registers into the agent’s configuration during sync --agent.

There are multiple MCP transports:

Stdio

[[mcp_servers]]
name = "my-server"
command = "/usr/local/bin/my-server"
args = ["--stdio"]
env = []
FieldTypeDescription
namestringServer name as it appears in the agent’s MCP config.
depends-onstring or arrayWhich crates this server applies to. Optional if plugin has top-level depends-on.
predicatesarray of stringsPredicates (depends-on, shell, path_exists, env, workspace-member, not, any, all) that must all hold for the server to register. See Predicates.
commandstringPath to the server binary.
argsarray of stringsArguments passed to the binary.
envarray of objectsEnvironment variables to set when launching the server.

Stdio entries do not need a type field.

HTTP

[[mcp_servers]]
type = "http"
name = "my-server"
url = "http://localhost:8080/mcp"
headers = []
FieldTypeDescription
typestringMust be "http".
namestringServer name as it appears in the agent’s MCP config.
depends-onstring or arrayWhich crates this server applies to. Optional if plugin has top-level depends-on.
urlstringHTTP endpoint URL.
headersarray of objectsHTTP headers to set when making requests.

SSE

[[mcp_servers]]
type = "sse"
name = "my-server"
url = "http://localhost:8080/sse"
headers = []
FieldTypeDescription
typestringMust be "sse".
namestringServer name as it appears in the agent’s MCP config.
depends-onstring or arrayWhich crates this server applies to. Optional if plugin has top-level depends-on.
urlstringSSE endpoint URL.
headersarray of objectsHTTP headers to set when making requests.

How registration works

During cargo agents sync --agent, each MCP server entry is written into the agent’s config file in the format that agent expects. Registration is idempotent — existing entries with correct values are left untouched, stale entries are updated in place.

When a user runs cargo agents sync (or the hook triggers it automatically), Symposium:

  1. Collects [[mcp_servers]] entries from all enabled plugins.
  2. Writes each server into the agent’s MCP configuration file.

All supported agents have MCP server configuration. Symposium handles the format differences — you declare the server once and it works across agents.

AgentConfig locationKey
Claude Code.claude/settings.jsonmcpServers.<name>
GitHub Copilot.vscode/mcp.json<name> (top-level)
Gemini CLI.gemini/settings.jsonmcpServers.<name>
Codex CLI.codex/config.toml[mcp_servers.<name>]
Kiro.kiro/settings/mcp.jsonmcpServers.<name>
OpenCodeopencode.jsonmcp.<name>
Goose~/.config/goose/config.yamlextensions.<name>

Example: full manifest

name = "widgetlib"
depends-on = ["widgetlib"]

# Skills shipped inside the widgetlib crate source (in skills/)
[[plugins]]
source.cargo = "widgetlib"

# Additional skills hosted in a git repo
[[skills]]
depends-on = ["widgetlib=1.0"]
source.git = "https://github.com/org/widgetlib/tree/main/symposium/serde-skills"

[[hooks]]
name = "check-widget-usage"
event = "PreToolUse"
matcher = "Bash"
command = { source = "local", command = "./scripts/check-widget.sh" }

[[mcp_servers]]
name = "widgetlib-mcp"
command = "/usr/local/bin/widgetlib-mcp"
args = ["--stdio"]
env = []

Validation

cargo agents plugin validate path/to/symposium.toml

This parses the manifest and reports any errors. Crate name checking against crates.io is on by default; use --no-check-crates to skip it.