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

Introduction

Symposium makes Rust dependencies actionable for AI agents. It discovers crate-matched plugins and wires in skills, hooks, and MCP servers so your agent can work with project-specific context.

Getting started

cargo binstall symposium # or: cargo install symposium
cargo agents init

After initialization, start your agent in a Rust project as usual.

Recent posts

What is Symposium?

Symposium is a one-stop shop to help agents write great Rust code. It connects you to best-in-class tools and workflows but it also serves up skills and extensions tailored to the crates you are using, all authored by the people who know those crates best – the crate authors.

init and go

Getting started with Symposium is easy:

#![allow(unused)]
fn main() {
cargo binstall symposium       # or `cargo install` if you prefer
cargo agents init
}

The init command will guide you through picking your personal agent. It will then configure the agent to use Symposium (e.g., by installing hooks). This will immediately give you some benefits, such as introducing Rust guidance and reducing token usage with the rtk project.

Leveraging the wisdom of crates.io

To truly get the most out of Symposium, you also want to install it into your project. When you run cargo agents init in a project directory, it will scan your dependencies and create customized skills, tools, and other improvements. These extensions are source either from our central recommendations repository. In the future, we plan to enable crate authors to embed extensions within their crates themselves and skip the central repo altogether.

Everybody picks their own agent

Work on an open-source project or a team where people use different agents? No problem. Your Symposium configuration is agent agnostic, and the cargo agents tool adapts it to the agent that each person is using. You can also specify the agent centrally if you prefer.

Staying synchronized

By default, Symposium is setup to synchronize itself. It’ll source the latest skills automatically and add them to your project. If you prefer, you can disable auto-updates and run cargo agents sync manually.

For crate authors

If you maintain a Rust crate, you can publish skills for Symposium so that every AI-assisted user of your library gets your best practices built in. See Supporting your crate for how to get started.

Blog

Welcome to the Symposium blog.

Announcing Symposium - AI The Rust Way

Authored By: Jack Huey

Are you using an AI agent to write Rust code (or curious to try it)? If so, GREAT NEWS! We’d like to share with you Symposium - a Rust-focused interoperability layer that connects AI agents to crate-authored skills, tools, and workflows.

(If you’ve read Niko’s previous blog posts talking about Symposium, this is pretty different! The tool we’re announcing today is the result of many iterations of figuring out what exactly the “thing we want” is. So please, read on!)

Also, this announcement comes with exciting news: Symposium has joined the Rust Foundation’s Rust Innovation Lab (RIL)! Be sure to check out the Foundation’s blog post.

What is Symposium?

There are really two answers to that question. The first one is that Symposium is a tool that examines what crates your project depends on and uses that to automatically install new skills, MCP servers, or other extensions. These extensions help your AI agent to write better code, avoid common footguns and pitfalls, and even leverage ecosystem tools like the Rust Token Killer (RTK) to save you tokens.

The second one is that Symposium is an organization dedicated to one goal, “AI the Rust way”, meaning reliable, efficient, and extensible. We are focused on interoperable, vendor-neutral, and community-oriented ways to make agents more reliable and efficient.

Getting started

You interact with Symposium through the cargo agents CLI command. If you want to try it, do this:

#![allow(unused)]
fn main() {
cargo binstall symposium # or `cargo install`
cargo agents init
}

The init command will prompt you to select what agents you want to use and a few other things. Based on that we install hooks that will cause Symposium to be invoked automatically. The next time you start an agent on a Rust project, Symposium will check if there are available skills or other extensions for the crates you use and set them up automatically. You shouldn’t have to do anything else.

Symposium helps your agent write better code and use fewer tokens

You may be familiar with various extensions that agents can work with, such has MCP servers, Skills, or Hooks. You may also know that different agents have different levels of support for these, and even different takes on them (Hooks, for example, are not as well-standardized as MCP servers and Skills). However, that doesn’t diminish the fact that many people have built many tools around these extension systems. We want you to easily use these ecosystem tools.

You may also have run into cases where a model is “outdated” compared to either the Rust language itself (e.g., there may be a newer language feature that is more idiomatic) or was trained on an older versions of a crate that you are using. It’s generally not hard to get models to follow newer conventions, but they need to be told to do so. We want to make that easier and more automated.

Finally, we want writing code with agents to be more efficient and reliable. Some of this comes from the above two goals, but part of it also comes from making sure that agents write code the way you would write it. For example, when you finish writing Rust code, you likely run cargo check, run your tests, or format your code - and we think that you should expect your agent to do the same. Simulatenously, efficiency also means that we want these tools to use as few tokens as possible.

Symposium Plugins

A Symposium plugin defines a set of extensions (mcp servers, skills, hooks, etc) and the conditions in which they should be used (currently: when a given version of a given crate is in the project’s dependencies). Plugins are hosted on repositories called a “plugin source”; we define a central repository with our globally recommended plugins, but you can additional plugin sources of your own if you like.

Skills

Agent Skills are a lightweight format for defining specialized knowledge or workflows for agents to use. Most agents have a pre-defined list of places that they look for skills, but don’t currently have a way to dynamically make them available.

In Symposium, we automatically discover skills from plugins applicable to the current crate. By default, we automatically sync them to the current project’s directory so they can be used by your agent (either .agent/skills or .claude/skills). This is done through a custom hook (if your agent supports it), but can be disabled or manually synced with cargo agents sync.

Hooks

Unlike skills which are dynamically loaded by agents, hooks are dispatched on certain events such as on agent start, after a user prompt, or prior to a tool use. Symposium has a small number of hooks it installs (when available) that it uses to ensure that plugins are discovered and loaded for an agent to use.

Additionally, today, hooks defined by plugins are also dispatched through Symposium. This allows, for example, dispatching hooks written for one agent when using a different agent (to the extent that we’ve implemented support). The list of supported hooks is fairly small, but we’re far from done with expanding hooks support.

MCP servers

MCP servers were one of the first extensions made available by agents. They expose a set of tools, either local or remote, that agents can call. MCP servers defined by a Symposium plugin get installed into your agent’s settings for use.

What’s next?

As we said in the beginning, the Symposium org is focused on “AI the Rust way” – so what does that mean? We’re starting with a minimal, usual product for users to experiment with and hopefully find use from. But, we’re far from done. We have a number of really interesting ideas to make Symposium even more useful.

We want to continue to expand the set of agent features that Symposium supports. When an agent supports a tool or similar, we want it to be a minimal process to be able to recommend that users of your crate also use that tool. Often, this means that we should “just install” those tools into project-local agent settings; but, we want to make sure that this is done correctly and supports the agents that our users use. However, we also want to support (when possible) more dynamic loading - such as by dispatching hooks through Symposium itself, or having Symposium register a transparent MCP server layer. There are lots of things we can do here, and we’re excited to hear what people want and need first.

We currently have pretty minimal support for how to run hooks or MCP servers - really just a command to run. We already have in-progress work to support declarative dependencies, which in turns allows both auto-installation and auto-updates. Using a symposium plugin should “just work”.

The work we’re presenting today is focused mainly around Rust crates, but our vision also includes better recommendations around the Rust language. We’ve already seen a few ecosystem-driven projects with this goal - we plan to review these and find what works best for Symposium users and make it the default for the best experience possible when writing Rust code. Similarly, we plan to write our own plugins that help your agent format and test Rust code that it has written, before you even look at it.

Symposium previously was focused around the Agent Client Protocol (ACP), which provides a programmatic way to interact with and extend agent capabilities. We still love this vision, but our current focus is on an ecosystem-first approach of meeting agents where they are today. We do expect that as ACP adoption continues to increase and we have a solid foundation with the work we’ve presented today, that we will again focus on ACP to further increase the interoperability and extensibility we provide for users of Symposium.

Finally, although our initial work is focused around Rust, we think this idea - discoverability and use of plugins defined by dependencies - is applicable and useful for other language ecosystems too. We would love to expand this to other languages.

In all, we’re really excited for people to use Symposium. We hope that what we’ve shared today gets you excited about building better Rust with AI, and we think that this is only the beginning. If you have thoughts or questions, either open an issue on Github or join the Symposium Zulip; we’d love to hear your thoughts!

Getting Started

Install

The fastest way to install Symposium is with cargo binstall:

cargo binstall symposium

If you prefer to build from source, use cargo install instead:

cargo install symposium

Initialization

Once you have installed Symposium, you need to run the init command:

cargo agents init

Select your agents

This will prompt you to select the agents you use (Claude Code, Copilot, Gemini, etc.) — you can pick more than one:

Which agents do you use? (space to select, enter to confirm):
> [ ] Claude Code
  [x] Codex CLI
  [ ] GitHub Copilot
  [ ] Gemini CLI
  [ ] Goose
  [x] Kiro
  [x] OpenCode

Global vs project hook registration

Next, Symposium will ask you whether you want to register hooks globally or per-project:

  • global registration means Symposium will activate automatically for all Rust projects.
  • project registration means Symposium only activates in projects once you run cargo agents sync to add hooks to that project.

We recommend global registration for maximum convenience.

Tweaking other settings

You may wish to browse the configuration page to learn about other settings, such as how to disable auto-sync.

After setup

Symposium will now install skills, MCP servers, and other extensions based on your dependencies automatically.

Currently all the plugins installed by Symposium can be found in the central recommendations repository. We expect eventually to allow crates to define their own plugins without any central repository, but not yet. If you have a crate and would like to add a plugin for it to symposium, see the Supporting your crate page.

If you have private crates or would like to install plugins for your own use, you can consider adding a custom plugin source.

Workspace skills

In addition to adding skills based on your dependencies, Symposium will also copy any additional skills found in .agents/skills into the directory appropriate for your configured agent(s).

This “skill-syncing” feature allows your project to add skills in one central location that will work for all developers, regardless of which agent they use (for example, Claude Code users will have the skills synced to .claude/skills).

The default skill location therefore varies depending on the intended audience:

Skills intended for…Go into…
Maintaining your crate.agents/skills
Using your crateskills/

We recommend you commit your .agents/skills or skills/ into the repository. Symposium installs a .gitignore file into every skill that it creates, so automatically copied and installed skills should not dirty your git status.

Pre-existing files

Symposium never touches skills in .claude/skills/, .kiro/skills/ etc. that it did not put there itself. If you previously hand-wrote a skill with the same name as one in .agents/skills/, propagation is skipped for that name and a warning is printed — your existing file stays in place.

Custom plugin sources

Custom plugin sources let you define your own sets of plugins without putting them in the central recommendations repository.

Custom plugin sources are useful for:

  • Company-specific plugins — internal tools and guidelines for your organization
  • Development plugins — local plugins you’re working on

Custom plugins in your home directory

plugin definitions or standalone skills added to the ~/.symposium/plugins directory will be registered by default and propagated appropriately to your other projects.

Adding your own custom sources

You can also define a custom plugin source in a git repository or at another path on your system.

Git repository

Add a remote Git repository as a plugin source:

# In ~/.symposium/config.toml
[[plugin-source]]
name = "my-company"
git = "https://github.com/mycompany/symposium-plugins"
auto-update = true

We recommend creating a CI tool that runs cargo agents plugin validate on your repository with every PR to ensure it is properly formatted.

Local directory

Add a local directory as a plugin source:

[[plugin-source]]
name = "local-dev"
path = "./my-plugins"
auto-update = false

Structure of a plugin source

See the reference section for details on what a plugin source looks like.

Managing sources

The cargo agents plugin command allows you to perform operatons on the installed plugin sources, like synchronizing their contents or validating their structure.

Supporting your crate

If you maintain a Rust crate, you can extend Symposium with skills, MCP servers, or other extensions that will teach agents the best way to use your crate.

Embed skills in your crate

The recommended approach is to ship skills directly in your crate’s source tree. Add a skills/ directory with SKILL.md files, then add a small plugin manifest to our central recommendations repository. Users will get guidance that matches the exact version of your crate they’re using.

See Authoring a plugin for the full walkthrough.

Skill layout metadata

You can control where Symposium looks for skills (and redirect to other crates) by adding [package.metadata.symposium] to your Cargo.toml:

# Optional — absence means "look in skills/ by default"
[[package.metadata.symposium.skills]]
path = "guidance"   # custom subdirectory for skills

[[package.metadata.symposium.skills]]
crate = { name = "other-crate", version = ">=1.0" }  # redirect to another crate

Each [[package.metadata.symposium.skills]] entry specifies either path (a subdirectory of your crate source) or crate (a redirect to another crate). The two are mutually exclusive within a single entry.

Resolution rules

  1. No metadata section — Symposium falls back to the default skills/ subdirectory.
  2. Metadata present but skills = [] — no skills from this crate (NOT a fallback to skills/).
  3. path entries — look in that subdirectory of your crate’s source.
  4. crate entries — fetch that crate’s source and follow its metadata recursively.

Redirects

Redirects allow a crate to delegate skill hosting to another crate. This is useful when:

  • Your main crate is small but skills live in a larger companion package.
  • Multiple crates in a workspace want to share a single set of skills.
  • You want to version skills separately from the library.
# In dial9-tokio-telemetry/Cargo.toml
[[package.metadata.symposium.skills]]
crate = { name = "dial9-viewer" }

Redirects can target any crate, not just workspace dependencies. If the target isn’t in the workspace, Symposium fetches it from the registry using the specified version constraint (or latest if omitted).

Cycle detection prevents infinite loops (A → B → A stops and warns). Redirect chains are capped at 10 hops. Crate name comparison is hyphen/underscore-insensitive (my-crate and my_crate are the same crate for cycle detection purposes).

Edge cases

  • Malformed metadata — If [package.metadata.symposium] is present but unparseable (wrong types, missing fields), Symposium logs a warning and falls back to the default skills/ subdirectory. Fix any warnings to ensure your intended layout is respected.
  • Missing path directory — If a path entry references a directory that doesn’t exist, Symposium silently produces zero skills for that entry. Other entries in the same metadata section are still processed.
  • Diamond redirects — If multiple crates in the workspace all redirect to the same target crate, the target’s skills are installed once (deduplication is based on crate name + version, not who redirected).

Want to write a skill for someone else’s crate?

We prefer crates to ship their own skills, but some crates may not want to or may not be actively maintained. We also accept skills for those crates to help bootstrap the ecosystem. External skills must be uploaded directly into our central recommendations repository so that we can vet them.

See Authoring a plugin for the details.

Moar power!

Beyond skills, there are two more extension types you can publish through a plugin:

  • Hooks — checks and transformations that run when the AI performs certain actions, like writing code or running commands.
  • MCP servers — tools and resources exposed to agents via the Model Context Protocol.

See Authoring a plugin for how to set one up.

Authoring a plugin

Symposium lets you ship skills, hooks, and MCP servers that are automatically loaded when a user’s project depends on your crate. This page walks through how to create a plugin and configure each extension type.

Step 1. Create a SYMPOSIUM.toml manifest

Every plugin starts with a SYMPOSIUM.toml manifest uploaded to the central recommendations repository. The manifest declares your plugin’s name, which crates it applies to, and what extensions it provides.

# `my-crate/SYMPOSIUM.toml` on the symposium-dev/recommendations repository
name = "my-crate"
crates = ["my-crate"]

The crates field controls when the plugin is active — it will only load for projects that depend on the listed crates. Use ["*"] to apply to all projects.

See the plugin definition reference for the full manifest schema.

Why is the central repository required?

We currently require an entry in our central recommendations repository before Symposium will install a plugin. This protects against malicious plugins (e.g., from typosquatting crates) and lets us centrally yank a plugin that proves problematic. Once Symposium has reached a steady state and we have established security protocols we are comfortable with, we expect to lift this requirement.

Step 2. Add skills, hooks, and/or MCP servers

With your manifest in place, you can add any combination of the extension types below.

Skills

Skills are guidance documents that teach AI assistants how to use a crate. Each skill is a directory containing a SKILL.md file with YAML frontmatter and a markdown body:

---
name: my-crate-basics
description: Basic guidance for my-crate usage
---

Prefer using `Widget::builder()` over constructing widgets directly.
Always call `.validate()` before passing widgets to the runtime.

See the Skill definition reference for the full format and the agentskills.io quickstart for writing effective skills.

If you maintain the crate, we recommend shipping skills directly in your source tree. This way users always get skills matching the exact version they have installed.

1. Put skills in your crate sources under skills/
my-crate/
    Cargo.toml
    src/
        lib.rs
    skills/
        basics/
            SKILL.md
        advanced-patterns/
            SKILL.md
2. Add source = "crate" to your manifest
# `my-crate/SYMPOSIUM.toml` on the symposium-dev/recommendations repository
name = "my-crate"
crates = ["my-crate"]

[[skills]]
source = "crate"

Symposium fetches the crate source (from the local cargo cache or crates.io) and discovers skills in the skills/ directory.

Prefer a directory other than skills/?

Add [package.metadata.symposium] to your crate’s Cargo.toml to specify a custom path:

# In your crate's Cargo.toml
[[package.metadata.symposium.skills]]
path = "docs/agent-skills"

When no metadata section is present, Symposium defaults to the skills/ directory. See Supporting your crate for the full metadata schema including redirects to other crates.

Standalone skills (on the recommendations repo)

You can also upload skills directly to the recommendations repo — without embedding them in the crate source. This is the right approach when you’re writing skills for a crate you don’t maintain.

Place skill directories alongside your SYMPOSIUM.toml:

my-crate/
    SYMPOSIUM.toml
    basics/
        SKILL.md
    advanced-patterns/
        SKILL.md

And point the manifest at the local directory:

name = "my-crate"
crates = ["my-crate"]

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

Standalone skills must include crates in their frontmatter so Symposium knows which crate they apply to:

---
name: widgetlib-basics
description: Basic guidance for widgetlib usage
crates: widgetlib=1.0
---

Guidance body here.

Skills from a git repository

Symposium also supports fetching skills from a GitHub URL:

[[skills]]
source.git = "https://github.com/org/my-crate/tree/main/symposium/skills"

This is useful for hosting skills in a dedicated repository or a subdirectory of a monorepo. Note that the central recommendations repository does not currently accept source.git entries by policy — use source = "crate" or source.path for submissions there.

Installing auxiliary tools

An installation tells symposium how to obtain a binary that your hooks or MCP servers will run. The recommended approach is a cargo installation, which installs a crate binary from crates.io:

[[installations]]
name = "my-crate-hooks"
source = "cargo"
crate = "my-crate-hooks"
executable = "my-crate-hooks"

Symposium caches the binary under ~/.symposium/cache/. Binaries are updated automatically when new versions are available on crates.io.

See the plugin definition reference for other installation sources (GitHub repositories, local paths) and advanced options like install_commands.

Hooks

Hooks run when the AI performs certain actions — invoking a tool, starting a session, or submitting a prompt. They receive JSON on stdin describing the event and can return guidance, inject context, or block the action.

Every agent varies in the specifics of what hooks it offers and how those hooks are configured. Symposium allows you to provide agent-specific hook handlers, but we recommend instead using a Symposium hook handler, which is portable across all agents.

Symposium hooks (portable across agents)

To define a Symposium hook handler you add a [[hooks]] section. This defines the command to run as well as the events it expects and other filters.

[[hooks]]
name = "check-usage"
event = "PreToolUse"
matcher = "Bash"
command = "my-crate-hook-command"

The command field references the name of an installation defined in the [[installations]] section described previously. For example:

[[installations]]
name = "my-crate-hook-command"
source = "cargo"
crate = "my-crate-hooks"
executable = "my-crate-hooks"

The hook binary receives symposium canonical JSON on stdin and writes symposium canonical JSON to stdout. Symposium handles converting to and from each agent’s wire format, so a single implementation works across all supported agents. See Writing a hook handler for how to implement the binary using the symposium-hook crate, and Symposium hook events for input/output JSON schemas.

Agent-specific hooks

You can also provide hooks specialized for a particular agent by setting format to an agent name. The handler receives that agent’s native wire format on stdin — giving you access to agent-specific features (e.g., Claude Code’s updatedInput, Copilot’s modifiedArgs). Symposium still intermediates; it just delivers in the declared format instead of converting to canonical. On agents without a matching hook, symposium falls back to delivering any symposium-format hook the plugin declares.

[[hooks]]
name = "check-usage-claude"
event = "PreToolUse"
format = "claude"
command = "my-crate-hooks"
args = ["--claude"]

On Claude, check-usage-claude fires (receives Claude’s native JSON). On other agents, check-usage fires (receives symposium canonical JSON). See the plugin definition reference for the full [[hooks]] manifest syntax.

MCP servers

MCP servers expose tools and resources to agents via the Model Context Protocol. Symposium registers them into each agent’s configuration during sync — you declare the server once and it works across all agents.

An MCP server typically uses the same installation as your hooks:

[[installations]]
name = "my-crate-mcp"
source = "cargo"
crate = "my-crate-mcp"
executable = "my-crate-mcp"

[[mcp_servers]]
name = "my-crate-tools"
command = "my-crate-mcp"
args = ["--stdio"]

See the plugin definition reference for HTTP and SSE transports, crate filtering, and registration details.

Step 3. Validate your plugin

Before submitting a PR, validate your plugin or skill directory to catch errors early — missing fields, bad crate predicates, unreachable skill paths, and crate names that don’t exist on crates.io. You can run this on your local checkout of the recommendations repo once you’ve prepared your changes:

# Validate a plugin manifest
cargo agents plugin validate path/to/SYMPOSIUM.toml

# Validate a directory of standalone skills
cargo agents plugin validate path/to/skill-directory/

# Skip the crates.io name check (e.g., for private crates)
cargo agents plugin validate path/to/SYMPOSIUM.toml --no-check-crates

Writing a hook handler

This guide walks through writing a symposium hook handler in Rust using the symposium-hook crate.

Step 1. Create a new binary crate

Create your new crate:

cargo new my-hook-handler
cd my-hook-handler

And then add symposium-hook to your dependencies:

cargo add symposium-hook

Step 2. Write the handler

A hook handler is a program that reads a JSON event on stdin and writes a JSON response to stdout. The symposium-hook crate provides a HookHandler trait and a run() harness that handles the plumbing.

Implement HookHandler and override the methods for the events you care about:

// src/main.rs
use std::process::ExitCode;
use symposium_hook::{HookHandler, PreToolUseInput, PreToolUseOutput, run};

struct MyHook;

impl HookHandler for MyHook {
    fn pre_tool_use(&self, event: &PreToolUseInput) -> anyhow::Result<PreToolUseOutput> {
        if event.tool_name == "Bash" {
            Ok(PreToolUseOutput::context("Remember: prefer non-destructive commands"))
        } else {
            Ok(PreToolUseOutput::default())
        }
    }
}

fn main() -> ExitCode {
    run(MyHook)
}

The run() function:

  1. Reads symposium canonical JSON from stdin.
  2. Deserializes it into an Input event.
  3. Calls handler.handle_event(), which dispatches to the appropriate method.
  4. Serializes the output to stdout.

You only need to override the methods you care about — unimplemented methods return the default (empty) output for their event type.

Step 3. Register it in your plugin manifest

In your SYMPOSIUM.toml, reference the built binary as a hook command:

name = "my-crate"
crates = ["my-crate"]

[[hooks]]
name = "check-usage"
event = "PreToolUse"
command = { source = "cargo", crate = "my-hook-handler", executable = "my-hook-handler" }

Output types

Each handler method returns its event-specific output type:

MethodReturn typeKey fields
pre_tool_usePreToolUseOutputadditional_context, updated_input
post_tool_usePostToolUseOutputadditional_context
user_prompt_submitUserPromptSubmitOutputadditional_context
session_startSessionStartOutputadditional_context

Each output type has convenience constructors:

  • ::default() — empty output, no-op.
  • ::context("...") — inject text into the agent’s context.
  • PreToolUseOutput::with_updated_input(value) — replace the tool input.
  • PreToolUseOutput::deny("reason") — block the tool call with a reason.

Return Err(...) from any method to report an error (exit code 1, message on stderr).

The HookHandler trait

#![allow(unused)]
fn main() {
pub trait HookHandler {
    fn handle_event(&self, input: &Input) -> anyhow::Result<Output> { /* dispatches */ }
    fn pre_tool_use(&self, event: &PreToolUseInput) -> anyhow::Result<PreToolUseOutput> { /* default */ }
    fn post_tool_use(&self, event: &PostToolUseInput) -> anyhow::Result<PostToolUseOutput> { /* default */ }
    fn user_prompt_submit(&self, event: &UserPromptSubmitInput) -> anyhow::Result<UserPromptSubmitOutput> { /* default */ }
    fn session_start(&self, event: &SessionStartInput) -> anyhow::Result<SessionStartOutput> { /* default */ }
}
}

Override handle_event only if you need custom dispatch logic (e.g., shared state across events). Otherwise, just override the per-event methods.

Testing locally

You can test your handler by piping JSON directly:

cargo build
echo '{"PreToolUse":{"tool_name":"Bash","tool_input":{"command":"rm -rf /"},"session_id":null,"cwd":"/tmp"}}' \
  | ./target/debug/my-hook-handler

Or via the symposium CLI:

echo '{"PreToolUse":{"tool_name":"Bash","tool_input":{"command":"rm -rf /"},"session_id":null,"cwd":"/tmp"}}' \
  | cargo agents hook symposium pre-tool-use

Example: blocking destructive commands

use std::process::ExitCode;
use symposium_hook::{HookHandler, PreToolUseInput, PreToolUseOutput, run};

struct BlockDestructive;

impl HookHandler for BlockDestructive {
    async fn pre_tool_use(&self, event: &PreToolUseInput) -> anyhow::Result<PreToolUseOutput> {
        if event.tool_name == "Bash"
            && let Some(cmd) = event.tool_input.get("command").and_then(|v| v.as_str())
            && cmd.contains("rm -rf")
        {
            return Ok(PreToolUseOutput::deny(
                "Destructive rm -rf commands are not allowed",
            ));
        }
        Ok(PreToolUseOutput::default())
    }
}

fn main() -> ExitCode {
    run(BlockDestructive)
}

Example: injecting context on session start

use std::process::ExitCode;
use symposium_hook::{HookHandler, SessionStartInput, SessionStartOutput, run};

struct InjectContext;

impl HookHandler for InjectContext {
    async fn session_start(
        &self,
        _event: &SessionStartInput,
    ) -> anyhow::Result<SessionStartOutput> {
        Ok(SessionStartOutput::context(
            "This project uses tokio 1.x for async. Prefer spawn over block_on.",
        ))
    }
}

fn main() -> ExitCode {
    run(InjectContext)
}

Reference

The reference defines the behavior of the Symposium system in detail.

The cargo agents command

cargo agents manages agent extensions for Rust projects. It discovers skills based on your project’s dependencies and configures your AI agent to use them.

Subcommands

CommandDescription
cargo agents initSet up user-wide configuration
cargo agents syncSynchronize skills with workspace dependencies
cargo agents pluginManage plugin sources
cargo agents self-updateUpdate symposium to the latest version
cargo agents crate-infoFind crate sources (agent-facing)

Global options

FlagDescription
--update <LEVEL>Plugin source update behavior: none (default), check, fetch
-q, --quietSuppress status output
--helpPrint help
--versionPrint version

cargo agents init

Set up Symposium for the current user.

Usage

cargo agents init [OPTIONS]

Behavior

Prompts for which agents you use (e.g., Claude Code, Copilot, Gemini) and where to install hooks, writes ~/.symposium/config.toml, and registers hooks for each selected agent.

If a user config already exists, init updates it (preserving existing settings not affected by the flags).

Options

FlagDescription
--add-agent <name>Add an agent (e.g., claude, copilot, gemini). Repeatable. Skips the interactive prompt.
--remove-agent <name>Remove an agent. Repeatable.
--hook-scope <scope>Where to install hooks: global (default, writes to ~/) or project (writes to the project directory).

Examples

Interactive setup:

cargo agents init

Non-interactive, specifying agents directly:

cargo agents init --add-agent claude --add-agent gemini

Adding an agent to an existing config:

cargo agents init --add-agent copilot

Removing an agent:

cargo agents init --remove-agent gemini

cargo agents sync

Synchronize skills with workspace dependencies.

Usage

cargo agents sync

Behavior

Must be run from within a Rust workspace. Performs the following steps:

  1. Find workspace root — runs cargo metadata to locate the workspace.

  2. Scan dependencies — reads the full dependency graph from the workspace.

  3. Discover applicable skills — loads plugin sources (from user config) and matches skill predicates against workspace dependencies.

  4. Install skills — for each configured agent, copies applicable SKILL.md files into the agent’s expected skill directory (e.g., .claude/skills/ for Claude Code, .agents/skills/ for Copilot/Gemini/Codex). A .gitignore containing * is written into every new skill directory (and its skills/ parent if new), and an empty .symposium marker file is dropped into each installed skill directory.

  5. Mirror workspace skills — if agents-syncing is enabled (default), user-authored skills in <workspace>/.agents/skills/ are propagated into the skill directories of any configured agent that doesn’t natively use .agents/skills/ (e.g., .claude/skills/, .kiro/skills/). See Workspace skills.

  6. Clean up stale skills — scans every agent’s skills parent directory and removes any subdirectory containing the .symposium marker that wasn’t installed (or propagated) this sync. Directories without the marker (user-managed) are left untouched.

  7. Register hooks — ensures hooks and MCP servers are registered for all configured agents. Registers both global hooks (for all projects) and project-specific hooks (for the current project). Unregisters hooks for agents no longer in the config.

Automatic sync

By default (auto-sync = true), cargo agents sync runs automatically during hook invocations. This keeps skills in sync with workspace dependencies without manual intervention. Set auto-sync = false in the user config to disable this and sync manually.

Example

# One-time setup
cargo agents init --add-agent claude

# Sync skills for the current workspace
cargo agents sync

cargo agents self-update

Update symposium to the latest version.

Usage

cargo agents self-update

Behavior

  1. Check for updates — runs cargo search against the configured registry to find the latest published version of symposium. If the installed version is already current, prints a message and exits.

  2. Install — runs cargo install symposium --force to build and install the latest version.

Configuration

The auto-update key in ~/.symposium/config.toml controls update behavior. It is also configurable during cargo agents init.

auto-update

ValueBehavior
"on" (default)Check the registry at most once per 24 hours. When a newer version is found, automatically install it and re-execute the current command with the new binary.
"warn"Check the registry at most once per 24 hours. Print a message when a newer version is available. During hook invocations, the nudge is included in the session-start hook’s additionalContext.
"off"Never check for updates.

The 24-hour throttle is tracked in ~/.symposium/state.toml. The check is skipped for self-update itself (which always checks unconditionally).

State file

~/.symposium/state.toml tracks:

  • version — the semver of the binary that last ran. Updated on every invocation. Future versions can use a version mismatch to trigger migrations.
  • last-update-check — timestamp of the last registry query. Used to throttle checks to once per 24 hours.

Examples

Manual update:

cargo agents self-update

Disable all update checks:

# ~/.symposium/config.toml
auto-update = "off"

Warn instead of auto-updating:

# ~/.symposium/config.toml
auto-update = "warn"

cargo agents plugin

Manage plugin sources.

Usage

cargo agents plugin <SUBCOMMAND>

Subcommands

cargo agents plugin sync

cargo agents plugin sync [PROVIDER]

Fetch or update git-based plugin sources. If a provider name is given, syncs only that provider (ignoring auto-update settings). If omitted, syncs all providers that have auto-update = true.

cargo agents plugin list

cargo agents plugin list

List all configured plugin sources and the plugins they provide.

cargo agents plugin show

cargo agents plugin show <PLUGIN>

Show details for a specific plugin, including its TOML configuration and source file path.

cargo agents plugin validate

cargo agents plugin validate <PATH> [--no-check-crates]

Validate a plugin source directory or a single TOML manifest file. Useful when authoring plugins.

FlagDescription
<PATH>Path to a directory or a single .toml file
--no-check-cratesSkip checking that crate names in predicates exist on crates.io

cargo agents crate-info

Find crate sources and guidance.

This is an agent-facing command, listed under “Commands for agents” in cargo agents --help. Its output format and exit codes may change in future releases.

Usage

cargo agents crate-info <NAME> [--version <VERSION>]

Behavior

Fetches the crate’s source code and returns:

  • Path to the extracted crate source
  • Available skills for the crate

Options

FlagDescription
<NAME>Crate name to get guidance for
--version <VERSION>Version constraint (e.g., 1.0.3, ^1.0). Defaults to the workspace version or latest.

Unstable agent commands

The commands in this section are invoked by AI agents, not by users directly. They are hidden from cargo agents --help, and their arguments, output format, and exit codes may change in future releases without notice.

Currently this is cargo agents hook, the hook protocol entry point. (crate-info is also agent-facing but is no longer hidden — it appears under “Commands for agents” in cargo agents --help.)

cargo agents hook

Entry point invoked by your agent’s hook system. This is an internal command — you generally don’t need to run it yourself.

Usage

cargo agents hook <AGENT> <EVENT>

Behavior

When your agent triggers a hook event, it calls cargo agents hook with the agent name and event type. The hook does two things:

  1. Auto-sync (if enabled) — when auto-sync = true in the user config, runs cargo agents sync to ensure skills are current for the workspace. The workspace root is resolved from the hook payload’s cwd field; if the payload does not include a working directory, the process’s current working directory is used as a fallback. Failures are logged but don’t block hook dispatch.

  2. Dispatches to plugin hooks — runs any hook handlers defined by plugins for the given event.

Events

The specific events depend on which agent you are using. cargo agents init configures the hook registration appropriate for your agents.

When is the hook invoked?

The hook is registered globally during cargo agents init. It runs automatically when your agent triggers supported events (e.g., session start, tool use).

Supported agents

Symposium supports seven AI coding agents. Each agent gets skill installation; hook support varies by agent.

Claude Code

Config name: claude

Skills

ScopePath
Project.claude/skills/<name>/SKILL.md
Global~/.claude/skills/<name>/SKILL.md

Claude Code does not support the vendor-neutral .agents/skills/ path.

Hooks

Symposium merges hook entries into Claude Code’s settings.json.

ScopeFile
Project.claude/settings.json
Global~/.claude/settings.json

Events registered: PreToolUse, PostToolUse, UserPromptSubmit, SessionStart (PascalCase).

Output format: JSON with hookSpecificOutput wrapper. Exit code 2 blocks tool use.

MCP servers

ScopeFileKey
Project.claude/settings.jsonmcpServers.<name>
Global~/.claude/settings.jsonmcpServers.<name>

GitHub Copilot

Config name: copilot

Skills

ScopePath
Project.agents/skills/<name>/SKILL.md
Global(none)

Copilot has no global skills path.

Hooks

Symposium creates a symposium.json file in the project hooks directory, and merges entries into the global config.

ScopeFile
Project.github/hooks/symposium.json
Global~/.copilot/config.json

Events registered: preToolUse, postToolUse, userPromptSubmitted, sessionStart (camelCase).

Output format: JSON. Uses "bash" key instead of "command" for platform-specific dispatch. Any non-zero exit code denies (not just exit 2).

MCP servers

ScopeFileKey
Project.vscode/mcp.json<name> (top-level)
Global~/.copilot/mcp-config.json<name> (top-level)

Gemini CLI

Config name: gemini

Skills

ScopePath
Project.agents/skills/<name>/SKILL.md
Global~/.gemini/skills/<name>/SKILL.md

Hooks

Symposium merges hook entries into Gemini’s settings.json.

ScopeFile
Project.gemini/settings.json
Global~/.gemini/settings.json

Events registered: BeforeTool, AfterTool, BeforeAgent, SessionStart (Gemini’s own naming).

Output format: JSON with nested matcher groups. Timeouts in milliseconds.

MCP servers

ScopeFileKey
Project.gemini/settings.jsonmcpServers.<name>
Global~/.gemini/settings.jsonmcpServers.<name>

Codex CLI

Config name: codex

Skills

ScopePath
Project.agents/skills/<name>/SKILL.md
Global~/.agents/skills/<name>/SKILL.md

Hooks

Symposium merges hook entries into Codex’s hooks.json.

ScopeFile
Project.codex/hooks.json
Global~/.codex/hooks.json

Events registered: PreToolUse, PostToolUse, UserPromptSubmit, SessionStart (PascalCase).

Output format: JSON. Exit code 2 blocks tool use.

Caveat: Codex hooks are experimental and disabled by default. To enable, add to ~/.codex/config.toml:

[features]
codex_hooks = true

MCP servers

ScopeFileKey
Project.codex/config.toml[mcp_servers.<name>]
Global~/.codex/config.toml[mcp_servers.<name>]

Kiro

Config name: kiro

Skills

ScopePath
Project.kiro/skills/<name>/SKILL.md
Global~/.kiro/skills/<name>/SKILL.md

Kiro uses its own skill path, not the vendor-neutral .agents/skills/.

Hooks

Kiro requires hooks to be registered with a named agent. We create a symposium agent by creating a symposium.json agent definition file in .kiro/agents/. This registers Symposium as a Kiro agent with hooks attached. If you use a different agent, you won’t benefit from hook-based features like token reduction unless you manually add the hooks into your agent definition.

ScopeFile
Project.kiro/agents/symposium.json
Global~/.kiro/agents/symposium.json

Events registered: preToolUse, postToolUse, userPromptSubmit, agentSpawn (camelCase; agentSpawn maps to session-start internally).

Output format: plain text on stdout (not JSON). Exit code 2 blocks preToolUse only.

The generated agent file includes "tools": ["*"] (all tools available) and "resources": ["skill://.kiro/skills/**/SKILL.md"] (auto-discover skills). Without tools, a Kiro custom agent has zero tools.

Caveat: Kiro uses a flat hook entry format ({ "command": "..." }) unlike the nested format used by Claude/Gemini/Codex. Unregistration deletes the symposium.json file entirely.

MCP servers

ScopeFileKey
Project.kiro/settings/mcp.jsonmcpServers.<name>
Global~/.kiro/settings/mcp.jsonmcpServers.<name>

OpenCode

Config name: opencode

Skills

ScopePath
Project.agents/skills/<name>/SKILL.md
Global~/.agents/skills/<name>/SKILL.md

Hooks

OpenCode does not support shell-command hooks. Its extensibility is based on TypeScript/JavaScript plugins. Symposium cannot register hooks for OpenCode.

OpenCode is supported as a skills-only agent — cargo agents sync will install skill files, but no hooks are registered.

MCP servers

ScopeFileKey
Projectopencode.jsonmcp.<name>
Global~/.config/opencode/opencode.jsonmcp.<name>

Goose

Config name: goose

Skills

ScopePath
Project.agents/skills/<name>/SKILL.md
Global~/.agents/skills/<name>/SKILL.md

Hooks

Not supported. Goose has no hook system. It uses MCP extensions for extensibility.

Skill files are installed but cargo agents hook will never be called by this agent.

MCP servers

ScopeFileKey
Project.goose/config.yamlextensions.<name>
Global~/.config/goose/config.yamlextensions.<name>

Configuration

cargo agents uses a single user-wide configuration file at ~/.symposium/config.toml. Created by cargo agents init.

Full example

auto-sync = true
agents-syncing = true
hook-scope = "global"
auto-update = "on"

[[agent]]
name = "claude"

[[agent]]
name = "gemini"

[logging]
level = "info"

[defaults]
symposium-recommendations = true
user-plugins = true

[[plugin-source]]
name = "my-org"
git = "https://github.com/my-org/symposium-plugins"

[[plugin-source]]
name = "local-dev"
path = "my-plugins"

Top-level keys

KeyTypeDefaultDescription
auto-syncbooltrueAutomatically run cargo agents sync during hook invocations. When enabled, skills are kept in sync with workspace dependencies without manual intervention.
agents-syncingbooltruePropagate user-authored skills from .agents/skills/ into the per-agent skill directories of any configured agent that does not natively use .agents/skills/ (such as .claude/skills/ or .kiro/skills/). Skills that symposium itself installed — identified by the .symposium marker file — are not propagated. See Workspace skills for the user-guide overview, or Agents syncing below for details.
hook-scopestring"global"Where agent hooks are installed. "global" writes to the user’s home directory (e.g., ~/). "project" writes to the project directory, keeping hooks local to the workspace.
auto-updatestring"on"Controls automatic update behavior. "off" disables update checks entirely. "warn" checks the registry (at most once per 24 hours) and prints a message when a newer version is available. "on" automatically installs the update via cargo install and re-executes the command with the new binary.

Agents syncing: mirror user-authored skills

Agents such as Copilot, Gemini, Codex, Goose, and OpenCode all read skills from the vendor-neutral .agents/skills/ directory, but Claude Code and Kiro use their own paths (.claude/skills/ and .kiro/skills/). When agents-syncing is enabled, cargo agents sync mirrors any skill that you put in .agents/skills/ into each configured agent’s own skill directory, so a single authored copy is visible to every agent.

A skill is treated as user-authored when its directory contains SKILL.md but does not contain the .symposium marker. Symposium never writes a marker into source skills, so the distinction between “user content” and “a copy symposium made” is unambiguous.

Propagated destinations receive the same .symposium marker and * .gitignore that plugin-installed skills get, which means:

  • Updates to the source (.agents/skills/<name>/) are re-copied on each sync.
  • Removing the source removes the propagated copies on the next sync (via the normal stale-skill reap).
  • Disabling agents-syncing = false also removes previously propagated copies on the next sync.
  • A pre-existing, user-managed file in the target directory (no marker) is never overwritten.

When the only configured agents use .agents/skills/ directly, the feature is a no-op (the source and target are the same directory).

Hook scope: control whether Symposium activates in all projects or only those you select

Registering hooks globally ensures that Symposium activates whenever you use the selected agent, which means that it will work in any Rust project automatically.

Registering hooks at the project level requires you to run cargo agents sync within each project at least once to create the hooks. After that, the auto-sync feature will keep you up-to-date.

[[agent]]

Each [[agent]] entry identifies an agent you use. You can configure multiple agents.

KeyTypeDefaultDescription
namestring(required)Agent name: claude, codex, copilot, gemini, goose, kiro, or opencode.

[logging]

KeyTypeDefaultDescription
levelstring"info"Minimum log level. One of: trace, debug, info, warn, error.

[defaults]

Controls the two built-in plugin sources. Both are enabled by default.

KeyTypeDefaultDescription
symposium-recommendationsbooltrueFetch plugins from the symposium-dev/recommendations repository.
user-pluginsbooltrueScan ~/.symposium/plugins/ for user-defined plugins.

[[plugin-source]]

Defines additional plugin sources. Each entry must have exactly one of git or path.

KeyTypeDefaultDescription
namestring(required)A name for this source (used in logs and cache paths).
gitstringRepository URL. Fetched and cached under ~/.symposium/cache/plugin-sources/.
pathstringLocal directory containing plugins. Relative paths are resolved from ~/.symposium/.
auto-updatebooltrueCheck for updates on startup. Only applies to git sources.

Directory resolution

User-wide data lives under ~/.symposium/ by default. Override with environment variables:

ConfigCacheLogs
SYMPOSIUM_HOME$SYMPOSIUM_HOME/$SYMPOSIUM_HOME/cache/$SYMPOSIUM_HOME/logs/
XDG$XDG_CONFIG_HOME/symposium/$XDG_CACHE_HOME/symposium/$XDG_STATE_HOME/symposium/logs/
Default~/.symposium/~/.symposium/cache/~/.symposium/logs/

SYMPOSIUM_HOME takes precedence over XDG variables.

File locations

PathPurpose
~/.symposium/config.tomlUser configuration
~/.symposium/state.tomlPersistent state (binary version stamp, last update check)
~/.symposium/plugins/User-defined plugins
~/.symposium/cache/Cache directory (crate sources, plugin sources)
~/.symposium/logs/Log files (one per invocation, timestamped)

Plugin sources

A plugin source is a directory or repository containing plugins and standalone skills that Symposium discovers automatically. Plugin sources can be local directories or remote Git repositories, and Symposium searches them recursively to find all available extensions.

Discovery rules

Symposium scans a plugin source recursively to find plugins and standalone skills:

  • A plugin is a directory that contains a SYMPOSIUM.toml file;
  • A standalone skill is a directory that contains a SKILL.md and does not contain a SYMPOSIUM.toml file. Standalone skills must have crates metadata in their frontmatter.

We do not allow plugins or standalone skills to be nested within one another. When we find a directory that is either a plugin or a skill, we do not search its contents any further.

Example structure

plugin-source/
  my-plugin/
    SYMPOSIUM.toml        # ✓ Plugin
    skills/               # ✗ Not searched (parent claimed)
      basic/
        SKILL.md
  serde-skill/
    SKILL.md              # ✓ Standalone skill
  nested/
    deep/
      tokio-skill/
        SKILL.md          # ✓ Standalone skill (found recursively)
  mixed/
    SYMPOSIUM.toml        # ✓ Treated as plugin
    SKILL.md              # ✗ Ignored (plugin takes precedence)

Configuration

Plugin sources are configured in your config.toml file. See the Configuration reference for details on setting up local directories, Git repositories, and built-in sources.

Validation

You can validate a plugin source directory:

# Validate all plugins and skills in a directory
cargo agents plugin validate path/to/plugin-source/

# Also verify that crate names exist on crates.io (on by default; use --no-check-crates to skip)
cargo agents plugin validate path/to/plugin-source/ --no-check-crates

This scans the directory, attempts to load all plugins and skills, and reports any errors found.

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"
crates = ["*"]

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

Top-level fields

FieldTypeRequiredDescription
namestringyesPlugin name. Used in logs and CLI output.
cratesstring or arraynoWhich crates this plugin applies to. Use ["*"] for all crates. See Plugin-level filtering.
installationsarray of tablesnoNamed installation declarations ([[installations]]). Hooks reference these by name. See Installations.
skillsarray of tablesnoSkill groups ([[skills]]).
hooksarray of tablesnoHooks ([[hooks]]).
mcp_serversarray of tablesnoMCP server registrations ([[mcp_servers]]).

Note: Every plugin must specify crates somewhere — either at the plugin level, in [[skills]] groups, or in [[mcp_servers]] entries. Plugins without any crate targeting will fail validation.

Plugin-level filtering

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

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

# OR use wildcard to always apply
crates = ["*"]

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
cratesstring or arrayWhich crates this group advises on. Accepts a single string ("serde") or array (["serde", "tokio>=1.0"]). See Crate predicates for syntax.
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.
source = "crate"stringLook for skills inside the matched crates’ source trees. Layout is determined by [package.metadata.symposium] in each crate’s Cargo.toml. See Crate-sourced skills.

A skill group must have exactly one of source.path, source.git, or source = "crate".

Crate-sourced skills

When using source = "crate", Symposium resolves the crate predicates in scope (plugin-level and group-level) to determine which crate sources to fetch, then reads each crate’s [package.metadata.symposium] section to find skills.

This is the recommended way for crate authors to ship skills alongside their crate. See Supporting your crate for details on the metadata format.

name = "serde-plugin"
crates = ["serde"]

[[skills]]
source = "crate"

At least one non-wildcard crate predicate must be present (at either the plugin or group level) so that Symposium knows which crate sources to fetch. See Matched crate set for details.

Skill layout in crate source

Symposium reads [package.metadata.symposium] from the crate’s Cargo.toml:

  • No metadata section — fall back to the default skills/ subdirectory.
  • Malformed metadata — logged as a warning, then fall back to the default skills/ subdirectory.
  • skills = [] — no skills from this crate (not a fallback to skills/). This is an explicit opt-out and is respected even when the crate is reached via a redirect.
  • [[skills]] entries — process each entry independently:
    • path = "..." — look in that subdirectory of this crate’s source. If the directory does not exist, zero skills are produced for that entry (no error).
    • crate = { name = "...", version = "..." } — redirect to another crate and follow its metadata recursively.

Redirects are followed with cycle detection (hyphen/underscore-insensitive) and a depth limit of 10. When multiple crates redirect to the same target, the target’s skills are installed once (deduplication by crate name + version).

See Supporting your crate for the full metadata schema.

Semantics when matching against multiple crates

If your plugin lists multiple crates, then skills will be loaded from whichever crates are present. For example, the following plugin will activate if foo, bar, or baz are present in the dependencies:

name = "my-plugin"
crates = ["foo", "bar", "baz"]

[[skills]]
source = "crate"

Once activated, source = "crate" will cause Symposium to look for skills in whichever crates are present. So if the project has foo and bar (but not baz), we would look at the sources for foo and bar (but not baz).

Redirects via crate metadata

Crate authors can redirect skill resolution to another crate using their Cargo.toml metadata. For example, dial9-tokio-telemetry could redirect to dial9-viewer:

# In dial9-tokio-telemetry/Cargo.toml
[[package.metadata.symposium.skills]]
crate = { name = "dial9-viewer" }

This means the plugin just needs source = "crate" and the crate itself controls where skills are fetched from. Redirects are followed recursively (with cycle detection and a depth limit of 10).

Semantics of crates predicates at multiple levels

When you apply the crates predicate at multiple levels, all levels must match. This can be used to narrow the set of crates that have skills versus the set that activates your plugin overall.

name = "my-plugin"

# Any of these crates activates the plugin
crates = ["foo", "bar", "baz"]

[[skills]]
crates = ["foo", "bar"] # ... but this block applies only to "foo" and "bar"
source = "crate" # ...which get their skills from their sources

[[skills]]
crates = ["baz"] # ... this block applies to "baz"
source = "crate" # ... baz's Cargo.toml metadata controls where skills come from

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.

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.

[[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.
cratesstring or arrayWhich crates this server applies to. Optional if plugin has top-level crates.
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.
cratesstring or arrayWhich crates this server applies to. Optional if plugin has top-level crates.
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.
cratesstring or arrayWhich crates this server applies to. Optional if plugin has top-level crates.
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"
crates = ["widgetlib"]

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

# Additional skills hosted in a git repo
[[skills]]
crates = ["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.

Symposium hook events

This page documents the JSON schemas for symposium-format hooks — the input your hook receives on stdin and the output it should write to stdout. Symposium converts to and from each agent’s native wire format, so you only need to handle these canonical types.

Events

EventDescription
PreToolUseBefore the agent invokes a tool. Can inject context or modify the tool input.
PostToolUseAfter a tool completes. Can inject context.
UserPromptSubmitWhen the user submits a prompt. Can inject context.
SessionStartWhen an agent session begins. Can inject context.

Input schemas

Your hook receives one of the following JSON objects on stdin, depending on which event it is registered for.

PreToolUse

{
  "PreToolUse": {
    "tool_name": "Bash",
    "tool_input": { "command": "cargo test" },
    "session_id": "abc-123",
    "cwd": "/home/user/project"
  }
}
FieldTypeDescription
tool_namestringName of the tool being invoked.
tool_inputobjectArguments the agent is passing to the tool.
session_idstring or nullAgent session identifier, if available.
cwdstring or nullWorking directory of the agent.

PostToolUse

{
  "PostToolUse": {
    "tool_name": "Bash",
    "tool_input": { "command": "cargo test" },
    "tool_response": { "stdout": "test result: ok" },
    "session_id": "abc-123",
    "cwd": "/home/user/project"
  }
}
FieldTypeDescription
tool_namestringName of the tool that was invoked.
tool_inputobjectArguments passed to the tool.
tool_responseobjectThe tool’s response/output.
session_idstring or nullAgent session identifier, if available.
cwdstring or nullWorking directory of the agent.

UserPromptSubmit

{
  "UserPromptSubmit": {
    "prompt": "Fix the failing test in src/lib.rs",
    "session_id": "abc-123",
    "cwd": "/home/user/project"
  }
}
FieldTypeDescription
promptstringThe text the user submitted.
session_idstring or nullAgent session identifier, if available.
cwdstring or nullWorking directory of the agent.

SessionStart

{
  "SessionStart": {
    "session_id": "abc-123",
    "cwd": "/home/user/project"
  }
}
FieldTypeDescription
session_idstring or nullAgent session identifier, if available.
cwdstring or nullWorking directory of the agent.

Output schemas

Your hook writes a JSON object to stdout. The object is wrapped in an enum tag matching the event, just like the input.

PreToolUse output

{
  "PreToolUse": {
    "additionalContext": "Remember to use --release for benchmarks",
    "updatedInput": { "command": "cargo test --release" }
  }
}
FieldTypeDescription
decision"allow" or "deny"Whether to allow or block the tool call. Defaults to "allow" and may be omitted.
additionalContextstring or nullText injected into the agent’s context for this tool call.
updatedInputobject or nullReplacement tool input. If set, overrides the original tool_input.

PostToolUse output

{
  "PostToolUse": {
    "additionalContext": "Note: 3 tests were skipped due to missing fixtures"
  }
}
FieldTypeDescription
additionalContextstring or nullText injected into the agent’s context after the tool result.

UserPromptSubmit output

{
  "UserPromptSubmit": {
    "additionalContext": "Relevant context: this project uses tokio 1.x"
  }
}
FieldTypeDescription
additionalContextstring or nullText injected into the agent’s context for this prompt.

SessionStart output

{
  "SessionStart": {
    "additionalContext": "symposium 0.5.0 is available (current: 0.4.2). Run `cargo agents self-update` to upgrade."
  }
}
FieldTypeDescription
additionalContextstring or nullText injected into the agent’s context at session start.

Exit codes

CodeMeaning
0Success. Stdout is parsed as JSON and merged into the hook result.
2Block. The action is blocked and stderr is returned to the agent as the reason.
Other non-zeroWarning. The hook is considered to have succeeded for dispatch purposes; stdout is still parsed if possible.

Matcher

The matcher field on a hook entry is a regex matched against tool_name for PreToolUse and PostToolUse events. For UserPromptSubmit and SessionStart, the matcher is ignored (all hooks fire). Use "*" to match all tools.

Testing

You can test a symposium-format hook directly from the command line:

echo '{"PreToolUse":{"tool_name":"Bash","tool_input":{"command":"rm -rf /"},"session_id":null,"cwd":"/tmp"}}' \
  | ./scripts/check.sh

Or via the cargo agents hook CLI with the symposium format:

echo '{"PreToolUse":{"tool_name":"Bash","tool_input":{"command":"cargo test"},"session_id":null,"cwd":"/tmp"}}' \
  | cargo agents hook symposium pre-tool-use

Skill definitions

A skill is a SKILL.md file inside a skill directory. Skills follow the agentskills.io format.

Skills can be supplied by a plugin or by adding skills into the .agents/skills directory within the workspace.

Directory layout

skills/
  my-skill/
    SKILL.md
    scripts/       # optional
    resources/     # optional

SKILL.md format

A SKILL.md file has YAML frontmatter followed by a markdown body:

---
name: serde-basics
description: Basic guidance for serde usage
crates: serde
---

Prefer deriving `Serialize` and `Deserialize` on data types.

Frontmatter fields

FieldTypeRequiredDescription
namestringyesSkill identifier.
descriptionstringyesShort description shown in skill listings.
cratesstringnoComma-separated crate atoms this skill is about (e.g., crates: serde, tokio>=1.0). Narrows the enclosing [[skills]] group scope — cannot widen it.

Crate atoms

Crate atoms specify a crate name with an optional version constraint:

  • serde — any version
  • tokio>=1.40 — 1.40 or newer
  • tokio>1.40 — strictly above 1.40
  • regex<2.0 — below 2.0
  • regex<=2.0 — 2.0 or below
  • serde^1.0 — compatible with 1.0 (same as =1.0)
  • serde~1.2 — patch-level changes only (>=1.2.0, <1.3.0)
  • serde=1.0 — compatible-with-1.0 (equivalent to ^1.0)
  • serde==1.0.219 — exact version

See Crate predicates for the full syntax.

Scope composition

crates can be declared at the [[skills]] group level (in the plugin TOML) and at the individual skill level (in SKILL.md frontmatter). They compose as AND: both layers must match for a skill to activate. A skill-level crates narrows the group’s scope — it does not widen it.

Crate predicates

Crate predicates control when plugins, skill groups, and individual skills are active. A predicate matches against a workspace’s direct dependency set — not against individual crates in isolation.

Predicate syntax

A crate predicate is a crate name with an optional version requirement.

Examples:

  • serde
  • serde>=1.0
  • tokio^1.40
  • regex<2.0
  • serde=1.0
  • serde==1.0.219
  • *

Semantics:

  • bare crate name: matches if the workspace has this crate as a direct dependency (any version)
  • >=, <=, >, <, ^, ~: standard semver operators applied to the workspace’s version of the crate
  • =1.0: compatible-version matching, equivalent to ^1.0
  • ==1.0.219: exact-version matching
  • *: wildcard — always matches, even a workspace with zero dependencies

Predicates match against direct workspace dependencies only, not transitive ones.

Usage in different contexts

Plugin manifests (TOML)

The crates field accepts an array of predicate strings:

  • crates = ["serde"]
  • crates = ["serde", "tokio>=1.40"]
  • crates = ["*"] (wildcard — always active)

Skill frontmatter (YAML)

The crates field uses comma-separated values:

  • crates: serde
  • crates: serde, tokio>=1.40

Matching behavior

A crates list matches if at least one predicate in the list matches the workspace. The wildcard * always matches — even a workspace with zero dependencies.

If there are multiple crates declarations in scope, all of them must match (AND composition). For example with skills, crates predicates can appear at three distinct levels:

  • If a plugin defines crates at the top-level, it must match before any other plugin contents will be considered.
  • If a skill-group within a plugin defines crates, that predicate must match before the skills themselves will be fetched.
  • If the skills define crates in their front-matter, those crates must match before the skills will be added to the project.

Matched crate set

When a skill group uses source = "crate", predicates serve a second purpose beyond filtering: they determine which crate sources to fetch.

Each non-wildcard predicate that matches a workspace dependency contributes that dependency’s name and version to the matched crate set. Symposium then fetches the source for each crate in the set and looks for skills inside it.

  • "serde" against a workspace with serde 1.0.210 → matched set: {serde@1.0.210}
  • "serde" against a workspace without serde → no match, plugin skipped
  • "*" → matches, but contributes no concrete crates to the set
  • ["serde", "tokio"] with both in workspace → {serde@1.0.210, tokio@1.38.0}

Predicates from both the plugin level and the group level are unioned together to form the matched set.

Because wildcards contribute no concrete crates, at least one non-wildcard predicate must be present (at either the plugin or group level) when using crate-sourced skills. A manifest with only wildcards and source = "crate" is rejected at parse time.

Contributing to Symposium

Welcome! This section is for people who want to work on Symposium itself. If you’re a crate author who wants to publish skills or hooks for your library, see Supporting your crate instead.

Building and testing

Symposium is a standard Cargo project with both a library and a binary:

cargo check              # type-check
cargo test               # run the test suite
cargo run -- crate-info tokio # run locally: crate-specific guidance

Tests use snapshot assertions via the expect-test crate. If a snapshot changes, run with UPDATE_EXPECT=1 to update it:

UPDATE_EXPECT=1 cargo test

Logging and debugging

Symposium uses tracing for structured logging. Each invocation writes a timestamped log file to ~/.symposium/logs/.

The default log level is info. To get more detail, set the level in ~/.symposium/config.toml:

[logging]
level = "debug"   # or "trace" for maximum detail

Log files are named symposium-YYYYMMDD-HHMMSS.log. When debugging an issue, the log file from the relevant invocation is usually the best place to start.

Tenets

Design principles that guide symposium’s architecture. When in doubt, these break ties.

Unobtrusive

Symposium should never be a reason to opt out. Using it should be non-disruptive to existing workflows:

  • Existing projects should be able to adopt symposium without restructuring.
  • Never dirty the user’s repo with unexpected files or diffs or require users to manually edit .gitignore.
  • Avoid adding “symposium-specific” files or modifications in project repositories (when possible).

Prefer existing standards over our own

When there’s an existing mechanism that works, use it rather than inventing a new one. Adopt agent conventions, standard file layouts, and community norms wherever possible. Symposium’s own canonical format exists only where no cross-agent standard exists.

Union, not least-common-denominator

Symposium’s plugins should be able to take full advantage of what agents can do. We aim for interoperability but we also let plugin authors opt into agent-specific formats or capabilities.

Vendor neutral, interoperable

Plugins and repository provide functionality; users pick their agent. Symposium provides the bridge, exposing plugin functionality in whatever way is requested by an individual user.

Safety

Avoid exposing users to fresh risk. Plugins run code on the user’s machine — symposium should make it easy to audit, constrain, and revoke. The central repository requirement exists to prevent supply-chain attacks until we have better decentralized trust mechanisms.

Empower the ecosystem

Crate authors should be able to ship agent extensions independently, without waiting for central approval or coordination beyond the initial plugin registration. Once registered, updates flow through normal crate publishing. The symposium project’s role is infrastructure, not gatekeeping.

Key repositories

All repositories live under the symposium-dev GitHub organization.

symposium

The main repository. Contains the Symposium CLI/library (Rust), the mdbook documentation, and integration tests.

symposium-claude-code-plugin

The Claude Code plugin that connects Symposium to Claude Code. Contains a static skill, hook registrations (PreToolUse, PostToolUse, UserPromptSubmit), and a bootstrap script that finds or downloads the Symposium binary.

recommendations

The central plugin repository. Crate authors submit skills and plugin manifests here. Symposium fetches this as a plugin source by default.

Key modules

Symposium is a Rust crate with both a library (src/lib.rs) and a binary (src/bin/cargo-agents.rs). The library re-exports all modules so that integration tests can access internals.

config.rs — application context

Everything hangs off the Symposium struct, which wraps the parsed Config with resolved paths for config, cache, and log directories. Two constructors: from_environment() for production and from_dir() for tests.

Defines the user-wide Config (stored at ~/.symposium/config.toml) with [[agent]] entries, logging, plugin sources, defaults, and auto-update (off/warn/on, default warn). Provides plugin_sources() to resolve the effective list of plugin source directories.

agents.rs — agent abstraction

Centralizes agent-specific knowledge: hook registration file paths, skill installation directories, and hook registration logic for each supported agent (Claude Code, GitHub Copilot, Gemini CLI, Codex CLI, Kiro, OpenCode, Goose). Handles the differences between agents — e.g., Claude Code uses .claude/skills/ and Kiro uses .kiro/skills/, while Copilot, Gemini, Codex, OpenCode, and Goose use the vendor-neutral .agents/skills/. OpenCode and Goose are skills-only agents (no hook registration).

init.rs — initialization command

Implements cargo agents init. Prompts for agents (or accepts --add-agent/--remove-agent flags), writes user config, and registers global hooks.

sync.rs — synchronization command

Implements cargo agents sync. Scans workspace dependencies, finds applicable skills from plugin sources, installs them into each configured agent’s skill directory, and drops a .symposium marker file into each installed skill directory. On subsequent syncs, scans every agent’s skills parent directory and reaps any marker-bearing subdirectory it didn’t install this time, leaving user-managed skills (which lack the marker) untouched. Writes a .gitignore with * into every directory it creates. Also provides register_hooks() for use by init, which registers only symposium’s own global hook handler — individual plugin hooks are never written into agent configs.

plugins.rs — plugin registry

Scans configured plugin source directories for TOML manifests and parses them into Plugin structs. Validation here turns the raw TOML into:

  • Installation entries (optional source, optional executable/script, optional args, plus requirements and install_commands) collected on Plugin.installations. Inline installation references on hooks or other installations are promoted into synthetic Installation entries with derived names (<hook> for an inline command, <owner>__req_<i> for an inline requirement), so all references in the validated form are plain names.
  • Hook entries with command: String (the name of an Installation) plus optional hook-level executable / script / args. Validation guarantees at most one of executable/script is set across hook + installation, and at most one layer sets args.

Also discovers standalone SKILL.md files not wrapped in a plugin. Returns a PluginRegistry — a table of contents that doesn’t load skill content.

installation.rs — sources and acquisition

Defines Source (the source = "..."-tagged enum: cargo, github, binary) and acquire_source, which downloads / installs / clones the source and returns an AcquiredSource whose resolve_executable / resolve_script methods turn a relative executable/script name into a concrete path. The Runnable enum (Exec(PathBuf) or Script(PathBuf)) is the final form a hook command resolves to. The git submodule handles GitHub tarball acquisition and caching.

Validates skill group source constraints at parse time: mutual exclusivity of source.path/source.git/source = "crate", and the requirement that source = "crate" has at least one non-wildcard predicate.

crate_metadata.rs — parse Cargo.toml metadata

Parses [package.metadata.symposium] from crate Cargo.toml files. Crate authors embed skill layout metadata so Symposium knows where to find skills (or which other crate to redirect to). Returns SkillSource::Path(subdir) or SkillSource::Crate { name, version } for redirects.

skills.rs — skill resolution and matching

Given a PluginRegistry and workspace dependencies, this module resolves skill group sources (fetching from git if needed), discovers SKILL.md files, and evaluates crate predicates at each level (plugin, group, skill) to determine which skills apply. For source = "crate" groups, resolves predicates to a matched crate set, fetches each crate’s source via RustCrateFetch, reads [package.metadata.symposium] to determine skill paths, and follows redirects recursively with cycle detection and a depth limit of 10.

Each applicable skill carries a SkillOrigin describing where its bytes live, used at sync time for dedup and install-path disambiguation. What matters for identity is the on-disk location of the skill, not which plugin manifest pointed at it — two plugins in the same source pointing at the same skill bundle dedupe.

  • Crate { name, version } — from a crate-source resolution (source = "crate"). Two Crate origins with the same (name, version) are the same logical skill, regardless of which plugin pointed at them.
  • Git { repo, commit_sha, skill_path } — from a source.git group. Identity is the triple (owner/repo, resolved commit SHA, SKILL.md path within the repo tree). Two plugins that pointed at the same repo via different URL forms (root URL vs. tree/<ref>/<subpath>) collapse to one install when they end up loading the same SKILL.md from the same commit; different SKILL.md files within one repo stay distinct.
  • Source { source_name, skill_path } — from a plugin’s source.path group, or from a standalone SKILL.md discovered in a registry source. source_name is the registry source’s display name (e.g. "user-plugins"); skill_path is the SKILL.md’s parent directory relative to the source root (canonicalized first, so ../-laden joins collapse to the same string as a direct standalone walk).

sync prefers the plain <agent-skills-dir>/<skill-name>/ and only falls back to <skill-name>-<origin-disambiguator>/ when needed: when more than one origin claims the same skill name, or when the unsuffixed slot is already occupied by a user-managed directory (one without the .symposium marker). The disambiguator is human-readable for Crate (<crate-name>-<version>) and an 8-hex SHA-256 prefix for the other variants. The .symposium marker, wildcard .gitignore, and stale-cleanup walk all key on the marker file rather than directory name shape, so transitions between unsuffixed and suffixed names self-heal across syncs.

subcommand_dispatch.rs — plugin-vended subcommands

Routes the Commands::External arm of clap’s allow_external_subcommands. find_subcommand walks the PluginRegistry, applying plugin-level and subcommand-level crate predicates against the workspace, and returns the matched (Plugin, Subcommand) (or an error if more than one plugin claims the name). dispatch_external then looks up the named Installation, resolves it via installation::resolve_runnable, and spawns the child with stdio inherited — propagating the exit code as a u8 so callers can convert to ExitCode (binary) or treat non-zero as an error (library). applicable_subcommands is the shared iterator over workspace-applicable plugin subcommands, reused by help rendering.

help_render.rs--help rendering

Renders cargo agents --help as two audience-grouped sections, “Commands for humans” and “Commands for agents”, mixing built-in subcommands with plugin-vended ones filtered by the active workspace. Built-in audience comes from cli::builtin_audience; plugin subcommands come from subcommand_dispatch::applicable_subcommands.

help_text is the single help decision, shared by the binary and the test harness. clap’s own help flag and help subcommand are disabled (in cli::Cli), --help/-h is a manual global bool, and the entry points parse with try_parse_from — so help is decided after parsing and argument order (--help --quiet) is irrelevant. It returns the top-level grouped help for no subcommand / --help / -h / the bare help keyword; for <built-in> --help it re-renders clap’s own per-command help by walking clap’s command tree (so required-arg commands like crate-info, required-subcommand groups like plugin, and nested commands like plugin list all work); a plugin <name> --help returns None so dispatch forwards --help to the child.

render builds the grouped text by slicing clap’s rendered help — keeping the header (before Commands:) and the options block (from Options: on) and hand-rendering only the two section headings between them. If a slice marker is missing (clap format drift), it falls back to clap’s unmodified help rather than panicking.

hook.rs — hook handling

Handles the hook pipeline: parse agent wire-format input → auto-sync → builtin dispatch → plugin hook dispatch → serialize output. Builtin dispatch currently only acts on SessionStart, where handle_session_start composes two independently-computed additionalContext fragments: a discovery_hint (suggests cargo agents --help when the workspace exposes applicable plugin subcommands, reusing subcommand_dispatch::applicable_subcommands) and an update_nudge (the throttled self-update warning); the discovery hint is not gated behind the update-check throttle. The plugin dispatch path matches plugin Hooks against the event, selects the best format for each plugin (native match > symposium > single-other-agent fallback), builds a ResolvedHook per match (looking up the named installations on the plugin), then for each ResolvedHook: acquires its requirements (best-effort), runs install_commands after the source step, picks a Runnable from (hook-or-install) executable/script, and spawns it (binary directly for Exec, via sh <path> for Script). Input is delivered in the selected format; output is converted back to the agent’s wire format before returning.

state.rs — persistent state

Manages state.toml in the config directory. Tracks the semver of the binary that last touched the directory (for future migration hooks) and the timestamp of the last update check (to throttle crates.io queries to once per 24 hours). ensure_current() is called on startup to silently stamp the current version. should_check_for_update() / record_update_check() gate the auto-update flow.

self_update.rs — self-update

Implements cargo agents self-update. Queries the registry for the latest published version via cargo search, then installs it via cargo install symposium --force. Also provides re_exec() which replaces the current process with the newly installed binary (Unix exec, spawn-and-exit on Windows) — used by the auto-update = "on" startup path. Contains maybe_warn_for_update() (sync, for the warn library path) and maybe_check_for_update() (async, for the binary on + re-exec path).

crate_command.rs — crate source lookup

Contains dispatch_crate(), which resolves a crate’s version and fetches its source code. Called by the CLI’s crate-info command. Path dependencies are resolved to their local source directory via WorkspaceCrate.path.

Configuration loading

Directory resolution

User-wide paths are resolved using the directories crate, which handles XDG Base Directory conventions automatically. If XDG environment variables are set, they are respected; otherwise paths fall back to ~/.symposium/.

See the configuration reference for the full resolution table.

Config loading

The user config (~/.symposium/config.toml) is loaded once at startup into the Symposium struct. If the file is missing or empty, defaults are used. If parsing fails, a warning is printed and defaults are used.

Agents

cargo agents supports multiple AI agents. Each agent has its own hook protocol, file layout, and configuration locations. This page documents the agent-specific details that cargo agents needs to handle.

Supported agents

Config nameAgent
claudeClaude Code
copilotGitHub Copilot
geminiGemini CLI
codexCodex CLI
kiroKiro
opencodeOpenCode
gooseGoose

The agent name is stored in [agent] name in either the user or project config.

Agent responsibilities

For each agent, cargo agents needs to know how to:

  1. Register hooks — write the hook configuration so the agent calls cargo-agents hook on the right events.
  2. Install extensions — place skill files (and eventually workflow/MCP definitions) where the agent expects them.

Where these files go depends on whether the agent is configured at the user level or the project level (see sync --agent).

Extension locations

When installing skills, cargo agents prefers vendor-neutral paths where possible:

ScopePathSupported by
Project skills.agents/skills/<skill-name>/SKILL.mdCopilot, Gemini, Codex, OpenCode, Goose
Project skills.claude/skills/<skill-name>/SKILL.mdClaude Code (does not support .agents/skills/)
Project skills.kiro/skills/<skill-name>/SKILL.mdKiro (uses its own path)

At the project level, Claude Code requires .claude/skills/, Kiro requires .kiro/skills/, while Copilot, Gemini, Codex, OpenCode, and Goose all support .agents/skills/. cargo agents uses the vendor-neutral .agents/skills/ path whenever the agent supports it.

At the global level, each agent has its own path:

AgentGlobal skills path
Claude Code~/.claude/skills/<skill-name>/SKILL.md
Copilot(no global skills path)
Gemini~/.gemini/skills/<skill-name>/SKILL.md
Codex~/.agents/skills/<skill-name>/SKILL.md
Kiro~/.kiro/skills/<skill-name>/SKILL.md
OpenCode~/.agents/skills/<skill-name>/SKILL.md
Goose~/.agents/skills/<skill-name>/SKILL.md

Claude Code

Hooks reference · Settings reference · Skills reference

Hook registration

Claude Code hooks live under the "hooks" key in settings JSON files. Each event maps to an array of matcher groups, each containing an array of hook commands.

ScopeFile
Global~/.claude/settings.json
Project (shared).claude/settings.json
Project (personal).claude/settings.local.json

Example hook registration:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "cargo-agents hook claude pre-tool-use"
          }
        ]
      }
    ]
  }
}

Supported events

Claude Code supports many hook events. The ones relevant to Symposium are:

EventDescription
PreToolUseBefore a tool is invoked. Can allow, block, or modify the tool call.
PostToolUseAfter a tool completes. Used to track skill activations.
UserPromptSubmitWhen the user submits a prompt. Used for skill nudges.

Other events include SessionStart, Stop, Notification, SubagentStart, and more.

Hook payload/output

Claude Code wraps hook-specific fields in a nested hookSpecificOutput object:

{
  "continue": true,
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "additionalContext": "...",
    "updatedInput": "..."
  }
}

GitHub Copilot

Hooks reference · Using hooks (CLI) · Skills reference

Hook registration

Copilot hooks are defined in JSON files with a version field. Hook entries use platform-specific command keys (bash, powershell) rather than a single command field.

ScopeFile
Global~/.copilot/config.json (under hooks key)
Project.github/hooks/*.json

Example hook registration:

{
  "version": 1,
  "hooks": {
    "preToolUse": [
      {
        "type": "command",
        "bash": "cargo-agents hook copilot pre-tool-use",
        "timeoutSec": 10
      }
    ]
  }
}

Note: Copilot uses camelCase event names (preToolUse), unlike Claude Code’s PascalCase (PreToolUse).

Supported events

EventDescription
preToolUseBefore a tool is invoked. Can allow, deny, or modify tool args.
postToolUseAfter a tool completes.
sessionStartNew session begins. Supports command and prompt types.
sessionEndSession completes.
userPromptSubmittedWhen the user submits a prompt.
errorOccurredWhen an error occurs.

Hook payload/output

Copilot uses a flat output structure (no nested hookSpecificOutput). The input payload has toolName and toolArgs (where toolArgs is a JSON string that must be parsed separately):

{
  "permissionDecision": "allow",
  "permissionDecisionReason": "...",
  "modifiedArgs": { ... },
  "additionalContext": "..."
}

Valid permissionDecision values: "allow", "deny", "ask".


Gemini CLI

Hooks reference · Configuration reference · Skills reference · Extensions reference

Hook registration

Gemini CLI hooks live under the "hooks" key in settings.json. Hook groups use regex matchers for tool events and exact-string matchers for lifecycle events.

ScopeFile
Global~/.gemini/settings.json
Project.gemini/settings.json

Example hook registration:

{
  "hooks": {
    "BeforeTool": [
      {
        "matcher": ".*",
        "hooks": [
          {
            "name": "symposium",
            "type": "command",
            "command": "cargo-agents hook gemini pre-tool-use",
            "timeout": 10000
          }
        ]
      }
    ]
  }
}

Note: Gemini uses BeforeTool (not PreToolUse), and timeouts are in milliseconds (default: 60000).

Supported events

EventTypeDescription
BeforeToolToolBefore a tool is invoked.
AfterToolToolAfter a tool completes.
BeforeToolSelectionToolBefore the LLM selects tools.
BeforeModelModelBefore LLM requests.
AfterModelModelAfter LLM responses.
BeforeAgentLifecycleBefore agent loop starts.
AfterAgentLifecycleAfter agent loop completes.
SessionStartLifecycleWhen a session starts.
SessionEndLifecycleWhen a session ends.
PreCompressLifecycleBefore history compression.
NotificationLifecycleOn notification events.

Hook payload/output

Gemini uses a structure similar to Claude Code, with a nested hookSpecificOutput:

{
  "decision": "allow",
  "reason": "...",
  "hookSpecificOutput": {
    "hookEventName": "BeforeTool",
    "additionalContext": "...",
    "tool_input": { ... }
  }
}

The input payload includes tool_name, tool_input, mcp_context, session_id, and transcript_path.


Kiro

Hooks reference

Hook registration

Kiro hooks live in agent JSON files under .kiro/agents/. Symposium creates a symposium.json agent file with its hooks. Kiro uses camelCase event names.

ScopeFile
Global~/.kiro/agents/symposium.json
Project.kiro/agents/symposium.json

Example hook registration:

{
  "hooks": {
    "preToolUse": [
      {
        "matcher": "*",
        "command": "cargo-agents hook kiro pre-tool-use"
      }
    ],
    "agentSpawn": [
      {
        "command": "cargo-agents hook kiro session-start"
      }
    ]
  }
}

Kiro uses a flat entry format: each entry has command directly (and optional matcher), with no nested hooks array or type field.

Supported events

EventDescription
preToolUseBefore a tool is invoked. Can block (exit code 2).
postToolUseAfter a tool completes.
userPromptSubmitWhen the user submits a prompt.
agentSpawnSession starts (maps to session-start internally).
stopAgent finishes.

Hook payload/output

Kiro uses exit-code-based control flow:

  • Exit 0: stdout captured as additional context
  • Exit 2: block (preToolUse only), stderr as reason
  • Other: warning, stderr shown

Input includes hook_event_name, cwd, tool_name, and tool_input on stdin as JSON.

Unregistration

Unregistration deletes the symposium.json file.


Codex CLI

Hooks reference

Hook registration

Codex CLI hooks live in hooks.json files. The structure is similar to Claude Code — nested matcher groups with hook command arrays. Codex uses PascalCase event names and timeout in seconds.

ScopeFile
Global~/.codex/hooks.json
Project.codex/hooks.json

Example hook registration:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "",
        "hooks": [{
          "type": "command",
          "command": "cargo-agents hook codex pre-tool-use",
          "timeout": 10
        }]
      }
    ]
  }
}

Note: An empty matcher string matches everything in Codex (equivalent to "*" in other agents).

Supported events

EventDescription
PreToolUseBefore a tool is invoked. Can block.
PostToolUseAfter a tool completes. Can stop session (continue: false).
UserPromptSubmitWhen the user submits a prompt.
SessionStartSession starts or resumes.
StopAgent turn completes.

Hook payload/output

Codex uses a protocol similar to Claude Code, with two methods to block:

  1. JSON output: { "decision": "block", "reason": "..." }
  2. Exit code 2 with reason on stderr

Also supports hookSpecificOutput with additionalContext, and { "continue": false } to stop the session.

Input includes session_id, cwd, hook_event_name, model, turn_id, tool_name, tool_use_id, and tool_input.


OpenCode

Hooks reference

Hook registration

OpenCode does not support shell-command hooks. Its extensibility is based on TypeScript/JavaScript plugins. Symposium cannot register hooks for OpenCode.

Supported events

OpenCode’s plugin system supports these events, but Symposium does not currently bridge them:

OpenCode eventSymposium eventDescription
tool.execute.beforepre-tool-useBefore a built-in tool runs. Can block by throwing Error, or mutate output.args.
tool.execute.afterpost-tool-useAfter a built-in tool completes.
message.updateduser-prompt-submitFiltered to role === "user" messages.
session.createdsession-startWhen a new session begins.

Goose

Hooks reference

Hook registration

Goose does not implement lifecycle hooks. It uses MCP extensions for extensibility. symposium cannot register hooks for Goose.

Goose is supported as a skills-only agent — cargo agents sync will install skill files in the vendor-neutral .agents/skills/ path.


Cross-agent event mapping

The following table maps symposium’s internal event names to each agent’s wire-format event name. means the agent does not support shell-command hooks.

Symposium eventClaudeCopilotGeminiCodexKiroOpenCodeGoose
pre-tool-usePreToolUsepreToolUseBeforeToolPreToolUsepreToolUse
post-tool-usePostToolUsepostToolUseAfterToolPostToolUsepostToolUse
user-prompt-submitUserPromptSubmituserPromptSubmittedBeforeAgentUserPromptSubmituserPromptSubmit
session-startSessionStartsessionStartSessionStartSessionStartagentSpawn

Adding a new agent

To add support for a new agent:

  1. Add a variant to the HookAgent enum in hook_schema.rs.
  2. Create an agent module (e.g., hook_schema/newagent.rs) implementing the Agent trait and the event-specific payload/output types.
  3. Implement the AgentHookPayload and AgentHookOutput traits to convert between the agent’s wire format and the internal HookPayload/HookOutput types.
  4. Document the agent’s hook registration locations and extension file layout in this page.

State

Documents the kind of state maintained by the Symposium agent.

Session state

Session state (activations, nudge history, prompt count) is persisted as JSON files at ~/.symposium/sessions/<session-id>.json, so state survives across hook invocations within a single coding session.

Hooks

Symposium’s hook system is guided by the project tenets: symposium is always the intermediary, it never dirties the user’s repo, and portability is the default.

Hook formats

A plugin hook declares which wire format its handler expects:

  • format = "symposium" (default) — the handler receives symposium canonical JSON. This is portable across all agents.
  • format = "claude" / "copilot" / "gemini" / "codex" / "kiro" — the handler receives that agent’s native wire format.

Dispatch rule

When symposium’s global handler receives an event from agent A, it loads all plugins and finds hooks matching the event. For each plugin, it picks at most one hook to deliver:

  1. If the plugin declares a hook with format matching agent A → deliver the input unmodified (the handler already expects this agent’s native format).
  2. Otherwise, if the plugin declares a symposium-format hook → convert to symposium canonical and deliver.
  3. Otherwise → nothing fires for this plugin.

Symposium never converts between agent-specific formats. A format = "claude" hook will only fire on Claude — it won’t be translated for Copilot or Gemini. If you want cross-agent coverage, provide a symposium-format hook as a fallback.

Example

A plugin with hooks for claude, gemini, and symposium:

  • On Claude: the format = "claude" hook receives Claude’s native JSON.
  • On Gemini: the format = "gemini" hook receives Gemini’s native JSON.
  • On Copilot: no native handler → the format = "symposium" hook receives symposium canonical JSON.

A plugin with only format = "symposium":

  • Works on all agents. Symposium converts the agent’s wire format to canonical before delivering.

A plugin with only format = "claude":

  • On Claude: receives Claude’s native JSON directly.
  • On other agents: nothing fires (no symposium fallback declared).

Output handling

Symposium converts the hook’s output back to the current agent’s wire format before returning it to the agent:

  • Native format matching the host agent → pass through directly.
  • Symposium format → convert to host agent’s wire format.

Alternatives considered

Registering agent-specific hooks directly

An earlier design had symposium write plugin hook commands directly into agent configuration files (e.g., .claude/settings.json, .github/hooks/*.json) at sync time. The agent would invoke them natively, and symposium’s global handler would skip delivery for those plugins.

We rejected this because it violates the Unobtrusive tenet:

  • Agent config files are often git-tracked. Writing plugin hooks into them creates unexpected diffs that pollute pull requests and cause merge conflicts.
  • Users would need to .gitignore symposium-managed entries, or accept noise in their version history.
  • It couples symposium’s state to files the user considers “theirs,” making it harder to adopt or remove symposium cleanly.

The current design avoids these problems by keeping symposium as the sole registered hook handler. Plugin hooks are dispatched internally — the agent’s config only ever contains one symposium entry, registered at init time.

Cross-agent format conversion

We also considered converting between agent-specific formats (e.g., delivering a format = "claude" hook on Copilot by translating Copilot’s input into Claude’s format). We rejected this because:

  • The conversion is lossy — agents have different fields, semantics, and capabilities.
  • It creates surprising behavior: a hook author declares format = "claude" expecting Claude’s schema, but receives a synthetic approximation on other agents.
  • It’s simpler and more predictable to require a symposium-format fallback for cross-agent coverage.

Subcommands

A subcommand is a top-level cargo agents <name> command vended by a plugin. Subcommands are the fourth thing a plugin can contribute, alongside skills, hooks, and MCP servers. Where skills and MCP servers extend the agent’s surface, subcommands extend cargo agents itself, exposing crate-aware tooling that runs on the user’s machine.

The motivating use cases:

  • A crate ships its own analysis binary alongside the library. The crate author wants cargo agents <name> … to be a discoverable entry point for agents working in projects that depend on that crate, rather than requiring users to install and remember a separate CLI.
  • crate-info is moved out of the built-in CLI into a first-party plugin, shrinking the static command surface.
  • A [subcommand.<name>] named after the crate is the expected convention, but is not enforced.

Relationship to [[installations]]

Subcommands reuse the installation framework introduced for hooks. An installation declares how to acquire a binary or script (cargo install with binstall fast-path, github clone, or a path on disk), where it caches, and which executable or script to run. Subcommands reference installations by name, or declare them inline — the same shape hooks use.

This means a plugin author writes installation logic once and shares it across hooks and subcommands. Symposium owns acquisition, caching, idempotency, and post-install setup; subcommands only own dispatch.

Manifest schema

name = "demo-plugin"
crates = ["example-crate"]

[[installations]]
name = "example-tool"
source = "cargo"
crate = "example-tool"
executable = "example-tool"
args = ["serve"]

[subcommand.demo]
description = "Run the demo tool"
audience = "agents"
command = "example-tool"
FieldTypeRequiredDescription
descriptionstringyesShown in cargo agents --help. Capped at 1024 chars.
audience"humans" | "agents"no, defaults to "agents"Controls grouping in cargo agents --help.
commandstring or tableyesA string names an [[installations]] entry; a table is an inline installation, promoted to a synthetic entry named after the subcommand. Same shape as [[hooks]].command.
cratesstring or arraynoSubcommand-level crate predicate, AND-combined with the plugin-level crates.

Reserved names that cannot be used as subcommand keys: init, sync, hook, plugin, crate-info, help. A plugin cannot shadow a built-in.

The TOML key is singular ([subcommand.<name>]), matching the natural read of a TOML table. The internal field on Plugin is plural (subcommands).

Inline form

For one-off subcommands the inline form avoids a separate [[installations]] block:

[subcommand.demo]
description = "..."
command = { source = "cargo", crate = "example-tool", executable = "example-tool", args = ["serve"] }

The inline table is promoted to a synthetic installation named after the subcommand and resolved through the same pipeline.

Pass-through contract

Symposium does not own the subcommand’s argument grammar. The plugin’s binary owns its own --help, validation, and exit codes. What symposium contributes is mechanical:

  1. Name registration and lookup.
  2. Workspace-aware filtering (the subcommand only appears for projects matching the plugin’s crate predicates).
  3. A short description shown in cargo agents --help.
  4. Resolution of command through the installation pipeline to a concrete (executable, base_args).
  5. Forwarding the user’s trailing CLI args verbatim, appended after the installation’s args.

This boundary keeps the manifest small, keeps plugins authoritative about their CLI, and avoids inventing a symposium-specific options DSL that would drift from each plugin’s real interface.

Dispatch

cargo-agents’s top-level CLI uses clap’s allow_external_subcommands: unknown subcommands are not errors but are routed to a catch-all variant. The binary then:

  1. Loads the plugin registry and the active workspace’s crates.
  2. Walks active plugins for one whose subcommands map contains the typed name and whose subcommand-level crates predicate (if any) also matches.
  3. Resolves the subcommand’s command through the installation pipeline — acquiring the binary if it isn’t already cached, running any install_commands, processing requirements.
  4. Execs the resolved (executable, base_args ++ user_args), inheriting stdio.
  5. Returns the child’s exit code as the cargo agents exit code. A signal-killed child becomes a generic failure.

Argument forwarding uses a structured Vec, not sh -c. User-supplied argv is preserved exactly — spaces, quotes, and shell metacharacters in args are not re-tokenized. This matters more for subcommands than for hooks (whose input arrives over stdin as JSON).

Script-mode installations (script = "...") are still invoked through sh <path>, mirroring hook dispatch. That path is not cross-platform on Windows and is tracked as a follow-up for both hooks and subcommands; in the meantime, plugin authors who need Windows support should use executable = "..." instead.

If no plugin matches the typed name, dispatch fails with a clear error pointing to cargo agents --help. If a matching subcommand exists but installation fails, the installation layer’s error is propagated as-is.

Workspace filtering

Plugin filtering is workspace-aware in two places: help rendering and dispatch.

Inside a Cargo workspace. Symposium reads the workspace’s resolved dependencies. A subcommand appears in cargo agents --help and is dispatchable only if both the plugin-level and subcommand-level crates predicates match. Built-in subcommands always appear.

Outside a Cargo workspace (no discoverable Cargo.toml upward). Only built-ins and plugins with crates = ["*"] appear. Invoking a crate-specific subcommand from outside a workspace produces an error explaining which crate it needs.

This rule keeps cargo agents --help outside a workspace limited to globally-applicable commands, rather than listing every installed plugin.

Help text grouping

cargo agents --help is rendered in two sections:

  • Commands for humans — operational commands a user runs themselves: init, plugin, self-update, sync, plus any plugin-vended subcommand with audience = "humans".
  • Commands for agents — discovery and analysis tools for the agent to invoke: crate-info and plugin-vended subcommands with audience = "agents" (the default).

The default of audience = "agents" reflects the expected shape of plugin-vended commands: most are analysis or context-fetching tools surfaced to agents, not workflows for humans. The exceptional case explicitly opts in.

For this grouping to be useful, crate-info is no longer hidden — it’s a discoverable agent tool. hook remains hidden; it’s an internal protocol entry point, not an end-user surface.

The renderer reads the active plugin registry filtered by workspace, so the help output adapts to the project the user is standing in.

--help, -h, the bare help keyword, and an empty invocation are intercepted after clap parses and routed to this renderer; help is never listed as its own command. A <built-in> --help instead shows that command’s own help (re-rendered from clap), and a plugin-vended <name> --help is forwarded to the plugin’s binary, which owns its --help.

Agent discovery

cargo agents --help is a pull surface — an agent only sees the crate-aware subcommands if it already knows to run it. To push that affordance, the built-in SessionStart hook injects a one-line hint suggesting cargo agents --help whenever the active workspace exposes at least one applicable plugin-vended subcommand. The trigger reuses the same workspace-filtered set as the help renderer (applicable_subcommands), so the hint stays silent in projects with nothing to discover.

The hint shares SessionStart’s additionalContext with the update nudge; each fragment is computed independently, and the discovery hint is not gated behind the update-check throttle. Agents without hook registration (OpenCode, Goose) don’t receive it; for them cargo agents --help is the only discovery surface.

Audience as metadata, not enforcement

audience controls help-text grouping only. It does not gate dispatch. A user can type cargo agents <agent-audience-subcommand> directly and it will run. The intent is to keep the discovery surface uncluttered for humans, not to lock anyone out.

Conflict resolution

Two plugins may declare the same subcommand name. Rather than silently picking one, dispatch fails with an error listing every plugin that defined the name, leaving the user to disambiguate (typically by tightening one of the plugin’s crates predicates or removing one of the plugin sources).

The strict-error stance trades silence for clarity: subcommand names tend to mirror crate names (which are unique on crates.io), so a collision usually signals a real configuration mistake rather than an intended override.

Namespacing (cargo agents <plugin>:<name>) is not implemented; it can be revisited if a real conflict pattern emerges.

What plugins own vs. what symposium owns

ConcernOwned by
Subcommand nameManifest
Short descriptionManifest
audienceManifest
Argument grammar, flags, <subcommand> --helpPlugin’s binary
Argument validationPlugin’s binary
Exit codesPlugin’s binary, propagated by symposium
Binary acquisition, caching, post-install setupShared installation framework
Workspace-aware filteringSymposium
Resolution of command(executable, args)Shared installation framework
Stdio forwardingSymposium (inherited)
cargo agents --help renderingSymposium
Conflict resolutionSymposium

Important flows

This section describes the logic of each cargo agents command.

Crate-sourced skill resolution

When a skill group uses source = "crate", the sync flow takes an additional path:

  1. predicate::union_matched_crates() resolves plugin-level and group-level predicates against the workspace to produce a set of concrete crate name/version pairs.
  2. For each crate in the set, RustCrateFetch fetches the source — checking path overrides (for local path deps), then the cargo registry cache, then crates.io.
  3. crate_metadata::parse_crate_metadata() reads [package.metadata.symposium] from the crate’s Cargo.toml:
    • No metadata — fall back to the default skills/ subdirectory.
    • skills = [] — no skills from this crate.
    • path = "..." entries — scan that subdirectory for skills.
    • crate = { name, version? } entries — redirect: fetch the target crate and follow its metadata recursively (with cycle detection and a depth limit of 10).
  4. discover_skills() scans each resolved directory for SKILL.md files.

The key code paths are in skills.rs (load_crate_skills, fetch_and_resolve_skills), crate_metadata.rs (parse_crate_metadata), predicate.rs (matched_crates, union_matched_crates), and crate_sources/mod.rs (RustCrateFetch, WorkspaceCrate).

Help rendering

cargo agents --help (and -h, the bare help keyword, or no subcommand) is rendered by help_render, not by clap’s default help.

  1. The binary and the test harness parse argv with Cli::try_parse_from, then call help_render::help_text(parse, args, sym, cwd). Because the decision happens after parsing, argument order (--help --quiet) does not matter and there is no second argv parser to keep in sync.
  2. For no subcommand, --help/-h, or the bare help keyword, help_text returns the top-level grouped help: render slices clap’s own rendered help (header + options block) and hand-renders “Commands for humans” / “Commands for agents” between them, mixing built-ins (cli::builtin_audience) with workspace-filtered plugin subcommands (subcommand_dispatch::applicable_subcommands).
  3. For <built-in> --help, help_text re-renders clap’s per-command help by walking clap’s command tree to the named subcommand — so required-arg commands (crate-info), required-subcommand groups (plugin), and nested commands (plugin list) all work even though clap’s auto help flag is disabled.
  4. A plugin-vended <name> --help is left alone: help_text returns None, and dispatch forwards --help to the child binary, which owns its own help.

clap’s auto help flag and help subcommand are disabled in cli::Cli; --help/-h is a manual global bool. The key code paths are in help_render.rs (help_text, render, subcommand_help), cli.rs (builtin_audience, the Cli flags), and bin/cargo-agents.rs plus symposium-testlib (the parse-then-help_text wiring).

Subcommand dispatch

When the user runs cargo agents <name> for a name not built into the binary, clap’s allow_external_subcommands routes it to Commands::External(argv).

  1. The binary (or library cli::run) calls subcommand_dispatch::dispatch_external(sym, cwd, argv).
  2. find_subcommand walks the plugin registry. For each plugin it applies the plugin-level crates predicate against the workspace, then looks up argv[0] in plugin.subcommands. If the entry has its own crates predicate, that must also match. Two or more matches → error.
  3. The matched subcommand’s command field names an Installation on the same plugin. installation::resolve_runnable acquires the source if any, runs install_commands, and picks the Runnable (Exec for binaries, Script for shell scripts).
  4. The child is spawned with stdio inherited. Its exit code is collapsed to a u8 — the binary wraps it in ExitCode::from; the library treats non-zero as an error so the test harness can assert on success/failure.

The key code paths are in subcommand_dispatch.rs, cli.rs (the External arm), and bin/cargo-agents.rs (binary-side wrapping that surfaces the numeric exit code to the OS).

cargo agents init

Sets up the user-wide configuration.

Flow

  1. Prompt for agents — ask which agents the user uses (e.g., Claude Code, Copilot, Gemini). Multiple agents can be selected.

  2. Write user config — create ~/.symposium/config.toml with the [[agent]] entries populated:

    [[agent]]
    name = "claude"
    
    [[agent]]
    name = "gemini"
    
  3. Register hooks — register global hooks and MCP servers for each selected agent. Also unregisters hooks for any agents that were removed.

If --add-agent or --remove-agent flags are provided, the interactive prompt is skipped and the specified changes are applied to the existing agent list.

cargo agents sync

Scans workspace dependencies, installs applicable skills into agent directories, and cleans up stale skills.

Flow

  1. Find workspace root — run cargo metadata to locate the workspace manifest directory.

  2. Load plugin sources — read the user config’s [[plugin-source]] entries and load their plugin manifests. For git sources, fetch/update as needed.

  3. Scan dependencies — read the full dependency graph from the workspace.

  4. Match skills to dependencies — for each plugin, parse SKILL.md YAML frontmatter, reject malformed or non-string metadata, warn about skipped invalid skills, then evaluate skill group crate predicates and individual skill crates frontmatter against the workspace dependencies.

  5. Install skills per agent — for each configured agent:

    • Copy applicable SKILL.md files into the agent’s expected skill directory.
    • Drop a .symposium marker file into each installed skill directory so future syncs (and other tools) can recognize it as symposium-managed.
    • For every skill directory symposium creates along the way (the skill directory itself or its skills/ parent), write a .gitignore containing a single * so symposium-managed files stay out of version control.
  6. Propagate user-authored skills (agents-syncing) — if agents-syncing is enabled in the user config, mirror skills the user placed in <workspace>/.agents/skills/ into each configured agent’s own skill directory. A skill is “user-authored” when its directory contains SKILL.md but lacks the .symposium marker (symposium never writes markers into source skills). Propagated destinations receive the same marker and * .gitignore as plugin-installed skills, so they participate in the normal stale-skill reap: removing the source — or disabling agents-syncing — causes the destinations to be cleaned up on the next sync. A destination directory without a marker is user-managed and is never overwritten.

  7. Reap stale skills — across every known agent’s skills parent directory, remove any subdirectory that contains the .symposium marker but wasn’t installed this sync. Directories without the marker (user-managed) are left untouched.

  8. Register hooks — ensure symposium’s global hook handler and MCP servers are registered for all configured agents. Unregister hooks for agents no longer in the config. Only symposium’s own handler is registered (e.g., cargo-agents hook claude pre-tool-use) — individual plugin hooks are never written into agent configs. See Hooks for the dispatch model.

Marker file

Each skill directory symposium installs contains an empty .symposium file. Cleanup walks every agent’s skills parent directory (.claude/skills/, .agents/skills/, .kiro/skills/, .gemini/skills/) and reaps any subdirectory whose marker is present but which wasn’t installed this sync. This lets symposium reclaim stale skills (including those left behind by agents removed from the config) without touching user-managed skills, which are identified by the absence of the marker.

Gitignore

Each skill directory symposium creates (and its skills/ parent if new) receives a .gitignore containing just *. Pre-existing directories are left alone. The wildcard also hides the marker file and the gitignore itself, so git status stays clean.

Auto-sync

When auto-sync = true is set in the user config, the hook handler runs sync automatically during agent sessions. This keeps skills in sync as dependencies change.

cargo agents hook

Entry point invoked by the agent’s hook system on session events.

Flow

  1. Auto-sync (if enabled) — when auto-sync = true in the user config, runs cargo agents sync to ensure skills are current. The workspace root is resolved from the payload’s cwd field; if the payload does not include a working directory, the process’s current working directory is used as a fallback. Runs quietly and non-fatally — failures are logged but don’t block hook dispatch.

  2. Built-in dispatch — symposium’s own handling, before plugin hooks. Currently only SessionStart produces output; PreToolUse, PostToolUse, and UserPromptSubmit are no-ops. On SessionStart two fragments are computed independently and, when present, joined into one additionalContext:

    • Discovery hint — when the active workspace exposes plugin-vended subcommands (the same workspace-filtered set listed by cargo agents --help), a line suggesting the agent run cargo agents --help to find them. Computed independently of the update-check throttle, so it fires whenever there is something to discover.
    • Update nudge — when auto-update = "warn", the 24-hour check throttle has elapsed, and the registry reports a newer version: a line suggesting cargo agents self-update.

    Agents without hook registration (OpenCode, Goose) never receive this; for them the only discovery surface is cargo agents --help itself.

  3. Dispatch to plugin hooks — for each enabled plugin that defines a hook handler for the incoming event:

    • Select format: for each plugin, pick the best hook to deliver (see Hooks for priority rules). If the plugin has a hook matching the current agent’s format, deliver the input unmodified. Otherwise deliver in symposium canonical format (or convert to the declared format if only one non-symposium hook exists).
    • Acquire and run:
      • Ensure any requirements for the hook are acquired (on-demand, best-effort).
      • Resolve the hook’s command (a named installation reference or inline declaration) into a runnable form:
        • If the installation declares a source, acquire it (install / cache / clone) and resolve the executable / script against the cached location.
        • If no source, the executable / script is taken as a path on disk.
        • Run the installation’s install_commands (post-source) before invoking the runnable.
        • Spawn path args… directly for Exec, or sh path args… for Script.
      • Pass the event JSON (in the selected format) on stdin to the plugin’s hook.
      • Collect output from each handler.
      • Convert output back to the agent’s wire format.
      • Merge results (e.g., allow/block decisions, output text) across all handlers.
      • Return the merged result to the agent.

Plugin hooks can respond to agent-specific events (e.g., pre-tool-use, post-tool-use, user-prompt-submit for Claude Code). The available events depend on which agent is in use.

Running tests

Quick start

cargo test              # simulation + configured agents

By default, cargo test runs simulation tests and then re-runs agent-mode tests against each agent listed in test-agents.toml. On a fresh clone (no file), the defaults are claude-sdk and kiro-cli-acp.

Configuring test agents

Create test-agents.toml in the repo root (gitignored):

# Run against these agents. Use `acpr --list` to see ACP registry agents.
test-agents = ["claude-sdk"]

Set to [] to skip agent tests entirely (used in CI):

test-agents = []

Available agent names:

NameBackendNotes
claude-sdkClaude Agent SDK (Python)Requires uv + ANTHROPIC_API_KEY
kiro-cli-acpKiro CLI via ACPRequires kiro-cli in PATH
Any name from acpr --listACP registry via acprAuto-downloaded

Filtering to a single agent

Override with the SYMPOSIUM_TEST_AGENT env var:

SYMPOSIUM_TEST_AGENT=kiro-cli-acp cargo test --test hook_agent

This ignores test-agents.toml and runs only the specified agent.

Running specific test files

cargo test --test hook_agent       # just the agent integration tests
cargo test --test init_sync        # just the init/sync tests
cargo test --test dispatch         # just the CLI dispatch tests

Debugging test failures

Add --nocapture to see test output (agent messages, hook traces):

cargo test --test hook_agent -- --nocapture

On failure, the test’s temporary directory is preserved and its path is printed to stderr so you can inspect the fixture state.

Writing tests

Symposium tests run in two modes:

  • Simulation mode — hooks and CLI calls are invoked directly by the harness. No real agent needed.
  • Agent mode — a real agent session processes prompts and we verify it triggers the expected hooks.

Tests declare which mode they need via TestMode:

  • TestMode::SimulationOnly — runs once in simulation.
  • TestMode::AgentOnly — runs once per configured test agent.
  • TestMode::Any — runs once in simulation + once per configured agent.

1. Create your setup by composing fixtures

Wrap your test in with_fixture, specifying the mode and fixtures:

#![allow(unused)]
fn main() {
use symposium_testlib::{TestMode, with_fixture};

#[tokio::test]
async fn my_test() {
    with_fixture(TestMode::SimulationOnly, &["plugins0"], async |mut ctx| {
        // test body
        Ok(())
    }).await.unwrap();
}
}

Fixtures are directories under tests/fixtures/. They are overlaid into a tempdir:

#![allow(unused)]
fn main() {
with_fixture(TestMode::SimulationOnly, &["plugins0", "workspace0"], async |mut ctx| { ... })
}

with_fixture scans fixtures for config.toml (user config dir) and Cargo.toml (workspace root). In agent mode it automatically runs init --add-agent and sync.

For TestMode::Any and TestMode::AgentOnly, the test closure runs once per configured agent.

Variable expansion in fixtures

Text files have variables expanded when copied:

  • $TEST_DIR — the tempdir root.
  • $BINARY — path to the cargo-agents binary.

Fixture requirements

All fixture config.toml files must include hook-scope = "project" so that hooks are installed into the project directory rather than globally.

2. Write the test body

Bimodal tests (TestMode::Any)

Use ctx.prompt_or_hook which dispatches based on mode:

#![allow(unused)]
fn main() {
with_fixture(TestMode::Any, &["plugins0", "project-plugins0"], async |mut ctx| {
    let result = ctx
        .prompt_or_hook("Say hello", &[HookStep::session_start()], HookAgent::Claude)
        .await?;

    assert!(!result.hooks.is_empty());
    assert!(result.has_context_containing("symposium start"));
    Ok(())
}).await.unwrap();
}

In agent mode, prompt_or_hook also asserts that the expected hook events appear in the trace.

Agent-only tests (TestMode::AgentOnly)

Use ctx.prompt to send prompts to the real agent:

#![allow(unused)]
fn main() {
with_fixture(TestMode::AgentOnly, &["plugin-tokio-weather0", "workspace-empty0"], async |mut ctx| {
    ctx.prompt("Run `cargo add tokio` please!").await?;
    let result = ctx.prompt("Use the tokio-weather skill to answer: ...").await?;
    assert!(result.response.unwrap().contains("MAGIC SENTENCE"));
    Ok(())
}).await.unwrap();
}

Simulation-only tests (TestMode::SimulationOnly)

Use ctx.symposium to invoke the CLI directly:

#![allow(unused)]
fn main() {
with_fixture(TestMode::SimulationOnly, &["plugins0"], async |mut ctx| {
    ctx.symposium(&["init", "--add-agent", "claude"]).await?;
    ctx.symposium(&["sync"]).await?;
    // assert on files...
    Ok(())
}).await.unwrap();
}

Governance

Symposium operates under the Rust Code of Conduct.

Teams

When a contributor has shown enduring interest in the codebase and made multiple non-trivial changes over time, they are invited to join the Symposium maintainers team:

  • Maintainers team
    • Members of this team can review and merge other PRs.
    • Members are expected to attend the regular sync meeting.

Overall project leadership is provided by the core team:

  • Core team
    • Final decision makers
    • Approve releases
    • All members of the core team are also members of the maintainers team

Decisions proceed by consensus at each level; if needed, @nikomatsakis acts as BDFL to resolve contentious questions.

PR disclosure policy

We request PRs answer the questions in our PR template regarding AI use, confidence level, and questions.

PR merge policy

We want to keep moving quickly, especially in this early phase, therefore we have established the following review policy:

CategoryPolicy
New contributorsPRs need review from a maintainer
Maintainer team memberPRs should be reviewed by another maintainer before landing
Core team memberReview by another maintainer is preferred but not required

Sync meeting

We hold a regular sync meeting to discuss recent changes, plans, and direction. The meeting is open to all maintainers and contributors. If you’re interested in attending, reach out to a core team member on Zulip.

Releases

Merging a release PR is coordinated among core team members.

Getting involved

The best way to get started is to pick up an issue, open a PR, and join the conversation on Zulip. Landing non-trivial contributions and attending the sync meeting is the path to joining the devs team.

Common issues

Known hook implementation gaps

The following issues were identified by auditing our hook implementations against the agent reference docs (md/design/agent-details/). They don’t cause crashes (the fallback path handles events without agent-specific handlers) but mean some features are incomplete.

toolArgs not parsed (Copilot)

Copilot sends toolArgs as a JSON string (not an object). Our CopilotPreToolUsePayload declares it as serde_json::Value and passes it through as-is in to_hook_payload(). Downstream code expecting structured tool args will get a raw string. Should parse the JSON string into a Value during conversion.

permissionDecision dropped (Copilot)

CopilotPreToolUseOutput::from_hook_output() never maps permissionDecision or permissionDecisionReason from the builtin hook output. If a builtin handler wants to deny a tool call, the decision is silently lost in Copilot output.

Gemini SessionStart matcher

ensure_gemini_hook_entry uses "matcher": ".*" for all events including SessionStart. Per the Gemini reference, lifecycle events use exact-string matchers, not regex. Likely harmless in practice since ".*" matches anything.

Agent details

Disclaimer: These documents reflect our current understanding of each agent’s hook system and extensibility surface. They are maintained as working references for symposium development, not as a substitute for each project’s official documentation. Details may be outdated or incomplete — always consult the primary sources linked in each agent’s page.

For each agent it supports, symposium needs to know:

  1. Hook registration — where and how to write config so the agent calls cargo-agents hook
  2. Hook I/O protocol — event names, input/output field names, exit code semantics
  3. Extension installation — where skill files go (project and global)
  4. Custom instructions — where the agent reads project-level instructions

The tables below summarize the answers for each agent. Individual agent pages contain the full reference. A ? indicates information we have not yet documented.

Hook registration

AgentProject config pathGlobal config pathFormat
Claude Code.claude/settings.json~/.claude/settings.jsonJSON, hooks key with matcher groups
GitHub Copilot.github/hooks/*.json~/.copilot/config.jsonJSON, version: 1 with hooks key
Gemini CLI.gemini/settings.json~/.gemini/settings.jsonJSON, hooks key with matcher groups
Codex CLI.codex/hooks.json~/.codex/hooks.jsonJSON, hooks key with matcher groups
Kiro.kiro/agents/*.json~/.kiro/agents/*.jsonJSON, hooks key in agent config
OpenCode.opencode/plugins/~/.config/opencode/plugins/JS/TS plugins (not shell hooks)
Goose(no hooks)(no hooks)N/A

Command field

AgentCommand fieldPlatform-specific?
Claude CodecommandNo
GitHub Copilotbash / powershellYes
Gemini CLIcommandNo
Codex CLIcommandNo
KirocommandNo
OpenCodeN/A (JS function)N/A
GooseN/AN/A

Timeout defaults

AgentDefault timeoutUnit
Claude Code600seconds
GitHub Copilot30seconds (timeoutSec)
Gemini CLI60,000milliseconds (timeout)
Codex CLI600seconds (timeout or timeoutSec)
Kiro30,000milliseconds (timeout_ms)
OpenCode60,000milliseconds (community hooks plugin)
GooseN/AN/A

Event names

Symposium registers hooks for four events. Each agent uses different names and casing conventions.

Symposium eventClaude CodeCopilotGemini CLICodex CLIKiro CLIOpenCodeGoose
pre-tool-usePreToolUsepreToolUseBeforeToolPreToolUsepreToolUsetool.execute.beforeN/A
post-tool-usePostToolUsepostToolUseAfterToolPostToolUsepostToolUsetool.execute.afterN/A
user-prompt-submitUserPromptSubmituserPromptSubmittedBeforeAgentUserPromptSubmituserPromptSubmitmessage.updated (filter by role)N/A
session-startSessionStartsessionStartSessionStartSessionStartagentSpawnsession.createdN/A

Blocking support

Not all events can block the action in all agents.

AgentPre-tool-use can block?Post-tool-use can block?User-prompt can block?Session-start can block?
Claude CodeYesNoYes (exit 2)No
GitHub CopilotYesNoNoNo
Gemini CLIYesYes (block result)Yes (deny discards message)No
Codex CLIYesYes (continue: false)Yes (continue: false)Yes (continue: false)
KiroYes (exit 2)NoNoNo
OpenCodeYes (throw Error)NoNo (observe only)No (observe only)
GooseN/AN/AN/AN/A

Hook I/O protocol

Input fields (pre-tool-use)

AgentTool name fieldTool args fieldSession/context fields
Claude Codetool_nametool_input (object)session_id, cwd, hook_event_name
GitHub CopilottoolNametoolArgs (JSON string)timestamp, cwd
Gemini CLItool_nametool_input (object)session_id, cwd, hook_event_name, timestamp
Codex CLItool_nametool_input (object)session_id, cwd, hook_event_name, model
Kirotool_nametool_input (object)hook_event_name, cwd
OpenCodetoolargs (mutable output object)sessionID, callID
GooseN/AN/AN/A

Output structure (pre-tool-use)

AgentPermission decision fieldDecision valuesModified input fieldNesting
Claude CodepermissionDecisionallow, deny, ask, deferupdatedInputnested in hookSpecificOutput
GitHub CopilotpermissionDecisionallow, deny, askmodifiedArgsflat
Gemini CLIdecisionallow, denytool_inputnested in hookSpecificOutput
Codex CLIdecision or permissionDecisionblock/deny(not yet implemented)flat or nested hookSpecificOutput
Kiro(exit code only)exit 0 = allow, exit 2 = block(not supported)N/A
OpenCode(throw to block)allow (return) / deny (throw)mutate output.argsJS mutation
GooseN/AN/AN/AN/A

Exit codes

All shell-based agents use the same convention (where applicable):

CodeMeaning
0Success; stdout parsed as JSON
2Block/deny; stderr used as reason
OtherNon-blocking warning, action proceeds

Exceptions: Copilot uses exit 0 = allow, non-zero = deny (no special meaning for exit 2). OpenCode uses JS exceptions, not exit codes.

Extension installation

Skill file paths

AgentProject skills pathGlobal skills path
Claude Code.claude/skills/<name>/SKILL.md~/.claude/skills/<name>/SKILL.md
GitHub Copilot.agents/skills/<name>/SKILL.md(none)
Gemini CLI.agents/skills/<name>/SKILL.md~/.gemini/skills/<name>/SKILL.md
Codex CLI.agents/skills/<name>/SKILL.md~/.agents/skills/<name>/SKILL.md
Kiro.kiro/skills/<name>/SKILL.md~/.kiro/skills/<name>/SKILL.md
OpenCode.agents/skills/<name>/SKILL.md~/.agents/skills/<name>/SKILL.md
Goose(N/A — uses MCP extensions)(N/A)

Symposium uses the vendor-neutral .agents/skills/ path whenever the agent supports it, falling back to agent-specific paths (e.g., .claude/skills/, .kiro/skills/) when required. Codex CLI and OpenCode also support .agents/skills/ natively.

Custom instructions

AgentProject instructionsGlobal instructions
Claude CodeCLAUDE.md, .claude/CLAUDE.md~/.claude/CLAUDE.md
GitHub Copilot.github/copilot-instructions.md, AGENTS.md~/.copilot/copilot-instructions.md
Gemini CLIGEMINI.md (walks up to .git)~/.gemini/GEMINI.md
Codex CLIAGENTS.md (each dir level)~/.codex/AGENTS.md
Kiro.kiro/steering/*.md, AGENTS.md~/.kiro/steering/*.md
OpenCodeAGENTS.md, CLAUDE.md~/.config/opencode/AGENTS.md
Goose.goosehints, AGENTS.md~/.config/goose/.goosehints

MCP server configuration

Relevant if symposium exposes functionality via MCP.

AgentMCP config locationFormat
Claude Code.claude/settings.json (mcpServers key)JSON
GitHub Copilot.vscode/mcp.json (VS Code), ~/.copilot/mcp-config.json (CLI)JSON
Gemini CLI.gemini/settings.json (mcpServers key)JSON
Codex CLI.codex/config.toml / ~/.codex/config.toml (mcp_servers key)TOML
Kiro.kiro/settings/mcp.json, ~/.kiro/settings/mcp.jsonJSON
OpenCodeopencode.json (mcp key)JSON
Goose~/.config/goose/config.yaml (extensions key)YAML

Claude Code Hooks Reference

Disclaimer: This document reflects our current understanding of Claude Code’s hook system. It is a working reference for symposium development, not a substitute for the official docs. Details may be outdated or incomplete — always consult the primary sources.

Primary sources: Hooks reference · Hooks guide · Extending Claude Code

Hooks are user-defined shell commands, HTTP endpoints, or LLM prompts that execute at specific points in the agent lifecycle. They provide deterministic control — actions always happen rather than relying on the model.

Hook Types

TypeDescription
commandShell script; communicates via stdin/stdout/exit codes
httpPOSTs JSON to a URL endpoint; supports header interpolation with $VAR_NAME
promptSingle-turn LLM evaluation returning {ok: true/false, reason}
agentSpawns a subagent with tool access (Read, Grep, Glob) for up to 50 turns

Events

EventTriggerCan block?Matcher target
SessionStartSession begins/resumesNostartup, resume, clear, compact
SessionEndSession terminates (1.5s default timeout)Noclear, resume, logout, etc.
UserPromptSubmitUser submits prompt, before processingYes (exit 2)None
PreToolUseBefore tool callYesTool name regex (Bash, Edit|Write, mcp__.*)
PostToolUseAfter tool succeedsNoTool name regex
PostToolUseFailureTool failsNoTool name regex
PermissionRequestPermission dialog appearsYesTool name regex
PermissionDeniedAuto-mode classifier denialNo (retry: true available)Tool name regex
StopMain agent finishes respondingYesNone
StopFailureTurn ends on API errorNo (output ignored)rate_limit, authentication_failed, etc.
SubagentStartSubagent spawnedNoAgent type
SubagentStopSubagent finishesYesAgent type
NotificationSystem notificationNopermission_prompt, idle_prompt, etc.
TaskCreatedTask createdYes (exit 2 rolls back)None
TaskCompletedTask completedYes (exit 2 rolls back)None
TeammateIdleTeammate about to go idleYesNone
ConfigChangeConfig file changes during sessionYes (except policy_settings)Config source
CwdChangedDirectory changeNoNone
FileChangedWatched file changesNoBasename
WorktreeCreateGit worktree createdYes (non-zero fails)None
WorktreeRemoveGit worktree removedNoNone
PreCompactBefore compactionNomanual, auto
PostCompactAfter compactionNomanual, auto
InstructionsLoadedCLAUDE.md loadedNoLoad reason
ElicitationMCP server requests user inputYesMCP server name
ElicitationResultMCP elicitation resultYesMCP server name

Configuration

Settings merge with precedence (highest first): Managed → Command line → Local → Project → User.

FileScope
Managed policy (MDM, registry, server, /etc/claude-code/)Organization-wide
.claude/settings.local.jsonSingle project, gitignored
.claude/settings.jsonSingle project, committable
~/.claude/settings.jsonAll projects (user)

Configuration structure

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "./validate.sh",
            "if": "Bash(rm *)",
            "timeout": 60,
            "statusMessage": "Validating...",
            "async": false,
            "shell": "bash"
          }
        ]
      }
    ]
  }
}
  • matcher: regex matched against event-specific values (tool name, session source, notification type).
  • if: permission-rule syntax for additional filtering on tool events (e.g., Bash(git *), Edit(*.ts)).

Input Schema (stdin)

Base fields (all events)

{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "permission_mode": "default|plan|auto|bypassPermissions|...",
  "hook_event_name": "string"
}

PreToolUse additions

  • tool_name: string
  • tool_input: object with tool-specific fields (command for Bash, file_path/content for Write, etc.)
  • tool_use_id: string

PostToolUse additions

  • tool_name, tool_input, tool_use_id (same as PreToolUse)
  • tool_response: string (tool output)

Stop additions

  • stop_hook_active: boolean
  • last_assistant_message: string

Output Schema (stdout)

Output is capped at 10,000 characters.

Universal fields

FieldTypeDescription
continuebooleanfalse stops Claude entirely
stopReasonstringMessage for user when continue is false
systemMessagestringWarning shown to user
suppressOutputbooleanOmits stdout from debug log

PreToolUse decision output

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow|deny|ask|defer",
    "permissionDecisionReason": "string",
    "updatedInput": { "command": "safe-cmd" },
    "additionalContext": "string"
  }
}

Decision precedence across parallel hooks: deny > defer > ask > allow. The allow decision does not override deny rules from settings. updatedInput replaces the entire tool input; if multiple hooks return it, the last to finish wins (non-deterministic).

Exit Codes

CodeMeaning
0Success; stdout parsed as JSON
2Blocking error — action blocked, stderr fed to Claude
OtherNon-blocking warning, action proceeds

Execution Behavior

  • All matching hooks run in parallel.
  • Identical handlers deduplicated by command string or URL.
  • Default timeouts: 600s (command), 30s (prompt), 60s (agent), 1.5s (SessionEnd, overridable via CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS).

Environment Variables

VariableDescription
CLAUDE_PROJECT_DIRAbsolute path to project root
CLAUDE_ENV_FILEFile for persisting env vars (SessionStart, CwdChanged, FileChanged only)
CLAUDE_CODE_REMOTE"true" in remote web environments

Enterprise Controls

  • allowManagedHooksOnly: true — blocks user/project/plugin hooks.
  • allowedHttpHookUrls — restricts HTTP hook destinations.
  • disableAllHooks: true — disables everything.
  • PreToolUse deny blocks even in bypassPermissions mode.

MCP Server Registration

In addition to hooks, symposium registers itself as an MCP server in the agent’s settings file. This provides an alternative integration path alongside the hook-based approach.

Configuration structure

The MCP server entry is added under mcpServers in the same settings file used for hooks:

{
  "mcpServers": {
    "symposium": {
      "command": "/path/to/cargo-agents",
      "args": ["mcp"]
    }
  }
}
  • Project-level: .claude/settings.json
  • User-level: ~/.claude/settings.json

Registration is idempotent — if the entry already exists with the correct values, no changes are made. If the entry exists but has stale values (e.g. the binary moved), it is updated in place.

GitHub Copilot Hooks Reference

Disclaimer: This document reflects our current understanding of GitHub Copilot’s hook system. It is a working reference for symposium development, not a substitute for the official docs. Details may be outdated or incomplete — always consult the primary sources.

Primary sources: About hooks · Using hooks · Hooks configuration

GitHub Copilot hooks are available for the Cloud Agent (coding agent), Copilot CLI (GA February 2026), and VS Code (8-event preview). The system is command-only and repository-native.

Hook Types

Only type: "command" is supported.

Events

Cloud Agent and CLI (6 events, lowerCamelCase)

EventTriggerCan block?
sessionStartNew or resumed sessionNo
sessionEndSession completes or terminatesNo
userPromptSubmittedUser submits a promptNo
preToolUseBefore tool callYes
postToolUseAfter tool completes (success or failure)No
errorOccurredError during agent executionNo

VS Code (8 events, PascalCase, preview)

SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PreCompact, SubagentStart, SubagentStop, Stop.

Only preToolUse/PreToolUse can make access-control decisions. All other events are observational.

Configuration

Cloud Agent and CLI

Hooks defined in .github/hooks/*.json. For the Cloud Agent, files must be on the repository’s default branch.

{
  "version": 1,
  "hooks": {
    "preToolUse": [
      {
        "type": "command",
        "bash": "./scripts/security-check.sh",
        "powershell": "./scripts/security-check.ps1",
        "cwd": "scripts",
        "env": { "LOG_LEVEL": "INFO" },
        "timeoutSec": 15,
        "comment": "Documentation string, ignored at runtime"
      }
    ]
  }
}
FieldTypeDescription
typestringMust be "command"
bashstringCommand for Linux/macOS
powershellstringCommand for Windows
cwdstringWorking directory relative to repo root
envobjectEnvironment variables
timeoutSecnumberDefault 30 seconds
commentstringDocumentation, ignored at runtime

There is no matcher field — hooks fire on all invocations of their event type. Tool-level filtering must be done inside the script by inspecting toolName from stdin.

VS Code

Also reads hooks from .claude/settings.json, .claude/settings.local.json, and ~/.claude/settings.json (Claude Code format compatibility). Converts lowerCamelCase to PascalCase and maps bashosx/linux, powershellwindows.

Input Schema (stdin)

preToolUse

{
  "timestamp": 1704614600000,
  "cwd": "/path/to/project",
  "toolName": "bash",
  "toolArgs": "{\"command\":\"rm -rf dist\",\"description\":\"Clean build\"}"
}

Note: toolArgs is a JSON string, not an object. Scripts must parse it (e.g., with jq).

sessionStart

  • source: "new" | "resume"
  • initialPrompt: string

sessionEnd

  • reason: string

Output Schema (stdout)

preToolUse output (Cloud Agent / CLI)

{
  "permissionDecision": "deny",
  "permissionDecisionReason": "Destructive operations blocked"
}
ValueMeaning
"allow"Permit the tool call
"deny"Block the tool call
"ask"Prompt user for confirmation

Exit code 0 = allow (if no JSON output), non-zero = deny.

VS Code output (preview, extended fields)

FieldTypeDescription
continuebooleanfalse stops agent
stopReasonstringMessage when continue is false
systemMessagestringWarning shown to user
hookSpecificOutput.permissionDecisionstringallow, deny, ask
hookSpecificOutput.updatedInputobjectReplace tool arguments
hookSpecificOutput.additionalContextstringExtra context for agent

Execution Behavior

  • Hooks run synchronously and sequentially (array order).
  • If the first hook returns deny, subsequent hooks are skipped.
  • Recommended execution time: under 5 seconds.
  • Default timeout: 30 seconds. On timeout, hook is terminated and agent continues.
  • Scripts read JSON from stdin (INPUT=$(cat)) and write to stdout; debug output goes to stderr.

Environment Variables

No built-in variables beyond those specified in the hook’s env field. The cwd field controls the working directory.

Custom instructions (soft/probabilistic)

FileScope
.github/copilot-instructions.mdRepository-wide instructions
.github/instructions/**/*.instructions.mdPath-specific instructions (with applyTo globs)
AGENTS.mdAgent-mode instructions
~/.copilot/copilot-instructions.mdUser-level (personal)
Organization-level instructionsAdmin-configured

Priority: Personal (user) > Repository (workspace) > Organization.

MCP server configuration

ScopeConfig pathRoot key
VS Code (workspace).vscode/mcp.jsonservers
VS Code (user)Via “MCP: Open User Configuration” commandservers
CLI~/.copilot/mcp-config.jsonmcpServers

Note: VS Code uses "servers" as root key while the CLI uses "mcpServers". MCP tools only work in Copilot’s Agent mode. Supported transports: local/stdio, http/sse.

MCP Server Registration

Symposium registers MCP servers in the Copilot config as top-level keys (matching the CLI’s mcpServers format, not VS Code’s servers format):

{
  "symposium": {
    "command": "/path/to/cargo-agents",
    "args": ["mcp"]
  }
}
  • Project-level: .vscode/mcp.json
  • User-level: ~/.copilot/mcp-config.json

Registration is idempotent — if the entry already exists with the correct values, no changes are made. If the entry exists but has stale values (e.g. the binary moved), it is updated in place.

Copilot SDK (programmatic hooks)

The @github/copilot-sdk (Node.js, Python, Go, .NET, Java) provides callback-style hooks for applications embedding the Copilot runtime:

  • onPreToolUse — can return modifiedArgs
  • onPostToolUse — can return modifiedResult
  • onSessionStart, onSessionEnd, etc.

Agent firewall (Cloud Agent)

Network-layer control with deny-by-default domain allowlist, configured at org/repo level. Not a hook — controls outbound network access.

Gemini CLI Hooks Reference

Disclaimer: This document reflects our current understanding of Gemini CLI’s hook system. It is a working reference for symposium development, not a substitute for the official docs. Details may be outdated or incomplete — always consult the primary sources.

Primary sources: Hooks reference · Extensions reference · GitHub repo

Gemini CLI’s hook system (v0.26.0, January 2026) mirrors Claude Code’s JSON-over-stdin contract and exit-code semantics. It adds model-level and tool-selection interception events unique to Gemini.

Hook Types

Only type: "command" is currently supported.

Events

EventTriggerCan block?Category
BeforeToolBefore tool invocationYesTool
AfterToolAfter tool executionYes (block result)Tool
BeforeAgentUser submits prompt, before planningYesAgent
AfterAgentAgent loop ends (final response)Yes (retry/halt)Agent
BeforeModelBefore sending request to LLMYes (mock response)Model
AfterModelAfter receiving LLM response (per-chunk during streaming)Yes (redact)Model
BeforeToolSelectionBefore LLM selects toolsFilter tools onlyModel
SessionStartSession beginsNo (advisory)Lifecycle
SessionEndSession endsNo (best-effort)Lifecycle
NotificationSystem notification (e.g., ToolPermission)No (advisory)Lifecycle
PreCompressBefore context compressionNo (async, cannot block)Lifecycle

Model-level events (unique to Gemini)

  • BeforeModel: can swap models, modify temperature, or return a synthetic response to skip the LLM call entirely.
  • BeforeToolSelection: can filter the candidate tool list using toolConfig.mode (AUTO/ANY/NONE) and allowedFunctionNames whitelists. Multiple hooks use union aggregation across allowed function lists.
  • AfterModel: can redact or modify the LLM response per-chunk during streaming.

Configuration

Four-tier precedence: Project → User → System → Extensions.

FileScope
.gemini/settings.jsonProject
~/.gemini/settings.jsonUser
/etc/gemini-cli/settings.jsonSystem
ExtensionsPlugin-provided

Configuration structure

{
  "hooks": {
    "BeforeTool": [
      {
        "matcher": "write_file|replace",
        "sequential": false,
        "hooks": [
          {
            "name": "secret-scanner",
            "type": "command",
            "command": "$GEMINI_PROJECT_DIR/.gemini/hooks/block-secrets.sh",
            "timeout": 5000,
            "description": "Prevent committing secrets"
          }
        ]
      }
    ]
  }
}
  • matcher: regex for tool events, exact string for lifecycle events.
  • sequential: boolean (default false). When true, hooks run in order with output chaining.
  • timeout: milliseconds (default 60,000).

Input Schema (stdin)

Base fields (all events)

{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "hook_event_name": "string",
  "timestamp": "2026-03-03T10:30:00Z"
}

BeforeTool additions

  • tool_name: string
  • tool_input: object (raw model arguments)
  • mcp_context: object (optional)
  • original_request_name: string (optional)

AfterTool additions

  • tool_name, tool_input (same as BeforeTool)
  • tool_response: object containing llmContent, returnDisplay, and optional error

BeforeModel additions

  • llm_request: object with model, messages, config, toolConfig

BeforeAgent additions

  • prompt: string (the user’s original prompt text)

AfterAgent additions

  • stop_hook_active: boolean (loop detection)

Output Schema (stdout)

Universal fields

FieldTypeDescription
decisionstring"allow" or "deny" (alias "block")
reasonstringFeedback sent to agent when denied
systemMessagestringDisplayed to user
continuebooleanfalse kills agent loop
stopReasonstringMessage when continue is false
suppressOutputbooleanHide from logs/telemetry

Event-specific output via hookSpecificOutput

BeforeTool: tool_input — merges with and overrides model arguments.

AfterTool:

  • additionalContext: string appended to tool result
  • tailToolCallRequest: object triggering a follow-up tool call

AfterAgent: when denied, reason is sent as a new prompt for retry.

BeforeAgent: additionalContext — string appended to the prompt for that turn. decision: "deny" discards the user’s message from history; continue: false preserves it.

BeforeModel:

  • llm_request: overrides outgoing request (swap model, modify temperature, etc.)
  • llm_response: provides synthetic response that skips the LLM call

Exit Codes

CodeMeaning
0Success; stdout parsed as JSON
2System block — stderr used as reason
OtherWarning (non-fatal), action proceeds

Execution Behavior

  • Hooks run in parallel by default; set sequential: true for ordered execution with output chaining.
  • Default timeout: 60,000ms.

Environment Variables

VariableDescription
GEMINI_PROJECT_DIRAbsolute path to project root
GEMINI_SESSION_IDCurrent session ID
GEMINI_CWDCurrent working directory
CLAUDE_PROJECT_DIRCompatibility alias for GEMINI_PROJECT_DIR

Environment redaction for sensitive variables (KEY, TOKEN patterns) is available but disabled by default.

Migration from Claude Code

gemini hooks migrate --from-claude

Converts .claude configurations to .gemini format. Tool name mappings:

Claude CodeGemini CLI
Bashrun_shell_command
Editedit_file
Writewrite_file
Readread_file

Custom Instructions

Gemini CLI reads GEMINI.md files at multiple levels:

ScopePath
Global~/.gemini/GEMINI.md
ProjectGEMINI.md in CWD and parent directories up to .git root
Just-in-timeGEMINI.md discovered when tools access a file/directory

The filename is configurable via context.fileName in settings.json (e.g., ["AGENTS.md", "GEMINI.md"]). Supports @file.md import syntax for including content from other files.

Skills

ScopePathNotes
Workspace.agents/skills/ or .gemini/skills/.agents/ takes precedence
User~/.agents/skills/ or ~/.gemini/skills/.agents/ takes precedence
Extension~/.gemini/extensions/<name>/skills/Bundled with extensions

Skills use SKILL.md with YAML frontmatter (name, description). Metadata is injected at session startup; full content loads on demand via activate_skill.

MCP Server Configuration

Configured under mcpServers in .gemini/settings.json or ~/.gemini/settings.json:

{
  "mcpServers": {
    "serverName": {
      "command": "path/to/executable",
      "args": ["--arg1"],
      "env": { "API_KEY": "$MY_TOKEN" },
      "timeout": 30000
    }
  }
}

Transport is auto-selected by key: command+args (stdio), url (SSE), httpUrl (streamable HTTP).

MCP Server Registration

In addition to hooks, symposium registers itself as an MCP server in the agent’s settings file. This provides an alternative integration path alongside the hook-based approach.

Configuration structure

The MCP server entry is added under mcpServers in the same settings file used for hooks:

{
  "mcpServers": {
    "symposium": {
      "command": "/path/to/cargo-agents",
      "args": ["mcp"]
    }
  }
}
  • Project-level: .gemini/settings.json
  • User-level: ~/.gemini/settings.json

Registration is idempotent — if the entry already exists with the correct values, no changes are made. If the entry exists but has stale values (e.g. the binary moved), it is updated in place.

Codex CLI Hooks Reference

Disclaimer: This document reflects our current understanding of Codex CLI’s hook system. It is a working reference for symposium development, not a substitute for the official docs. Details may be outdated or incomplete — always consult the primary sources.

Primary sources: Hooks · GitHub repo

OpenAI’s Codex CLI implements a shell-command hook system configured in hooks.json. It is experimental (disabled by default, not available on Windows) and first shipped in v0.114 (March 2026).

Enabling

Add to ~/.codex/config.toml:

[features]
codex_hooks = true

Configuration

FileScope
~/.codex/hooks.jsonUser-global
<repo>/.codex/hooks.jsonProject-scoped

Both are additive — all matching hooks from all files run. Project hooks follow the untrusted-project trust model.

Configuration structure

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "python3 ~/.codex/hooks/check_bash.py",
        "statusMessage": "Checking command safety",
        "timeout": 30
      }]
    }]
  }
}

Only handler type is "command". matcher is a regex string; omit or use "" / "*" to match everything. Default timeout: 600 seconds, configurable via timeout or timeoutSec.

Events

EventTriggerMatcher filters onCan block?
SessionStartSession starts or resumessource ("startup" or "resume")Yes (continue: false)
PreToolUseBefore tool executiontool_name (currently only "Bash")Yes
PostToolUseAfter tool executiontool_name (currently only "Bash")Yes (continue: false)
UserPromptSubmitUser submits a promptN/AYes (continue: false)
StopAgent turn completesN/AYes (deny → continuation prompt)

Input Schema (stdin)

Base fields (all events)

{
  "session_id": "string",
  "transcript_path": "string|null",
  "cwd": "string",
  "hook_event_name": "string",
  "model": "string"
}

Turn-scoped events (PreToolUse, PostToolUse, UserPromptSubmit, Stop) add turn_id.

PreToolUse additions

  • tool_name: string
  • tool_use_id: string
  • tool_input: object with command field

PostToolUse additions

  • tool_name, tool_use_id, tool_input (same as PreToolUse)
  • tool_response: string

UserPromptSubmit additions

  • prompt: string

Stop additions

  • stop_hook_active: boolean
  • last_assistant_message: string

Output Schema (stdout)

Deny/block (two equivalent methods)

Method 1 — JSON output:

{ "decision": "block", "reason": "Destructive command blocked" }

or:

{
  "hookSpecificOutput": {
    "permissionDecision": "deny",
    "permissionDecisionReason": "Destructive command blocked"
  }
}

Method 2 — Exit code 2 with reason on stderr.

Inject context

{
  "hookSpecificOutput": {
    "additionalContext": "Extra info for the agent"
  }
}

Plain text on stdout also works for SessionStart and UserPromptSubmit (ignored for PreToolUse, PostToolUse, Stop).

Stop session

{ "continue": false, "stopReason": "Session terminated by hook" }

Supported on SessionStart, UserPromptSubmit, PostToolUse, Stop.

System message (UI warning)

{ "systemMessage": "Warning text shown to user" }

Stop event special behavior

For the Stop event, { "decision": "block", "reason": "Run tests again" } tells Codex to create a continuation prompt — it does not reject the turn.

Exit Codes

CodeMeaning
0Success; stdout parsed. No output = continue normally.
2Block/deny; stderr used as reason
OtherNon-blocking warning

Execution Behavior

  • Multiple matching hooks run concurrently — no ordering guarantees.
  • Commands run with session cwd as working directory.
  • Shell expansion works.

Parsed but Not Yet Implemented

These fields are accepted but fail open (no effect): suppressOutput, updatedInput, updatedMCPToolOutput, permissionDecision: "allow", permissionDecision: "ask".

Current Limitations

  • Only Bash tool events fire PreToolUse/PostToolUse — no file-write or MCP tool hooks.
  • PreToolUse can only deny, not modify tool input.
  • No async hook mode.
  • Stop event requires JSON output (plain text is invalid).

Environment Variables

No dedicated environment variables are set during hook execution (unlike Claude Code’s CLAUDE_PROJECT_DIR). All context is passed via stdin JSON. The cwd field serves as the project directory equivalent. CODEX_HOME (defaults to ~/.codex) controls where Codex stores config and state.

Custom Instructions

ScopePath
Global~/.codex/AGENTS.md (or AGENTS.override.md)
ProjectAGENTS.md (or AGENTS.override.md) at each directory level from git root to CWD

The project_doc_fallback_filenames config option in ~/.codex/config.toml allows alternative filenames. Max combined size: 32 KiB (project_doc_max_bytes).

Skills

ScopePath
Repository.agents/skills/<name>/SKILL.md (each dir from CWD up to repo root)
User~/.agents/skills/<name>/SKILL.md
Admin/etc/codex/skills/<name>/SKILL.md
SystemBundled with Codex

Skills use SKILL.md with YAML frontmatter (name, description) and may include scripts/, references/, assets/, and agents/openai.yaml.

MCP Server Configuration

Configured in ~/.codex/config.toml or .codex/config.toml under [mcp_servers.<name>]:

[mcp_servers.my-server]
command = "path/to/executable"
args = ["--arg1"]
env = { API_KEY = "value" }
startup_timeout_sec = 10
tool_timeout_sec = 60

Supports stdio (command/args) and streamable HTTP (url/bearer_token_env_var). CLI management: codex mcp add <name> ....

MCP Server Registration

In addition to hooks, symposium registers itself as an MCP server in the agent’s config file. This provides an alternative integration path alongside the hook-based approach.

Configuration structure

The MCP server entry is added under [mcp_servers] in the TOML config:

[mcp_servers.symposium]
command = "/path/to/cargo-agents"
args = ["mcp"]
  • Project-level: .codex/config.toml
  • User-level: ~/.codex/config.toml

Registration is idempotent — if the entry already exists with the correct values, no changes are made. If the entry exists but has stale values (e.g. the binary moved), it is updated in place.

Other Extensibility

  • notify in config.toml (fire-and-forget on agent-turn-complete)
  • Execpolicy command-level rules
  • Subagents
  • Slash commands

Goose Hooks Reference

Disclaimer: This document reflects our current understanding of Goose’s extensibility surface. It is a working reference for symposium development, not a substitute for the official docs. Details may be outdated or incomplete — always consult the primary sources.

Primary sources: Extensions · Configuration · GitHub repo

Goose does not implement lifecycle hooks. There are no shell-command or programmatic interception points for tool execution, session start/stop, or prompt submission. No hooks.json equivalent. No JSON stdin/stdout protocol.

What Goose Offers Instead

MCP Extensions

The primary extensibility mechanism. Extensions are MCP servers (stdio or HTTP) that expose new tools, resources, and prompts. Configured in ~/.config/goose/config.yaml under extensions:. Built-in extensions include Developer (shell, file editing), Computer Controller, Memory, and Todo. Custom extensions are standard MCP servers built in Python, TypeScript, or Kotlin. Extensions add capabilities but cannot intercept or modify existing tool behavior.

Permission Modes

The closest analog to hook-based control flow. Static configuration, not programmable logic.

ModeBehavior
autoTools execute without approval (default)
approveEvery tool call requires manual confirmation
smart_approveAI risk assessment auto-approves low-risk, prompts for high-risk
chatNo tool use

Per-tool permissions can be set to Always Allow, Ask Before, or Never Allow.

Goosehints / AGENTS.md

Instruction files injected into the system prompt. Influence LLM behavior through prompting, not deterministic interception.

FileScope
~/.config/goose/.goosehintsGlobal
.goosehints (project root)Project
AGENTS.mdProject

GOOSE_TERMINAL Environment Variable

Shell scripts can detect whether they’re running under Goose and alter behavior (e.g., wrapping git to block git commit). This is a shell-level workaround, not a Goose-native mechanism.

Other Mechanisms

  • .gooseignore — gitignore-style file access restriction
  • Recipes — YAML workflow packages
  • Custom slash commands
  • Subagents
  • ACP integration
  • Tool Router — internal optimization for tool selection

MCP Server Registration

Since Goose has no lifecycle hooks, symposium integrates exclusively via MCP server registration. Symposium registers itself as an extension in the Goose config file.

Configuration structure

The MCP server entry is added under extensions in the YAML config:

extensions:
  symposium:
    provider: mcp
    config:
      command: /path/to/cargo-agents
      args: [mcp]
  • Project-level: .goose/config.yaml
  • User-level: ~/.config/goose/config.yaml

Registration is idempotent — if the entry already exists with the correct values, no changes are made. Stale entries are updated in place.

Kiro Hooks Reference

Disclaimer: This document reflects our current understanding of Kiro’s hook system. It is a working reference for symposium development, not a substitute for the official docs. Details may be outdated or incomplete — always consult the primary sources.

Primary sources: CLI hooks · Agent configuration reference · IDE hooks

Kiro is Amazon’s AI coding agent available as an IDE (VS Code fork) and CLI. Both have hook systems but they differ in configuration format, trigger types, and capabilities.

Kiro CLI Agent Definition

Each .kiro/agents/*.json file defines a complete agent. All fields are optional; omitting a field has specific defaults.

FileScope
.kiro/agents/*.jsonProject
~/.kiro/agents/*.jsonGlobal

Agent Definition Fields

FieldTypeDefault if omitted
namestringDerived from filename
descriptionstring(none)
promptstring or file:// URINo custom system context
toolsarray of stringsNo tools available
allowedToolsarray of strings/globsAll tools require confirmation
toolAliasesobject(none)
resourcesarray of URIs/objects(none)
hooksobject(none)
mcpServersobject(none)
toolsSettingsobject(none)
includeMcpJsonboolean(none)
modelstringSystem default
keyboardShortcutstring(none)
welcomeMessagestring(none)

Critical: Omitting tools means the agent has zero tools. Use "tools": ["*"] for all tools, "@builtin" for built-ins only, or list specific tools.

Tools Field Values

  • "*" — all available tools
  • "@builtin" — all built-in tools
  • "read", "write", "shell" — specific built-in tools
  • "@server_name" — all tools from an MCP server
  • "@server_name/tool_name" — specific MCP tool

AllowedTools Field

Specifies tools that execute without user confirmation. Supports exact matches and glob patterns ("@server/read_*", "@git-*/status"). Does not support "*" wildcard for all tools.

Resources Field

  • "file://README.md" — load file into context at startup
  • "skill://.kiro/skills/**/SKILL.md" — skill metadata loaded at startup, full content on-demand

Custom agents do not auto-discover skills. They require explicit skill:// URIs in resources.

Kiro CLI Hooks

Configured inside agent configuration JSON files. Shell commands receive JSON on stdin and use exit codes for control flow.

Events

EventTriggerMatcher?Can block?
agentSpawnSession startsNoNo
userPromptSubmitUser submits promptNoNo
preToolUseBefore tool executionYesYes (exit 2)
postToolUseAfter tool executionYesNo
stopAgent finishesNoNo

Input Schema (stdin)

All events include hook_event_name and cwd.

userPromptSubmit adds:

  • prompt: string

preToolUse adds:

  • tool_name: string
  • tool_input: object (full tool arguments)

postToolUse adds:

  • tool_name: string
  • tool_input: object
  • tool_response: string

MCP tools use @server/tool naming (e.g., @postgres/query).

Exit Codes

CodeMeaning
0Success; stdout captured as context
2Block (preToolUse only); stderr sent to LLM as reason
OtherWarning; stderr shown but execution continues

Matcher Patterns

  • Tool name strings: execute_bash, fs_write, read
  • Aliases: shell, write
  • MCP server globs: @git, @git/status
  • Wildcards: *
  • Built-in group: @builtin
  • No matcher = applies to all tools

Execution Behavior

  • Hooks execute in array order within each trigger type.
  • Default timeout: 30 seconds (30,000ms), configurable via timeout_ms.
  • cache_ttl_seconds: default 0 (no caching). agentSpawn hooks are never cached.

Configuration Example

{
  "hooks": {
    "preToolUse": [
      {
        "matcher": "execute_bash",
        "command": "./scripts/validate.sh"
      }
    ],
    "postToolUse": [
      {
        "matcher": "fs_write",
        "command": "cargo fmt --all"
      }
    ],
    "agentSpawn": [
      {
        "command": "git status"
      }
    ]
  }
}

Each entry is a flat object with command (required) and optional matcher. There is no nested hooks array or type field.

Kiro IDE Hooks

Stored as individual .kiro.hook files in .kiro/hooks/. Created via the Kiro panel UI or command palette.

Hook File Format

name: Format on save
description: Run formatter after file saves
when:
  type: fileEdit
  patterns: **/*.ts
then:
  type: shellCommand
  command: npx prettier --write {file}

Trigger Types (10)

TypeTrigger
promptSubmitUser submits a prompt
agentStopAgent finishes responding
preToolUseBefore tool execution
postToolUseAfter tool execution
fileCreateFile created
fileEditFile saved
fileDeleteFile deleted
preTaskExecutionBefore spec task runs
postTaskExecutionAfter spec task runs
userTriggeredManual invocation

The IDE adds file-event and spec-task triggers not available in the CLI.

Action Types (2)

TypeDescription
askAgentSends a natural language prompt to the agent (consumes credits)
shellCommandRuns locally; exit 0 = stdout added to context, non-zero = blocks on preToolUse/promptSubmit

IDE Tool Matching Categories

read, write, shell, web, spec, *, @mcp, @powers, @builtin, plus regex patterns with @ prefix.

IDE Execution Behavior

  • Default timeout: 60 seconds.
  • USER_PROMPT env var is available for promptSubmit shell commands.

Environment Variables

No dedicated environment variables are documented for CLI hook execution. Context is passed via stdin JSON. The IDE provides USER_PROMPT for promptSubmit shell command hooks.

Custom Instructions (Steering)

Kiro uses “steering files” instead of a single instructions file:

ScopePath
Workspace.kiro/steering/*.md
Global~/.kiro/steering/*.md
StandardAGENTS.md at workspace root (always included)

Steering files support YAML frontmatter with four inclusion modes: Always, FileMatch (glob pattern), Manual (referenced via #name in chat), and Auto (description-based matching). Kiro also auto-generates product.md, tech.md, and structure.md.

Skills

ScopePath
Workspace.kiro/skills/<name>/SKILL.md
Global~/.kiro/skills/<name>/SKILL.md

Workspace skills take precedence over global skills with the same name. The default agent auto-discovers skills from both locations. Custom agents require explicit skill:// URIs in their resources field. Skills use SKILL.md with YAML frontmatter (name, description).

MCP Server Configuration

ScopePath
Workspace.kiro/settings/mcp.json
Global~/.kiro/settings/mcp.json
Agent-levelmcpServers field in .kiro/agents/*.json

Priority: Agent config > Workspace > Global. Format is JSON with mcpServers key, supporting command/args/env for stdio and url/headers for remote servers.

MCP Server Registration

In addition to hooks, symposium registers itself as an MCP server in the agent’s MCP config file. This provides an alternative integration path alongside the hook-based approach.

Configuration structure

The MCP server entry is added under mcpServers:

{
  "mcpServers": {
    "symposium": {
      "command": "/path/to/cargo-agents",
      "args": ["mcp"]
    }
  }
}
  • Project-level: .kiro/settings/mcp.json
  • User-level: ~/.kiro/settings/mcp.json

Registration is idempotent — if the entry already exists with the correct values, no changes are made. If the entry exists but has stale values (e.g. the binary moved), it is updated in place.

OpenCode Plugin System Reference

Disclaimer: This document reflects our current understanding of OpenCode’s plugin/hook system. It is a working reference for symposium development, not a substitute for the official docs. Details may be outdated or incomplete — always consult the primary sources.

Primary sources: Plugins · GitHub repo

OpenCode’s extensibility centers on TypeScript/JavaScript plugins, not shell commands. Plugins are async functions that receive a context object and return a hooks object. A secondary experimental system supports shell-command hooks in opencode.json.

Symposium does not currently integrate with OpenCode’s hook system. OpenCode is supported as a skills-only agent.

Plugin Locations and Load Order

Hooks run sequentially in this order:

  1. Global config plugins (~/.config/opencode/opencode.json"plugin": [...])
  2. Project config plugins (opencode.json)
  3. Global plugin directory (~/.config/opencode/plugins/)
  4. Project plugin directory (.opencode/plugins/)

npm packages are auto-installed via Bun and cached in ~/.cache/opencode/node_modules/.

Plugin Context Object

All plugins receive: { project, client, $, directory, worktree }.

Core Plugin Hooks

HookTriggerControl Flow
eventEvery system event (~30 types including session.idle, session.created, tool.execute.before, file.edited, permission.asked)Observe only
tool.execute.beforeBefore any built-in tool runsThrow Error → block. Mutate output.args → modify tool arguments. Return normally → allow.
tool.execute.afterAfter a built-in tool completesMutate output.title, output.output, output.metadata
shell.envBefore any shell executionMutate output.env to inject environment variables
stopAgent attempts to stopCall client.session.prompt() to prevent stopping and send more work
configDuring configuration loadingMutate config object directly
toolPlugin load time (declarative)Registers custom tools via tool() definitions; overrides built-ins with same name
authAuth initializationObject with provider, loader, methods
chat.messageChat message processingMutate message and parts via output object
chat.paramsBefore LLM API callMutate temperature, topP, options via output object
permission.askPermission requestedSet output.status to 'allow' or 'deny'reportedly never called (bug #7006)

Experimental Hooks (prefix experimental.)

HookDescription
chat.system.transformPush strings to output.system array to augment system prompt
chat.messages.transformMutate output.messages array
session.compactingPush to output.context or replace output.prompt during compaction

tool.execute.before Schema

Input

{
  "tool": "string",
  "sessionID": "string",
  "callID": "string"
}

Output (mutable)

{
  "args": { "key": "value" }
}

Mutate output.args to change tool arguments before execution.

chat.params Schema

Input

{
  "model": "string",
  "provider": "string",
  "message": "object"
}

Output (mutable)

{
  "temperature": 0.7,
  "topP": 0.9,
  "options": {}
}

Limitations

  • MCP tool calls do NOT trigger tool.execute.before or tool.execute.after.
  • Plugin-level syntax errors prevent loading entirely.
  • tool.execute.before errors block the tool.
  • No explicit timeout documentation for plugin hooks.
  • No hook ordering guarantees beyond load order.

Experimental Config-Based Shell Hooks (opencode.json)

A simpler shell-command system under "experimental.hook":

{
  "experimental": {
    "hook": {
      "file_edited": {
        "*.ts": [{ "command": ["prettier", "--write"], "environment": {"NODE_ENV": "development"} }]
      },
      "session_completed": [{ "command": ["notify-send", "Done!"], "environment": {} }]
    }
  }
}

Only two events: file_edited (glob-matched) and session_completed. No session_start (requested in issue #12110).

Environment Variables

Core OpenCode sets these on child processes:

  • OPENCODE_SESSION_ID — current session identifier
  • OPENCODE_SESSION_TITLE — human-readable session name

The shell.env plugin hook allows injecting custom environment variables into all shell execution.

Configuration-related env vars (not hook-specific): OPENCODE_CONFIG, OPENCODE_CONFIG_DIR, OPENCODE_MODEL.

Custom Instructions

ScopePath
ProjectAGENTS.md at project root
Global~/.config/opencode/AGENTS.md
Legacy compatCLAUDE.md (project), ~/.claude/CLAUDE.md (global)
Config-based"instructions" array in opencode.json (file paths and globs)

Priority: local AGENTS.md > local CLAUDE.md > global ~/.config/opencode/AGENTS.md > global ~/.claude/CLAUDE.md.

Skills

ScopePath
Project.opencode/skills/, .claude/skills/, .agents/skills/
Global~/.config/opencode/skills/, ~/.claude/skills/, ~/.agents/skills/

OpenCode walks up from CWD to the git worktree root, loading matching skill definitions. Skills use SKILL.md with YAML frontmatter (name, description) and are loaded on-demand via the native skill tool.

Additional Events (Plugin System)

The full event list includes: session.created, session.idle, session.compacted, message.updated, file.edited, file.watcher.updated, permission.asked, permission.replied, tool.execute.before, tool.execute.after, shell.env, tui.prompt.append, tui.command.execute, and others (~30 total). The message.updated event (filtered by role === "user") is the closest equivalent to a user-prompt-submit hook. The session.created event is the session-start equivalent.

MCP Server Registration

In addition to hooks, symposium registers itself as an MCP server in the agent’s config file. This provides an alternative integration path alongside the hook-based approach.

Configuration structure

The MCP server entry is added under mcp in the JSON config:

{
  "mcp": {
    "symposium": {
      "command": "/path/to/cargo-agents",
      "args": ["mcp"]
    }
  }
}
  • Project-level: opencode.json
  • User-level: ~/.config/opencode/opencode.json

Registration is idempotent — if the entry already exists with the correct values, no changes are made. If the entry exists but has stale values (e.g. the binary moved), it is updated in place.