Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Symposium hook events

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

Events

EventDescription
PreToolUseBefore the agent invokes a tool. Can inject context, modify the tool input, or block the call.
PostToolUseAfter a tool completes. Can inject context.
UserPromptSubmitWhen the user submits a prompt. Can inject context.
SessionStartWhen an agent session begins. Can inject context.
StopWhen the agent’s turn ends.

Agent support

Symposium registers one handler per event with each agent, under the agent’s own name for that event. A “not delivered” cell means Symposium registers no handler for that event on that agent. The table gives each agent’s native event name; the name in parentheses is the agent’s name in a hook’s format and in cargo agents hook <agent>.

AgentPreToolUsePostToolUseUserPromptSubmitSessionStartStop
Claude Code (claude)PreToolUsePostToolUseUserPromptSubmitSessionStartStop
Antigravity CLI (antigravity)PreToolUsePostToolUsePreInvocation (first invocation of each turn only)SessionStartStop
Codex CLI (codex)PreToolUsePostToolUseUserPromptSubmitSessionStartnot delivered
GitHub Copilot (copilot)preToolUsepostToolUseuserPromptSubmittedsessionStartnot delivered
Kiro (kiro)preToolUsepostToolUseuserPromptSubmitagentSpawnnot delivered
Goosenot deliverednot deliverednot deliverednot deliverednot delivered
OpenCodenot deliverednot deliverednot deliverednot deliverednot delivered

Goose and OpenCode have no shell hooks, so no plugin hook ever runs there; they receive skills and MCP servers only. A hook for an event an agent does not deliver simply never fires on that agent. Agents also differ in what they do with a hook’s output; for example, Antigravity shows a PreToolUse hook’s output only when the hook blocks the call. Each agent’s page under Supported agents has the details.

Input schemas

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

PreToolUse

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

PostToolUse

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

UserPromptSubmit

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

SessionStart

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

Stop

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

Output schemas

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

PreToolUse output

{
  "PreToolUse": {
    "additionalContext": "Remember to use --release for benchmarks",
    "updatedInput": { "command": "cargo test --release" }
  }
}
FieldTypeDescription
decision"allow" or "deny"Whether to allow or block the tool call. Defaults to "allow" and may be omitted. The SDK’s PreToolUseOutput::deny(reason) sets it together with the reason.
additionalContextstring or nullText injected into the agent’s context for this tool call. With "deny", the reason the agent is given.
updatedInputobject or nullReplacement tool input. If set, overrides the original tool_input.

To block the call:

{
  "PreToolUse": {
    "decision": "deny",
    "additionalContext": "Refusing to run a command that deletes the filesystem root"
  }
}

Each agent receives the decision in the form it honors:

Agent"deny"updatedInput
Claude CodepermissionDecision: "deny", reason shown to ClaudeSent with permissionDecision: "ask": the user confirms the rewritten call. Claude applies a rewrite only alongside a decision, and "allow" would skip the user’s permission prompt. In a headless run (claude -p) there is nobody to ask, so the rewritten call is blocked.
Codex CLIpermissionDecision: "deny"Not applied. Codex rewrites only with "allow" and has no "ask"; the original call runs and a warning is logged.
GitHub CopilotpermissionDecision: "deny"Sent as modifiedArgs.
KiroExit 2, reason on stderr (Kiro reads no decision from stdout)Not applied; the original call runs and a warning is logged.
Antigravitydecision: "deny"Sent as overwrite.

Tool input is the agent’s own: write updatedInput in the shape that agent’s tool takes.

PostToolUse output

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

UserPromptSubmit output

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

SessionStart output

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

Stop output

{
  "Stop": {
    "additionalContext": "Things look good!"
  }
}
FieldTypeDescription
additionalContextstring or nullText for the agent when the turn ends. Only Antigravity and Claude Code run Stop hooks, and neither treats this as plain context: Claude Code ignores it, while Antigravity takes it as a reason to keep going and starts another turn.

Exit codes

CodeMeaning
0Success. Stdout is parsed as JSON and merged into the hook result.
2Block. The action is blocked and stderr is returned to the agent as the reason. On PreToolUse this is a "deny" decision, delivered to each agent as in the table above. On other events cargo agents hook exits 2 with the reason on stderr, and the agent applies its own semantics (Claude Code, for instance, erases a blocked prompt).
Other non-zeroWarning. The hook is considered to have succeeded for dispatch purposes; stdout is still parsed if possible.

Matcher

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

The regex is unanchored: it matches if it is found anywhere in the tool name, so Bash also matches BashOutput. Anchor it with ^...$ (^Bash$) to match one tool exactly.

tool_name is passed through exactly as the agent sends it, and tool_input keeps the agent’s own argument names. Agents name the same tool differently: the shell tool is Bash in Claude Code and Codex, bash in Copilot, execute_bash in Kiro, and run_command in Antigravity, whose input carries CommandLine rather than command. A symposium-format hook meant to fire on several agents must match each name, for example matcher = "^(Bash|bash|run_command|execute_bash)$", and read the input in each shape.

Testing

To unit-test a symposium-format handler, pipe canonical JSON straight into it, the way Symposium does (a script handler runs under sh):

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

To test end to end, pipe the agent’s own native JSON into cargo agents hook <agent> <event>, where <agent> is one of antigravity, claude, codex, copilot, or kiro and <event> is pre-tool-use, post-tool-use, user-prompt-submit, session-start, or stop. For Claude Code:

echo '{"session_id":"s","cwd":"/path/to/project","hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"cargo test"}}' \
  | cargo agents hook claude pre-tool-use

This runs the full dispatch, including the automatic sync when auto-sync is on, for the workspace at cwd, so the plugin must be installed and active there. Symposium converts the payload to canonical JSON for your hook and prints the result in the agent’s format (for Claude Code, a hookSpecificOutput object). There is no symposium agent name: the canonical format is only what your handler sees.