@danieliser/agent-hooks
v1.0.0
Published
WordPress-style event system for Claude Code plugins — fire named events, registered listeners respond
Maintainers
Readme
agent-hooks
WordPress-style event system for Claude Code plugins. Fire named events, registered listeners respond with shell output, template content, or MCP tool call instructions.
Why agent-hooks?
- Decoupling — Plugins don't need to know about each other. Emit events; whoever's listening handles the rest.
- Priority ordering — Listeners run in priority order (lower = first). Control execution sequence without tight coupling.
- Error isolation — One listener's failure never blocks others. Your workflows stay resilient.
- Zero coupling — If nothing listens, nothing happens. No overhead, no errors, no dependencies.
Quick Start
1. Install the plugin
# From npm
npm install @danieliser/agent-hooks
# Or add to your Claude Code plugin marketplace2. Initialize config
npx agent-hooks initThis creates:
.claude/agent-hooks.yml— starter config with commented examplesscripts/on-event.sh— example shell listener
3. Auto-approve permissions
Every emit() call requires user approval unless you allow it. Add to .claude/settings.local.json:
{
"permissions": {
"allow": [
"mcp__plugin_agent-hooks_agent-hooks__*"
]
}
}Or granular:
{
"permissions": {
"allow": [
"mcp__plugin_agent-hooks_agent-hooks__emit",
"mcp__plugin_agent-hooks_agent-hooks__has_listeners"
]
}
}4. Configure a listener
Edit .claude/agent-hooks.yml:
events:
session.lifecycle.start:
- name: project-context
type: template
path: .claude/templates/context.md
priority: 55. Emit events
From any Claude Code plugin or agent:
emit(event: "session.lifecycle.start", data: { git_branch: "main" })How It Works
Agent → emit("domain.entity.action", data)
↓
MCP Server (agent-hooks)
↓
Config lookup → matched listeners (sorted by priority)
↓
Execute each: shell | template | mcp
↓
Return merged results to agent- An agent calls
emit()with an event name and optional data payload - agent-hooks finds all listeners registered for that event (including wildcards)
- Listeners execute in priority order (lower number = first)
- Results are merged and returned to the calling agent
API Reference
emit
Fire a named event. Returns listener results and any MCP call instructions.
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| event | string | yes | — | Event name in dot-notation snake_case |
| data | object | no | {} | Event payload (max 10KB) |
| timeout | integer | no | 5000 | Max wait time in ms |
| mode | string | no | "sync" | "sync" waits for results; "async" fire-and-forget |
Sync response:
{
"event": "my.event",
"invocation_id": "evt-abc123",
"listeners_executed": 2,
"responses": [
{ "name": "listener-a", "status": "success", "result": { ... } },
{ "name": "listener-b", "status": "success", "result": { ... } }
],
"errors": [],
"duration_ms": 45
}Async response:
{
"event": "my.event",
"invocation_id": "evt-abc123",
"mode": "async",
"listeners_enqueued": 2,
"status": "enqueued"
}has_listeners
Check if listeners are registered for an event. Fast, no side effects.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| event | string | yes | Event name to query (no wildcards) |
Response:
{
"event": "my.event",
"has_listeners": true,
"listener_count": 2,
"priorities": [5, 10]
}Configuration
Config files are YAML. Both are optional:
- Project:
.claude/agent-hooks.yml— project-specific listeners - Global:
~/.claude/agent-hooks.yml— applies to all projects
When both exist, they deep-merge. Same-name listeners: project wins. Different names: both fire.
See docs/CONFIGURATION.md for the full schema reference.
Listener Types
Shell — Runs a subprocess. JSON on stdin, JSON on stdout.
- name: slack-notifier
type: shell
command: ./scripts/notify-slack.sh
priority: 20Template — Loads markdown, applies ${data.X} / ${env.Y} substitution.
- name: project-context
type: template
path: .claude/templates/context.md
priority: 5MCP — Returns tool call instructions for the agent to execute.
- name: create-adr
type: mcp
server: my-server
tool: create_record
priority: 10
args_mapping:
title: "${data.spec_name}"Event Naming
Dot-notation snake_case: domain.entity.action
strategize.spec.draftedsession.lifecycle.startexecute.batch.completed
Wildcard listeners: strategize.* matches any strategize.X.Y event.
Permissions
agent-hooks tools require explicit permission in Claude Code. Without this, every emit() call will prompt for user approval.
Recommended — wildcard allow (covers both tools):
"mcp__plugin_agent-hooks_agent-hooks__*"Add this to .claude/settings.local.json under permissions.allow. See the Quick Start for the full config block.
CLI
npx agent-hooks init # Scaffold config + example listener
npx agent-hooks validate # Validate config, report errors with line numbers
npx agent-hooks help # Show usageExamples
See the examples/ directory for ready-to-use listeners:
shell-listener.sh— Shell script with stdin/stdout JSONtemplate-listener.md— Markdown template with substitutionmcp-listener.yml— MCP listener config snippetstarter-config.yml— Full annotated config file
Security
This tool executes user-defined shell commands. Only register listeners you trust.
Mitigations:
- Payload size limit: 10KB max per emit
- Template size limit: 100KB max per file
- Listener timeout: configurable, default 5s, SIGTERM then SIGKILL
- Error isolation: failures don't cascade
- YAML safe loading: no object deserialization
- Shell commands use
execFile(no shell injection via command string)
See SECURITY.md for the full risk model and responsible disclosure.
Contributing
See CONTRIBUTING.md for development setup, code style, and PR process.
