Introduction
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
symposium 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 symposium 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 symposium 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 symposium 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.
Getting Started
Install
cargo binstall symposium # or `cargo install` if you prefer
Set up your user and project
From your project directory, run:
symposium init
This walks you through two things:
- User-wide setup — picks your agent (Claude Code, Copilot, Gemini) and stores it in
~/.symposium/config.toml. Registers a global hook so your agent picks up project extensions automatically. - Project setup — scans your workspace dependencies, discovers available extensions, and generates
.symposium/config.toml.
Check .symposium/ into version control so your team shares the same configuration. Each developer picks their own agent via symposium init --user.
You can also run the two steps separately with symposium init --user and symposium init --project.
What’s in .symposium/config.toml
The project config lists each available extension with a simple on/off toggle:
[skills]
salsa = true
tokio = true
[workflows]
rtk = true
When your agent starts, the registered hook installs the enabled extensions into the locations your agent expects (e.g., .claude/skills/ for Claude Code). You don’t need to do anything — the hook handles it.
Keeping in sync
As dependencies change, run:
symposium sync
This re-scans your dependencies, updates the config, and re-installs extensions. Your existing on/off choices are preserved. See symposium sync for options.
Usage patterns
This page describes how Symposium fits into your day-to-day workflow.
Skills activate automatically
When you ask your AI assistant about a crate in your project, Symposium checks your dependencies and loads matching skills. You don’t need to do anything special — just work as you normally would.
For example, if your project depends on tokio and a skill exists for it, your assistant will receive that guidance whenever it’s relevant.
Browsing available skills
To see which skills are available for crates in your current project:
symposium crate --list
To get guidance for a specific crate:
symposium crate tokio
Hooks run in the background
If your agent supports hooks (e.g., Claude Code), Symposium can intercept events like tool use and apply checks automatically. Hooks are configured by plugins — you don’t need to set them up yourself.
Keeping things up to date
Plugin sources are checked for updates on startup. You can also update manually:
symposium plugin sync
This fetches the latest skills and hooks from all configured git-based plugin sources.
Want to tweak how Symposium works?
Check out the configuration section.
Supporting your crate
If you maintain a Rust crate, you can teach AI assistants how to use your library well. Think of it as documentation that the AI actually reads.
What you can provide
There are two kinds of extensions you can publish:
- Skills — guidance documents that AI assistants receive automatically when a user’s project depends on your crate.
- Hooks — checks and transformations that run when the AI performs certain actions, like writing code or running commands.
Just want to add a skill?
If all you need is to publish guidance for your crate, you don’t need to set up a plugin. Just write a SKILL.md with a few lines of frontmatter (name, description, which crate it’s for) and a markdown body, then open a PR to the symposium-dev/recommendations repository.
See Publishing skills for the details.
Need hooks, or hooks and skills?
If you want to publish hooks — or a combination of hooks and skills — you’ll need to create a plugin. A plugin is a TOML manifest that ties everything together.
See Creating a plugin for how to set one up.
Publishing skills
Skills are guidance documents that teach AI assistants how to use your crate. When a user’s project depends on your crate, Symposium loads your skills automatically.
Writing a SKILL.md
A skill is a SKILL.md file with a few lines of YAML frontmatter followed by a markdown body:
---
name: widgetlib-basics
description: Basic guidance for widgetlib usage
crates: widgetlib
activation: always
---
Prefer using `Widget::builder()` over constructing widgets directly.
Always call `.validate()` before passing widgets to the runtime.
The frontmatter tells Symposium when to load the skill. The markdown body is what the AI assistant sees — write it as direct guidance about what to do, what to avoid, and which patterns work.
Publishing your skill
Currently publishing skills requires a PR to the symposium-dev/recommendations repository; once the system stabilizes more, we expect to allow you to embed skills directly in your crate with no central repository.
You can either upload the skill directly to the central repo or you can upload a plugin that points to skills in your own repository.
Uploading a single skill to the central repo
Add a directory for your crate containing a SKILL.md:
widgetlib/
SKILL.md
scripts/ # optional
resources/ # optional
Uploading multiple skills to the central repo
If you have several skills, add a subdirectory for each one:
widgetlib/
basics/
SKILL.md
advanced-patterns/
SKILL.md
scripts/ # optional
resources/ # optional
Hosting skills in your own repository
If you’d rather keep skills in your crate’s repository, you can add a plugin manifest to the recommendations repo that points to them and symposium will download the skills directly from your repository to users:
widgetlib.toml # create this
where widgetlib.toml looks someting like this:
name = "widgetlib"
[[skills]]
crates = ["widgetlib"]
source.git = "https://github.com/org/widgetlib/tree/main/symposium/skills"
See Creating a plugin for more on plugin manifests.
Frontmatter fields
| Field | Description |
|---|---|
name | Skill identifier. |
description | Short description shown in skill listings. |
crates | Which crate(s) this skill is about. Comma-separated: crates: serde, serde_json. |
activation | always (inline the body) or optional (list but don’t inline). Defaults to optional. |
compatibility | List of agents or editors this skill works with, if it doesn’t apply universally. See the compatibility field spec. |
See the Skill definition reference for the full format, the Skill matching reference for version constraint syntax, and the agentskills.io quickstart for general guidance on writing effective skills.
Activation modes
always— the skill body is included inline whenever the crate matches. Use this for guidance that’s broadly relevant.optional(the default) — the skill is listed with its metadata but the body isn’t inlined. Use this for targeted workflows, migration guides, or debugging aids that are only sometimes needed.
Testing your skills
From a project that depends on your crate:
symposium crate --list
symposium crate widgetlib
Creating a plugin
A plugin is a TOML manifest that bundles skills and hooks together. You need a plugin if you want to publish hooks, or if you want to host skills from your own repository rather than contributing them to the recommendations repo.
What’s in a plugin
A plugin manifest has three parts:
- A name — identifies the plugin in logs and CLI output.
- Skill groups (
[[skills]]) — each one points at a directory of skills and declares which crates they’re for. - Hooks (
[[hooks]]) — commands that run in response to agent events.
Skills and hooks are both optional — a plugin can have just skills, just hooks, or both.
Skill groups
Each [[skills]] entry declares which crates the skills apply to and where to find them. The source can be a local path or a git URL:
name = "widgetlib"
# Skills live next to the manifest
[[skills]]
crates = ["widgetlib"]
source.path = "skills"
name = "widgetlib"
# Skills are fetched from a GitHub repository
[[skills]]
crates = ["widgetlib"]
source.git = "https://github.com/org/widgetlib/tree/main/symposium/skills"
Use source.path when skills are on the local machine or in the same repository as the manifest. Use source.git when they’re hosted elsewhere — Symposium downloads and caches them automatically.
Warning: Skills in a plugin will only be fetched if the crates list matches, so you must include it.
You can have multiple skill groups in one plugin, each targeting different crates or versions:
name = "widgetlib"
[[skills]]
crates = ["widgetlib=1.0"]
source.path = "skills/v1"
[[skills]]
crates = ["widgetlib=2.0"]
source.path = "skills/v2"
Hooks
Hooks let your plugin respond to agent events. See Publishing hooks for details.
[[hooks]]
name = "check-widget-usage"
event = "PreToolUse"
matcher = "Bash"
command = "./scripts/check-widget.sh"
Where to put your plugin
You have two options:
- In your crate’s repository — add a
symposium.tomlat the root (or a subdirectory). The recommendations repo can then reference it via a git URL. - In the recommendations repo — submit a PR to symposium-dev/recommendations with your plugin manifest.
Validation
symposium plugin validate path/to/symposium.toml
This parses the manifest and reports any errors. Use --check-crates to also verify that crate names exist on crates.io.
Reference
See the Plugin definition reference for the full manifest format.
Publishing hooks
Hooks let your plugin respond to events during an AI coding session — for example, checking tool invocations or validating generated code.
Defining a hook
Hooks are declared in your plugin’s TOML manifest:
[[hooks]]
name = "check-widget-usage"
event = "PreToolUse"
matcher = "Bash"
command = "./scripts/check-widget.sh"
When the agent triggers a matching event, Symposium runs the command with the event payload as JSON on stdin.
Hook fields
| Field | Description |
|---|---|
name | A descriptive name for the hook (used in logs). |
event | The event type to match (e.g., PreToolUse). |
matcher | Which tool invocations to match (e.g., Bash, or omit for all). |
command | The command to run. Resolved relative to the plugin directory. |
Example: checking Bash commands
A hook that inspects Bash tool invocations before they run:
[[hooks]]
name = "inspect-bash"
event = "PreToolUse"
matcher = "Bash"
command = "./scripts/inspect-bash.sh"
The script receives the full event JSON on stdin and can:
- Exit 0 to allow the action
- Exit non-zero to block it
- Write guidance to stdout for the agent
Testing hooks
Use the CLI to test a hook with sample input (specify the agent):
echo '{"tool": "Bash", "input": "cargo test"}' | symposium hook claude pre-tool-use
You can also use copilot or gemini as the agent name, e.g. symposium hook copilot pre-tool-use.
Supported hooks
| Hook event | Description | CLI usage |
|---|---|---|
PreToolUse | Triggered before a tool (for example, Bash) is invoked by the agent. The hook receives the event payload on stdin. | pre-tool-use |
Agent → hook name mapping
The table below lists tool events as rows and agents as columns. Each cell is the hook event name the agent uses for that tool event.
| Tool / Event | Claude (claude) | Copilot (copilot) | Gemini (gemini) |
|---|---|---|---|
PreToolUse | PreToolUse | PreToolUse | BeforeTool |
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.
- If a hook writes non-JSON to stdout, the output will be ignored and a warning is logged.
-
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.
- If a hook exits with code
The symposium command
symposium init
symposium sync
symposium hook
Supported agents
Symposium supports seven AI coding agents. Each agent gets skill installation; hook support varies by agent.
Claude Code
Config name: claude
Skills
| Scope | Path |
|---|---|
| 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.
| Scope | File |
|---|---|
| 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.
GitHub Copilot
Config name: copilot
Skills
| Scope | Path |
|---|---|
| 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.
| Scope | File |
|---|---|
| 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).
Gemini CLI
Config name: gemini
Skills
| Scope | Path |
|---|---|
| Project | .agents/skills/<name>/SKILL.md |
| Global | ~/.gemini/skills/<name>/SKILL.md |
Hooks
Symposium merges hook entries into Gemini’s settings.json.
| Scope | File |
|---|---|
| Project | .gemini/settings.json |
| Global | ~/.gemini/settings.json |
Events registered: BeforeTool, AfterTool, SessionStart (Gemini’s own naming).
Output format: JSON with nested matcher groups. Timeouts in milliseconds.
Codex CLI
Config name: codex
Skills
| Scope | Path |
|---|---|
| Project | .agents/skills/<name>/SKILL.md |
| Global | ~/.agents/skills/<name>/SKILL.md |
Hooks
Symposium merges hook entries into Codex’s hooks.json.
| Scope | File |
|---|---|
| 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
Kiro
Config name: kiro
Skills
| Scope | Path |
|---|---|
| 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.
| Scope | File |
|---|---|
| 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.
OpenCode
Config name: opencode
Skills
| Scope | Path |
|---|---|
| Project | .agents/skills/<name>/SKILL.md |
| Global | ~/.agents/skills/<name>/SKILL.md |
Hooks
Not supported. OpenCode’s hook system is based on TypeScript/JavaScript plugins, not shell commands. Symposium cannot register hooks for OpenCode.
Skill files are installed but symposium hook will never be called by this agent.
Goose
Config name: goose
Skills
| Scope | Path |
|---|---|
| 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 symposium hook will never be called by this agent.
Configuration
symposium uses two configuration files: a user-wide config and an optional per-project config. When both exist, project settings override user settings.
User configuration
Stored at ~/.symposium/config.toml. Created by symposium init --user.
Full example
[agent]
name = "claude-code"
sync-default = true
auto-sync = true
[logging]
level = "info"
[[plugin-source]]
name = "my-org"
git = "https://github.com/my-org/symposium-plugins"
[[plugin-source]]
name = "local-dev"
path = "~/my-plugins"
[agent]
Your agent preference and default behaviors.
| Key | Type | Default | Description |
|---|---|---|---|
name | string | (required) | Which agent you use (e.g., "claude-code", "cursor"). |
sync-default | bool | true | Default on/off for newly discovered extensions. |
auto-sync | bool | false | Automatically run sync --workspace when workspace dependencies change. Detected by comparing the mtime of Cargo.lock against a cached value. |
[logging]
| Key | Type | Default | Description |
|---|---|---|---|
level | string | "info" | Minimum log level. One of: trace, debug, info, warn, error. |
[[plugin-source]]
Defines where symposium looks for skills, workflows, and MCP server definitions. Each entry must have exactly one of git or path.
| Key | Type | Default | Description |
|---|---|---|---|
name | string | (required) | A name for this source (used in logs and cache paths). |
git | string | — | Repository URL. Fetched and cached under ~/.symposium/cache/plugin-sources/. |
path | string | — | Local directory containing plugins. |
auto-update | bool | true | Check for updates on startup. Only applies to git sources. |
Project configuration
Stored at .symposium/config.toml in your project root. Created by symposium init --project and updated by symposium sync.
Full example
[agent]
name = "claude"
sync-default = false
self-contained = false
[defaults]
symposium-recommendations = true
user-plugins = true
[[plugin-source]]
name = "our-team"
git = "https://github.com/our-org/symposium-plugins"
[[plugin-source]]
name = "local"
path = "plugins"
[skills]
salsa = true
tokio = true
serde = false
[workflows]
rtk = true
autofmt = true
[agent]
Optional. If present, overrides the user’s agent settings for this project. Supports the same keys as the user-level [agent] section.
If omitted, each developer uses their own user-wide agent preference.
self-contained
| Key | Type | Default | Description |
|---|---|---|---|
self-contained | bool | false | If true, ignore user-level plugin sources entirely. Only project sources (including its own [defaults] and [[plugin-source]] entries) are used. |
[defaults]
Optional. Controls built-in plugin sources at the project level. Same keys as the user-level [defaults] section.
When self-contained = false (the default), project defaults are merged with user defaults — a project false overrides a user true. When self-contained = true, only the project defaults apply.
[[plugin-source]]
Project-level plugin sources. Same format as user-level [[plugin-source]] entries. Paths are resolved relative to the project root.
When self-contained = false, these are unioned with user-level sources. When self-contained = true, these are the only sources used (along with any enabled defaults).
[skills]
Lists available crate skills discovered from your workspace dependencies. Each key is a crate name, each value is a bool toggling the skill on or off.
Managed by symposium sync --workspace — new entries are added with the resolved sync-default, removed dependencies are cleaned up, and your existing choices are preserved.
[workflows]
Lists available workflow extensions. Same format as [skills]: each key is a workflow name, each value is a bool.
Setting resolution
When both user and project configs exist, project settings take precedence:
| Setting | Resolution |
|---|---|
agent.name | Project if set, else user |
agent.sync-default | Project if set, else user |
agent.auto-sync | Project if set, else user |
| Plugin sources | Union of user + project (or project only if self-contained) |
| Defaults | Merged (project false overrides user true; project only if self-contained) |
| Skills, workflows | Project config only |
Directory resolution
User-wide data lives under ~/.symposium/ by default. If XDG Base Directory environment variables are set, symposium respects them:
| Config | Cache | Logs | |
|---|---|---|---|
| XDG set | $XDG_CONFIG_HOME/symposium/ | $XDG_CACHE_HOME/symposium/ | $XDG_DATA_HOME/symposium/logs/ |
| Default | ~/.symposium/ | ~/.symposium/cache/ | ~/.symposium/logs/ |
File locations
| Path | Purpose |
|---|---|
<config-dir>/config.toml | User configuration |
<cache-dir>/ | Cache directory (crate sources, plugin sources) |
<data-dir>/logs/ | Log files |
.symposium/config.toml | Project configuration |
Plugin definition reference
A plugin is a TOML manifest loaded from a configured plugin source. It can be a standalone .toml file or a symposium.toml inside a directory.
Minimal manifest
name = "example"
[[skills]]
crates = ["serde"]
source.path = "skills"
Top-level fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Plugin name. Used in logs and CLI output. |
session-start-context | string | no | Text injected into the agent’s context at session start. See Session start context. |
[[skills]] groups
Each [[skills]] entry declares a group of skills.
| Field | Type | Description |
|---|---|---|
crates | string or array | Which crates this group advises on. Accepts a single string ("serde") or array (["serde", "tokio>=1.0"]). See Skill matching for atom syntax. |
source.path | string | Local directory containing skill subdirectories. Resolved relative to the manifest file. |
source.git | string | GitHub URL pointing to a directory in a repository (e.g., https://github.com/org/repo/tree/main/skills). Symposium downloads the tarball, extracts the subdirectory, and caches it. |
A skill group must have exactly one of source.path or source.git.
[[hooks]]
Each [[hooks]] entry declares a hook.
| Field | Type | Description |
|---|---|---|
name | string | Descriptive name for the hook (used in logs). |
event | string | Event type to match (e.g., PreToolUse). |
matcher | string | Which tool invocations to match (e.g., Bash). Omit to match all. |
command | string | Command to run when the hook fires. Resolved relative to the plugin directory. |
Session start context
The session-start-context field lets a plugin inject text into the agent’s conversation context when a session begins. This is useful for critical guidance that the agent should see before doing any work.
name = "rust-guidance"
session-start-context = "**Critical:** Before authoring Rust code, run `symposium start` for instructions."
When multiple plugins provide session-start-context, all of their texts are combined (separated by blank lines) and returned to the agent as additional context.
This works via the SessionStart hook event. When the agent starts a session, symposium collects session-start-context from all loaded plugins — including both user-level and project-level plugin sources — and returns the combined text.
Example: full manifest
name = "widgetlib"
[[skills]]
crates = ["widgetlib=1.0"]
source.path = "skills/general"
[[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 = "./scripts/check-widget.sh"
Validation
symposium plugin validate path/to/symposium.toml
This parses the manifest and reports any errors. Use --check-crates to also verify that crate names exist on crates.io.
Skill definition reference
A skill is a SKILL.md file inside a skill directory. Skills follow the agentskills.io format.
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
activation: always
---
Prefer deriving `Serialize` and `Deserialize` on data types.
Frontmatter fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Skill identifier. |
description | string | yes | Short description shown in skill listings. |
crates | string | no | Comma-separated crate atoms this skill is about (e.g., crates: serde, tokio>=1.0). Narrows the enclosing [[skills]] group scope — cannot widen it. |
activation | string | no | always or optional. Defaults to optional. |
Crate atoms
Crate atoms specify a crate name with an optional version constraint:
serde— any versiontokio>=1.40— 1.40 or newerregex<2.0— below 2.0serde=1.0— compatible-with-1.0 (equivalent to^1.0)serde==1.0.219— exact version
See Skill matching for the full syntax.
Activation modes
| Mode | Behavior |
|---|---|
always | Skill body is inlined in symposium crate output. Use for guidance that’s broadly relevant whenever the crate is in use. |
optional (default) | Skill is listed with metadata and path but body is not inlined. Use for targeted workflows, migration guides, or debugging aids. |
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.
Skill Matching Reference
Skill matching is based on crate predicates.
Atom forms
An atom is a crate name with an optional version requirement.
Examples:
serdeserde>=1.0tokio^1.40regex<2.0serde=1.0serde==1.0.219
Semantics:
- bare crate name: any version
>=,<=,>,<,^,~: standard semver operators=1.0: compatible-version matching, equivalent to^1.0==1.0.219: exact-version matching
Usage in matching fields
The crates field in both SKILL.md frontmatter and plugin [[skills]] groups accepts simple atom lists.
In TOML plugin manifests, crates accepts a string or array:
crates = "serde"crates = ["serde", "tokio>=1.40"]
In SKILL.md frontmatter, crates uses comma-separated values:
crates: serdecrates: serde, tokio>=1.40
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 -- start # run locally: Rust guidance + crate skill list
cargo run -- crate 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.
What to read next
- Key repositories — the repos that make up Symposium
- Key modules — the main pieces of the codebase
- Important flows — key paths through the code
- Integration test harness — how to write and run tests
- Governance — how we work together
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 (tells the agent to run symposium start), 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/symposium.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 two config types: user-wide Config (stored at ~/.symposium/config.toml) with AgentConfig, logging, plugin sources, and hook settings; and ProjectConfig (stored at .symposium/config.toml) with optional agent override, skills, and workflows. Provides resolve_agent_name() and resolve_sync_default() for merging project settings over user settings.
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 commands
Implements symposium init. Three entry points: init_user() prompts for agent and writes user config; init_project() finds the workspace root, creates project config, and runs sync; init_default() does both as needed.
sync.rs — synchronization commands
Implements symposium sync. Two main flows: sync_workspace() scans workspace dependencies, matches against plugin skill predicates, and merges into .symposium/config.toml; sync_agent() reads the project config and installs enabled skills into agent-specific directories while registering hooks.
plugins.rs — plugin registry
Scans configured plugin source directories for TOML manifests and parses them into Plugin structs. Each plugin contains SkillGroups (which crates, where to find the skills) and Hooks (event handlers). Also discovers standalone SKILL.md files not wrapped in a plugin. Returns a PluginRegistry — a table of contents that doesn’t load skill content.
skills.rs — skill resolution and matching
Given a PluginRegistry and workspace dependencies, this module does the actual work: resolves skill group sources (fetching from git if needed), discovers SKILL.md files, evaluates crate predicates, and formats output. Separates results into always (inlined) vs optional (listed with metadata).
hook.rs and session_state.rs — hook handling
hook.rs handles the three hook events: PostToolUse (tracks which skills the agent has loaded), UserPromptSubmit (scans prompts for crate mentions and nudges about unloaded skills), and PreToolUse (dispatches to plugin-defined hook commands). session_state.rs persists per-session data (activations, nudge history, prompt count) as JSON files.
dispatch.rs — shared CLI/MCP dispatch
The convergence point for the legacy CLI and MCP. Defines SharedCommand (clap-derived enum) and routes start and crate commands to the right handler. Both the legacy CLI (main.rs) and mcp.rs (MCP server) call into this layer.
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 merging
Both user (~/.symposium/config.toml) and project (.symposium/config.toml) configs are loaded and merged. Project settings override user settings field-by-field within the [agent] section. Plugin sources come from the user config only. Skills and workflows come from the project config only.
Agents
symposium supports multiple AI agents. Each agent has its own hook protocol, file layout, and configuration locations. This page documents the agent-specific details that symposium needs to handle.
Supported agents
| Config name | Agent |
|---|---|
claude | Claude Code |
copilot | GitHub Copilot |
gemini | Gemini CLI |
codex | Codex CLI |
kiro | Kiro |
opencode | OpenCode |
goose | Goose |
The agent name is stored in [agent] name in either the user or project config.
Agent responsibilities
For each agent, symposium needs to know how to:
- Register hooks — write the hook configuration so the agent calls
symposium hookon the right events. - 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, symposium prefers vendor-neutral paths where possible:
| Scope | Path | Supported by |
|---|---|---|
| Project skills | .agents/skills/<skill-name>/SKILL.md | Copilot, Gemini, Codex, OpenCode, Goose |
| Project skills | .claude/skills/<skill-name>/SKILL.md | Claude Code (does not support .agents/skills/) |
| Project skills | .kiro/skills/<skill-name>/SKILL.md | Kiro (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/. symposium uses the vendor-neutral .agents/skills/ path whenever the agent supports it.
At the global level, each agent has its own path:
| Agent | Global 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.
| Scope | File |
|---|---|
| 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": "symposium hook claude pre-tool-use"
}
]
}
]
}
}
Supported events
Claude Code supports many hook events. The ones relevant to symposium are:
| Event | Description |
|---|---|
PreToolUse | Before a tool is invoked. Can allow, block, or modify the tool call. |
PostToolUse | After a tool completes. Used to track skill activations. |
UserPromptSubmit | When 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.
| Scope | File |
|---|---|
| Global | ~/.copilot/config.json (under hooks key) |
| Project | .github/hooks/*.json |
Example hook registration:
{
"version": 1,
"hooks": {
"preToolUse": [
{
"type": "command",
"bash": "symposium hook copilot pre-tool-use",
"timeoutSec": 10
}
]
}
}
Note: Copilot uses camelCase event names (preToolUse), unlike Claude Code’s PascalCase (PreToolUse).
Supported events
| Event | Description |
|---|---|
preToolUse | Before a tool is invoked. Can allow, deny, or modify tool args. |
postToolUse | After a tool completes. |
sessionStart | New session begins. Supports command and prompt types. |
sessionEnd | Session completes. |
userPromptSubmitted | When the user submits a prompt. |
errorOccurred | When 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.
| Scope | File |
|---|---|
| Global | ~/.gemini/settings.json |
| Project | .gemini/settings.json |
Example hook registration:
{
"hooks": {
"BeforeTool": [
{
"matcher": ".*",
"hooks": [
{
"name": "symposium",
"type": "command",
"command": "symposium hook gemini pre-tool-use",
"timeout": 10000
}
]
}
]
}
}
Note: Gemini uses BeforeTool (not PreToolUse), and timeouts are in milliseconds (default: 60000).
Supported events
| Event | Type | Description |
|---|---|---|
BeforeTool | Tool | Before a tool is invoked. |
AfterTool | Tool | After a tool completes. |
BeforeToolSelection | Tool | Before the LLM selects tools. |
BeforeModel | Model | Before LLM requests. |
AfterModel | Model | After LLM responses. |
BeforeAgent | Lifecycle | Before agent loop starts. |
AfterAgent | Lifecycle | After agent loop completes. |
SessionStart | Lifecycle | When a session starts. |
SessionEnd | Lifecycle | When a session ends. |
PreCompress | Lifecycle | Before history compression. |
Notification | Lifecycle | On 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
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.
| Scope | File |
|---|---|
| Global | ~/.kiro/agents/symposium.json |
| Project | .kiro/agents/symposium.json |
Example hook registration:
{
"hooks": {
"preToolUse": [
{
"matcher": "*",
"command": "symposium hook kiro pre-tool-use"
}
],
"agentSpawn": [
{
"command": "symposium 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
| Event | Description |
|---|---|
preToolUse | Before a tool is invoked. Can block (exit code 2). |
postToolUse | After a tool completes. |
userPromptSubmit | When the user submits a prompt. |
agentSpawn | Session starts (maps to session-start internally). |
stop | Agent finishes. |
Hook payload/output
Kiro uses exit-code-based control flow:
- Exit 0: stdout captured as additional context
- Exit 2: block (
preToolUseonly), 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
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.
| Scope | File |
|---|---|
| Global | ~/.codex/hooks.json |
| Project | .codex/hooks.json |
Example hook registration:
{
"hooks": {
"PreToolUse": [
{
"matcher": "",
"hooks": [{
"type": "command",
"command": "symposium hook codex pre-tool-use",
"timeout": 10
}]
}
]
}
}
Note: An empty matcher string matches everything in Codex (equivalent to "*" in other agents).
Supported events
| Event | Description |
|---|---|
PreToolUse | Before a tool is invoked. Can block. |
PostToolUse | After a tool completes. Can stop session (continue: false). |
UserPromptSubmit | When the user submits a prompt. |
SessionStart | Session starts or resumes. |
Stop | Agent turn completes. |
Hook payload/output
Codex uses a protocol similar to Claude Code, with two methods to block:
- JSON output:
{ "decision": "block", "reason": "..." } - 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
Hook registration
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 — symposium sync will install skill files in the vendor-neutral .agents/skills/ path that OpenCode reads.
Goose
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 — symposium sync will install skill files in the vendor-neutral .agents/skills/ path.
Adding a new agent
To add support for a new agent:
- Add a variant to the
HookAgentenum inhook_schema.rs. - Create an agent module (e.g.,
hook_schema/newagent.rs) implementing theAgenttrait and the event-specific payload/output types. - Implement the
AgentHookPayloadandAgentHookOutputtraits to convert between the agent’s wire format and the internalHookPayload/HookOutputtypes. - 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.
Important flows
This section describes the logic of each symposium command.
symposium init --user
Sets up the user-wide configuration.
Flow
-
Prompt for agent — ask which agent the user uses (e.g., Claude Code, Cursor).
-
Write user config — create
~/.symposium/config.tomlwith the[agent]section populated:[agent] name = "claude-code" sync-default = true -
Run
sync --agent— delegates to thesync --agentflow to register global hooks for the chosen agent. Since there’s no project context, this just ensures the global hook is in place (e.g., a global hook in~/.claude/settings.jsonthat callssymposium hookon session start).
Combined init: When
initruns both user and project setup (either by default or with both flags), user setup runs first, then project setup. The project setup’ssync --agentstep will see the freshly written user config, so hooks end up in the right place.
symposium init --project
Sets up project-level configuration for the current workspace.
Flow
-
Find workspace root — run
cargo metadatato locate the workspace manifest directory. -
Prompt for agent override — ask whether to set a project-level agent (default: use each developer’s own user-wide preference).
-
Create project config — create the
.symposium/directory and an empty.symposium/config.toml. If an agent override was selected, write the[agent]section. -
Run
sync --workspace— delegates to thesync --workspaceflow to scan dependencies, discover available extensions, and populate the config file. -
Run
sync --agent— delegates to thesync --agentflow to install the discovered extensions into the agent’s expected locations and ensure hooks are in place.
Combined init: When
initruns both user and project setup (either by default or with both flags), user setup completes first (including its ownsync --agentfor global hooks), then project setup runs. Thesync --agentat this step sees the full context — user config plus project config — and places hooks accordingly.
symposium sync --workspace
Updates .symposium/config.toml to reflect the current workspace dependencies.
Flow
-
Check
Cargo.lockmtime — compare the mtime ofCargo.lockagainst the cached value (stored in the global cache directory). If unchanged, skip the rest — there’s nothing to do. -
Read workspace dependencies — run
cargo metadatato get the full dependency list for the workspace. -
Load plugin sources — read the user config’s
[[plugin-source]]entries and load their plugin manifests. For git sources, fetch/update as needed. -
Match extensions to dependencies — for each plugin, evaluate skill group crate predicates and individual skill
cratesfrontmatter against the workspace dependencies. Also discover available workflows. -
Merge with existing config — load the current
.symposium/config.tomland reconcile:- New extensions: add entries with the resolved
sync-defaultvalue (from project config if set, else user config). - Removed dependencies: remove entries for extensions whose crate predicates no longer match.
- Existing entries: preserve the user’s on/off choices.
- New extensions: add entries with the resolved
-
Write config — write the updated
.symposium/config.toml. -
Cache mtime — store the current
Cargo.lockmtime so future runs can skip work if nothing changed.
symposium sync --agent
Installs enabled extensions into the agent’s expected locations and ensures hooks are registered.
Flow
When run inside a project
-
Determine agent — check
.symposium/config.tomlfor a project-level[agent]override. If not set, fall back to the user-wide config. -
Ensure hooks are registered — where the hooks are placed depends on where the agent setting comes from:
- Project-level agent: install hooks into the project’s agent config (e.g.,
.claude/hooks.jsonfor Claude Code). - User-level agent: install hooks into the global agent config (e.g.,
~/.claude/settings.jsonfor Claude Code).
- Project-level agent: install hooks into the project’s agent config (e.g.,
-
Install extensions — read
.symposium/config.tomland, for each enabled extension:- Skills: resolve the skill source (local or git), copy/symlink
SKILL.mdfiles into the agent’s expected location (e.g.,.claude/skills/for Claude Code). - Workflows: install workflow definitions into the appropriate agent location.
- Skills: resolve the skill source (local or git), copy/symlink
When run outside a project
-
Read user config — load
~/.symposium/config.tomlto determine the agent. -
Ensure global hooks are registered — install hooks into the global agent config. This is all that can be done without a project context.
symposium hook
Entry point invoked by the agent’s hook system on session events.
Flow
-
Run
sync --agent— delegates to thesync --agentflow to ensure extensions are installed and hooks are current. The project root is resolved from the payload’scwdfield (checking for a.symposium/directory); if no project is detected, this step is a no-op. Runs quietly and non-fatally — failures are logged but don’t block hook dispatch. -
Dispatch to plugin hooks — for each enabled plugin that defines a hook handler for the incoming event:
- Pass the event JSON on stdin to the plugin’s hook command.
- Collect output from each handler.
- 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.
Integration test harness
Integration tests live in tests/ and use the symposium-testlib crate for composable, isolated test environments.
Fixtures
Test fixtures are directories under tests/fixtures/ that provide fragments of a Symposium environment:
plugins0/— adot-symposium/directory containing aconfig.tomland a local plugin with a serde skill. No network access needed.workspace0/— a minimalCargo.tomlworkspace with tokio and serde dependencies.
Fixtures are designed to compose. with_fixture(&["plugins0", "workspace0"]) copies both into a single temp directory, giving you a complete environment with config, plugins, and a workspace.
TestContext
with_fixture() returns a TestContext wrapping an isolated Symposium instance:
#![allow(unused)]
fn main() {
let ctx = with_fixture(&["plugins0", "workspace0"]);
}
The harness scans the copied files for config.toml (becomes the Symposium config directory) and Cargo.toml (becomes the workspace root). It panics if multiple of either are found.
TestContext provides three methods:
invoke(&["start"])— parses args via clap (same as the MCP server would) and routes through the shared dispatch layer. Returns the output string.invoke_hook(payload)— calls the built-in hook logic directly with a typed payload (e.g.,PostToolUsePayload,PreToolUsePayload). Returns aHookOutput.normalize_paths(&output)— replaces temp directory paths with$CONFIG_DIRso snapshots are stable across runs.
Snapshot testing
Tests use the expect-test crate for inline snapshot assertions:
#![allow(unused)]
fn main() {
#[tokio::test]
async fn start() {
let ctx = with_fixture(&["plugins0"]);
let output = ctx.invoke(&["start"]).await.unwrap();
let output = ctx.normalize_paths(&output);
expect![[r#"
...expected output...
"#]].assert_eq(&output);
}
}
When output changes, run with UPDATE_EXPECT=1 to update the inline snapshots in place:
UPDATE_EXPECT=1 cargo test
Adding a new fixture
Create a directory under tests/fixtures/ with whatever files your test needs. Convention: use dot-symposium/ for config/plugin files (the harness discovers config.toml by filename, not by directory name). Compose it with existing fixtures via with_fixture(&["your-fixture", "workspace0"]).
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:
| Category | Policy |
|---|---|
| New contributors | PRs need review from a maintainer |
| Maintainer team member | PRs should be reviewed by another maintainer before landing |
| Core team member | Review 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.
updatedInput type mismatch (Claude Code)
HookSpecificOutput.updated_input and Claude’s ClaudeHookSpecificOutput.updated_input are typed as Option<String>, but per the Claude Code reference, updatedInput is a JSON object (e.g., {"command": "safe-cmd"}). Should be Option<serde_json::Value>.
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.
matches_matcher uses substring instead of regex (Claude Code / Gemini)
HookSubPayload::matches_matcher() uses matcher.contains(&tool_name) — a substring check. The Claude Code and Gemini references specify regex matching for tool events. Patterns like "mcp__.*" or "^Bash$" would not work correctly. Also the operand order is inverted (checks if matcher contains tool name, not if tool name matches the matcher regex).
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
For each agent it supports, symposium needs to know:
- Hook registration — where and how to write config so the agent calls
symposium hook - Hook I/O protocol — event names, input/output field names, exit code semantics
- Extension installation — where skill files go (project and global)
- 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
| Agent | Project config path | Global config path | Format |
|---|---|---|---|
| Claude Code | .claude/settings.json | ~/.claude/settings.json | JSON, hooks key with matcher groups |
| GitHub Copilot | .github/hooks/*.json | ~/.copilot/config.json | JSON, version: 1 with hooks key |
| Gemini CLI | .gemini/settings.json | ~/.gemini/settings.json | JSON, hooks key with matcher groups |
| Codex CLI | .codex/hooks.json | ~/.codex/hooks.json | JSON, hooks key with matcher groups |
| Kiro | .kiro/agents/*.json | ~/.kiro/agents/*.json | JSON, 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
| Agent | Command field | Platform-specific? |
|---|---|---|
| Claude Code | command | No |
| GitHub Copilot | bash / powershell | Yes |
| Gemini CLI | command | No |
| Codex CLI | command | No |
| Kiro | command | No |
| OpenCode | N/A (JS function) | N/A |
| Goose | N/A | N/A |
Timeout defaults
| Agent | Default timeout | Unit |
|---|---|---|
| Claude Code | 600 | seconds |
| GitHub Copilot | 30 | seconds (timeoutSec) |
| Gemini CLI | 60,000 | milliseconds (timeout) |
| Codex CLI | 600 | seconds (timeout or timeoutSec) |
| Kiro | 30,000 | milliseconds (timeout_ms) |
| OpenCode | 60,000 | milliseconds (community hooks plugin) |
| Goose | N/A | N/A |
Event names
Symposium registers hooks for four events. Each agent uses different names and casing conventions.
| Symposium event | Claude Code | Copilot | Gemini CLI | Codex CLI | Kiro CLI | OpenCode | Goose |
|---|---|---|---|---|---|---|---|
| pre-tool-use | PreToolUse | preToolUse | BeforeTool | PreToolUse | preToolUse | tool.execute.before | N/A |
| post-tool-use | PostToolUse | postToolUse | AfterTool | PostToolUse | postToolUse | tool.execute.after | N/A |
| user-prompt-submit | UserPromptSubmit | userPromptSubmitted | BeforeAgent | UserPromptSubmit | userPromptSubmit | message.updated (filter by role) | N/A |
| session-start | SessionStart | sessionStart | SessionStart | SessionStart | agentSpawn | session.created | N/A |
Blocking support
Not all events can block the action in all agents.
| Agent | Pre-tool-use can block? | Post-tool-use can block? | User-prompt can block? | Session-start can block? |
|---|---|---|---|---|
| Claude Code | Yes | No | Yes (exit 2) | No |
| GitHub Copilot | Yes | No | No | No |
| Gemini CLI | Yes | Yes (block result) | Yes (deny discards message) | No |
| Codex CLI | Yes | Yes (continue: false) | Yes (continue: false) | Yes (continue: false) |
| Kiro | Yes (exit 2) | No | No | No |
| OpenCode | Yes (throw Error) | No | No (observe only) | No (observe only) |
| Goose | N/A | N/A | N/A | N/A |
Hook I/O protocol
Input fields (pre-tool-use)
| Agent | Tool name field | Tool args field | Session/context fields |
|---|---|---|---|
| Claude Code | tool_name | tool_input (object) | session_id, cwd, hook_event_name |
| GitHub Copilot | toolName | toolArgs (JSON string) | timestamp, cwd |
| Gemini CLI | tool_name | tool_input (object) | session_id, cwd, hook_event_name, timestamp |
| Codex CLI | tool_name | tool_input (object) | session_id, cwd, hook_event_name, model |
| Kiro | tool_name | tool_input (object) | hook_event_name, cwd |
| OpenCode | tool | args (mutable output object) | sessionID, callID |
| Goose | N/A | N/A | N/A |
Output structure (pre-tool-use)
| Agent | Permission decision field | Decision values | Modified input field | Nesting |
|---|---|---|---|---|
| Claude Code | permissionDecision | allow, deny, ask, defer | updatedInput | nested in hookSpecificOutput |
| GitHub Copilot | permissionDecision | allow, deny, ask | modifiedArgs | flat |
| Gemini CLI | decision | allow, deny | tool_input | nested in hookSpecificOutput |
| Codex CLI | decision or permissionDecision | block/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.args | JS mutation |
| Goose | N/A | N/A | N/A | N/A |
Exit codes
All shell-based agents use the same convention (where applicable):
| Code | Meaning |
|---|---|
0 | Success; stdout parsed as JSON |
2 | Block/deny; stderr used as reason |
| Other | Non-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
| Agent | Project skills path | Global 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
| Agent | Project instructions | Global instructions |
|---|---|---|
| Claude Code | CLAUDE.md, .claude/CLAUDE.md | ~/.claude/CLAUDE.md |
| GitHub Copilot | .github/copilot-instructions.md, AGENTS.md | ~/.copilot/copilot-instructions.md |
| Gemini CLI | GEMINI.md (walks up to .git) | ~/.gemini/GEMINI.md |
| Codex CLI | AGENTS.md (each dir level) | ~/.codex/AGENTS.md |
| Kiro | .kiro/steering/*.md, AGENTS.md | ~/.kiro/steering/*.md |
| OpenCode | AGENTS.md, CLAUDE.md | ~/.config/opencode/AGENTS.md |
| Goose | .goosehints, AGENTS.md | ~/.config/goose/.goosehints |
MCP server configuration
Relevant if symposium exposes functionality via MCP.
| Agent | MCP config location | Format |
|---|---|---|
| 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.json | JSON |
| OpenCode | opencode.json (mcp key) | JSON |
| Goose | ~/.config/goose/config.yaml (extensions key) | YAML |
Claude Code Hooks Reference
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
| Type | Description |
|---|---|
command | Shell script; communicates via stdin/stdout/exit codes |
http | POSTs JSON to a URL endpoint; supports header interpolation with $VAR_NAME |
prompt | Single-turn LLM evaluation returning {ok: true/false, reason} |
agent | Spawns a subagent with tool access (Read, Grep, Glob) for up to 50 turns |
Events
| Event | Trigger | Can block? | Matcher target |
|---|---|---|---|
SessionStart | Session begins/resumes | No | startup, resume, clear, compact |
SessionEnd | Session terminates (1.5s default timeout) | No | clear, resume, logout, etc. |
UserPromptSubmit | User submits prompt, before processing | Yes (exit 2) | None |
PreToolUse | Before tool call | Yes | Tool name regex (Bash, Edit|Write, mcp__.*) |
PostToolUse | After tool succeeds | No | Tool name regex |
PostToolUseFailure | Tool fails | No | Tool name regex |
PermissionRequest | Permission dialog appears | Yes | Tool name regex |
PermissionDenied | Auto-mode classifier denial | No (retry: true available) | Tool name regex |
Stop | Main agent finishes responding | Yes | None |
StopFailure | Turn ends on API error | No (output ignored) | rate_limit, authentication_failed, etc. |
SubagentStart | Subagent spawned | No | Agent type |
SubagentStop | Subagent finishes | Yes | Agent type |
Notification | System notification | No | permission_prompt, idle_prompt, etc. |
TaskCreated | Task created | Yes (exit 2 rolls back) | None |
TaskCompleted | Task completed | Yes (exit 2 rolls back) | None |
TeammateIdle | Teammate about to go idle | Yes | None |
ConfigChange | Config file changes during session | Yes (except policy_settings) | Config source |
CwdChanged | Directory change | No | None |
FileChanged | Watched file changes | No | Basename |
WorktreeCreate | Git worktree created | Yes (non-zero fails) | None |
WorktreeRemove | Git worktree removed | No | None |
PreCompact | Before compaction | No | manual, auto |
PostCompact | After compaction | No | manual, auto |
InstructionsLoaded | CLAUDE.md loaded | No | Load reason |
Elicitation | MCP server requests user input | Yes | MCP server name |
ElicitationResult | MCP elicitation result | Yes | MCP server name |
Configuration
Settings merge with precedence (highest first): Managed → Command line → Local → Project → User.
| File | Scope |
|---|---|
Managed policy (MDM, registry, server, /etc/claude-code/) | Organization-wide |
.claude/settings.local.json | Single project, gitignored |
.claude/settings.json | Single project, committable |
~/.claude/settings.json | All 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: stringtool_input: object with tool-specific fields (commandfor Bash,file_path/contentfor 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: booleanlast_assistant_message: string
Output Schema (stdout)
Output is capped at 10,000 characters.
Universal fields
| Field | Type | Description |
|---|---|---|
continue | boolean | false stops Claude entirely |
stopReason | string | Message for user when continue is false |
systemMessage | string | Warning shown to user |
suppressOutput | boolean | Omits 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
| Code | Meaning |
|---|---|
0 | Success; stdout parsed as JSON |
2 | Blocking error — action blocked, stderr fed to Claude |
| Other | Non-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
| Variable | Description |
|---|---|
CLAUDE_PROJECT_DIR | Absolute path to project root |
CLAUDE_ENV_FILE | File 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
bypassPermissionsmode.
GitHub Copilot Hooks Reference
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)
| Event | Trigger | Can block? |
|---|---|---|
sessionStart | New or resumed session | No |
sessionEnd | Session completes or terminates | No |
userPromptSubmitted | User submits a prompt | No |
preToolUse | Before tool call | Yes |
postToolUse | After tool completes (success or failure) | No |
errorOccurred | Error during agent execution | No |
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"
}
]
}
}
| Field | Type | Description |
|---|---|---|
type | string | Must be "command" |
bash | string | Command for Linux/macOS |
powershell | string | Command for Windows |
cwd | string | Working directory relative to repo root |
env | object | Environment variables |
timeoutSec | number | Default 30 seconds |
comment | string | Documentation, 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 bash→osx/linux, powershell→windows.
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"
}
| Value | Meaning |
|---|---|
"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)
| Field | Type | Description |
|---|---|---|
continue | boolean | false stops agent |
stopReason | string | Message when continue is false |
systemMessage | string | Warning shown to user |
hookSpecificOutput.permissionDecision | string | allow, deny, ask |
hookSpecificOutput.updatedInput | object | Replace tool arguments |
hookSpecificOutput.additionalContext | string | Extra 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.
Additional Extension Surfaces (not hooks, but related)
Custom instructions (soft/probabilistic)
| File | Scope |
|---|---|
.github/copilot-instructions.md | Repository-wide instructions |
.github/instructions/**/*.instructions.md | Path-specific instructions (with applyTo globs) |
AGENTS.md | Agent-mode instructions |
~/.copilot/copilot-instructions.md | User-level (personal) |
| Organization-level instructions | Admin-configured |
Priority: Personal (user) > Repository (workspace) > Organization.
MCP server configuration
| Scope | Config path | Root key |
|---|---|---|
| VS Code (workspace) | .vscode/mcp.json | servers |
| VS Code (user) | Via “MCP: Open User Configuration” command | servers |
| CLI | ~/.copilot/mcp-config.json | mcpServers |
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.
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 returnmodifiedArgsonPostToolUse— can returnmodifiedResultonSessionStart,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
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
| Event | Trigger | Can block? | Category |
|---|---|---|---|
BeforeTool | Before tool invocation | Yes | Tool |
AfterTool | After tool execution | Yes (block result) | Tool |
BeforeAgent | User submits prompt, before planning | Yes | Agent |
AfterAgent | Agent loop ends (final response) | Yes (retry/halt) | Agent |
BeforeModel | Before sending request to LLM | Yes (mock response) | Model |
AfterModel | After receiving LLM response (per-chunk during streaming) | Yes (redact) | Model |
BeforeToolSelection | Before LLM selects tools | Filter tools only | Model |
SessionStart | Session begins | No (advisory) | Lifecycle |
SessionEnd | Session ends | No (best-effort) | Lifecycle |
Notification | System notification (e.g., ToolPermission) | No (advisory) | Lifecycle |
PreCompress | Before context compression | No (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 usingtoolConfig.mode(AUTO/ANY/NONE) andallowedFunctionNameswhitelists. 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.
| File | Scope |
|---|---|
.gemini/settings.json | Project |
~/.gemini/settings.json | User |
/etc/gemini-cli/settings.json | System |
| Extensions | Plugin-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: stringtool_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 containingllmContent,returnDisplay, and optionalerror
BeforeModel additions
llm_request: object withmodel,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
| Field | Type | Description |
|---|---|---|
decision | string | "allow" or "deny" (alias "block") |
reason | string | Feedback sent to agent when denied |
systemMessage | string | Displayed to user |
continue | boolean | false kills agent loop |
stopReason | string | Message when continue is false |
suppressOutput | boolean | Hide from logs/telemetry |
Event-specific output via hookSpecificOutput
BeforeTool: tool_input — merges with and overrides model arguments.
AfterTool:
additionalContext: string appended to tool resulttailToolCallRequest: 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
| Code | Meaning |
|---|---|
0 | Success; stdout parsed as JSON |
2 | System block — stderr used as reason |
| Other | Warning (non-fatal), action proceeds |
Execution Behavior
- Hooks run in parallel by default; set
sequential: truefor ordered execution with output chaining. - Default timeout: 60,000ms.
Environment Variables
| Variable | Description |
|---|---|
GEMINI_PROJECT_DIR | Absolute path to project root |
GEMINI_SESSION_ID | Current session ID |
GEMINI_CWD | Current working directory |
CLAUDE_PROJECT_DIR | Compatibility 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 Code | Gemini CLI |
|---|---|
Bash | run_shell_command |
Edit | edit_file |
Write | write_file |
Read | read_file |
Custom Instructions
Gemini CLI reads GEMINI.md files at multiple levels:
| Scope | Path |
|---|---|
| Global | ~/.gemini/GEMINI.md |
| Project | GEMINI.md in CWD and parent directories up to .git root |
| Just-in-time | GEMINI.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
| Scope | Path | Notes |
|---|---|---|
| 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).
Codex CLI Hooks Reference
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
| File | Scope |
|---|---|
~/.codex/hooks.json | User-global |
<repo>/.codex/hooks.json | Project-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
| Event | Trigger | Matcher filters on | Can block? |
|---|---|---|---|
SessionStart | Session starts or resumes | source ("startup" or "resume") | Yes (continue: false) |
PreToolUse | Before tool execution | tool_name (currently only "Bash") | Yes |
PostToolUse | After tool execution | tool_name (currently only "Bash") | Yes (continue: false) |
UserPromptSubmit | User submits a prompt | N/A | Yes (continue: false) |
Stop | Agent turn completes | N/A | Yes (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: stringtool_use_id: stringtool_input: object withcommandfield
PostToolUse additions
tool_name,tool_use_id,tool_input(same as PreToolUse)tool_response: string
UserPromptSubmit additions
prompt: string
Stop additions
stop_hook_active: booleanlast_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
| Code | Meaning |
|---|---|
0 | Success; stdout parsed. No output = continue normally. |
2 | Block/deny; stderr used as reason |
| Other | Non-blocking warning |
Execution Behavior
- Multiple matching hooks run concurrently — no ordering guarantees.
- Commands run with session
cwdas 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
| Scope | Path |
|---|---|
| Global | ~/.codex/AGENTS.md (or AGENTS.override.md) |
| Project | AGENTS.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
| Scope | Path |
|---|---|
| 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 |
| System | Bundled 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> ....
Other Extensibility
notifyin config.toml (fire-and-forget on agent-turn-complete)- Execpolicy command-level rules
- Subagents
- Slash commands
Goose Hooks Reference
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.
| Mode | Behavior |
|---|---|
auto | Tools execute without approval (default) |
approve | Every tool call requires manual confirmation |
smart_approve | AI risk assessment auto-approves low-risk, prompts for high-risk |
chat | No 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.
| File | Scope |
|---|---|
~/.config/goose/.goosehints | Global |
.goosehints (project root) | Project |
AGENTS.md | Project |
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
Kiro Hooks Reference
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.
| File | Scope |
|---|---|
.kiro/agents/*.json | Project |
~/.kiro/agents/*.json | Global |
Agent Definition Fields
| Field | Type | Default if omitted |
|---|---|---|
name | string | Derived from filename |
description | string | (none) |
prompt | string or file:// URI | No custom system context |
tools | array of strings | No tools available |
allowedTools | array of strings/globs | All tools require confirmation |
toolAliases | object | (none) |
resources | array of URIs/objects | (none) |
hooks | object | (none) |
mcpServers | object | (none) |
toolsSettings | object | (none) |
includeMcpJson | boolean | (none) |
model | string | System default |
keyboardShortcut | string | (none) |
welcomeMessage | string | (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
| Event | Trigger | Matcher? | Can block? |
|---|---|---|---|
agentSpawn | Session starts | No | No |
userPromptSubmit | User submits prompt | No | No |
preToolUse | Before tool execution | Yes | Yes (exit 2) |
postToolUse | After tool execution | Yes | No |
stop | Agent finishes | No | No |
Input Schema (stdin)
All events include hook_event_name and cwd.
userPromptSubmit adds:
prompt: string
preToolUse adds:
tool_name: stringtool_input: object (full tool arguments)
postToolUse adds:
tool_name: stringtool_input: objecttool_response: string
MCP tools use @server/tool naming (e.g., @postgres/query).
Exit Codes
| Code | Meaning |
|---|---|
0 | Success; stdout captured as context |
2 | Block (preToolUse only); stderr sent to LLM as reason |
| Other | Warning; 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).agentSpawnhooks 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)
| Type | Trigger |
|---|---|
promptSubmit | User submits a prompt |
agentStop | Agent finishes responding |
preToolUse | Before tool execution |
postToolUse | After tool execution |
fileCreate | File created |
fileEdit | File saved |
fileDelete | File deleted |
preTaskExecution | Before spec task runs |
postTaskExecution | After spec task runs |
userTriggered | Manual invocation |
The IDE adds file-event and spec-task triggers not available in the CLI.
Action Types (2)
| Type | Description |
|---|---|
askAgent | Sends a natural language prompt to the agent (consumes credits) |
shellCommand | Runs 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_PROMPTenv var is available forpromptSubmitshell 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:
| Scope | Path |
|---|---|
| Workspace | .kiro/steering/*.md |
| Global | ~/.kiro/steering/*.md |
| Standard | AGENTS.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
| Scope | Path |
|---|---|
| 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
| Scope | Path |
|---|---|
| Workspace | .kiro/settings/mcp.json |
| Global | ~/.kiro/settings/mcp.json |
| Agent-level | mcpServers 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.
OpenCode Hooks Reference
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.
Plugin Locations and Load Order
Hooks run sequentially in this order:
- Global config plugins (
~/.config/opencode/opencode.json→"plugin": [...]) - Project config plugins (
opencode.json) - Global plugin directory (
~/.config/opencode/plugins/) - 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
| Hook | Trigger | Control Flow |
|---|---|---|
event | Every system event (~30 types including session.idle, session.created, tool.execute.before, file.edited, permission.asked) | Observe only |
tool.execute.before | Before any built-in tool runs | Throw Error → block. Mutate output.args → modify tool arguments. Return normally → allow. |
tool.execute.after | After a built-in tool completes | Mutate output.title, output.output, output.metadata |
shell.env | Before any shell execution | Mutate output.env to inject environment variables |
stop | Agent attempts to stop | Call client.session.prompt() to prevent stopping and send more work |
config | During configuration loading | Mutate config object directly |
tool | Plugin load time (declarative) | Registers custom tools via tool() definitions; overrides built-ins with same name |
auth | Auth initialization | Object with provider, loader, methods |
chat.message | Chat message processing | Mutate message and parts via output object |
chat.params | Before LLM API call | Mutate temperature, topP, options via output object |
permission.ask | Permission requested | Set output.status to 'allow' or 'deny' — reportedly never called (bug #7006) |
Experimental Hooks (prefix experimental.)
| Hook | Description |
|---|---|
chat.system.transform | Push strings to output.system array to augment system prompt |
chat.messages.transform | Mutate output.messages array |
session.compacting | Push 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.beforeortool.execute.after(issue #2319). - Plugin-level syntax errors prevent loading entirely.
tool.execute.beforeerrors 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 identifierOPENCODE_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
| Scope | Path |
|---|---|
| Project | AGENTS.md at project root |
| Global | ~/.config/opencode/AGENTS.md |
| Legacy compat | CLAUDE.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
| Scope | Path |
|---|---|
| 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.