HookBus documentation
Everything you need to install HookBus, publish events from an AI agent or assistant that surfaces hooks, subscribe to those events, and ship a subscriber of your own.
Introduction
HookBus is an agent event bus. Any AI agent or assistant that surfaces hooks can publish lifecycle events: a tool is about to run, a prompt is about to submit, a session just started. HookBus captures those events, routes them to subscribers in parallel, consolidates the verdicts, and threads the result back.
The bus is vendor-neutral. It does not care which LLM, which agent framework, or which CLI. If the runtime can emit a JSON event, it can publish to HookBus. If your code can receive a JSON event and return a JSON verdict, it can subscribe.
Who this is for
- Developers integrating an AI agent or assistant that surfaces hooks and wanting one place to do governance, cost tracking, memory, audit, or anything else
- Security and platform teams running AI infrastructure at scale who need auditable runtime controls
- Subscriber authors: anyone who wants to build a plugin that reacts to every agent's events, vendor-neutral
Quickstart
60 seconds from zero to your first governed event.
1. Install
One command. Pulls the Apache 2.0 bus and AgentProtect CRE Light as Docker images. Generates a bearer token. AgentSpend is optional.
Recommended: verify the script before running.
curl -fsSL https://hookbus.com/install.sh -o install.sh
curl -fsSL https://hookbus.com/install.sh.sha256 -o install.sh.sha256
sha256sum -c install.sh.sha256 && bash install.sh
Quickstart only (skips verification):
curl -fsSL https://hookbus.com/install.sh | bash
What you get: bus on :18800, dashboard on :18800/, AgentProtect CRE Light registered, bearer token written to .env in the install directory. Enable AgentSpend separately with --with-agentspend.
Bearer token handling. The install script generates a 32-byte URL-safe random token via openssl rand -base64 32 and writes it to $HOOKBUS_HOME/.env with mode 0600 (owner read-only). Treat it like a database credential. To rotate, edit .env, re-run docker compose up -d, and update any publishers using the old value.
2. Publish a test event
Any tool that can POST JSON can publish. No SDK required.
source $HOOKBUS_HOME/.env
curl -X POST http://localhost:18800/event \
-H "Authorization: Bearer $HOOKBUS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"event_id": "manual-smoke-1",
"event_type": "PreToolUse",
"timestamp": "2026-04-28T00:00:00Z",
"source": "manual",
"session_id": "demo-001",
"tool_name": "Bash",
"tool_input": {"command": "rm -rf /"},
"metadata": {}
}'
3. Watch the verdict
AgentProtect CRE Light evaluates, returns decision: deny with a reason. The bus consolidates and threads it back. The event appears in the dashboard at http://localhost:18800/.
{
"decision": "deny",
"reason": "Destructive command blocked by L1 policy",
"metadata": { "subscriber": "cre-agentprotect", "rule": "rm-rf-root" }
}
4. Wire up a real agent
Drop one of the publisher shims into your agent runtime. See Publishers.
Core concepts
| Concept | What it is |
|---|---|
| Publisher | Anything that emits an event. Your agent, your CLI hook, your SDK wrapper. |
| Event | A JSON envelope describing what the agent is about to do (or just did). |
| Bus | HookBus itself. Receives events, fans out to subscribers, consolidates responses. |
| Subscriber | Anything that receives events. Sync subscribers return verdicts. Async subscribers observe. |
| Decision | One of allow, deny, ask. Deny wins across all subscribers. |
| Consolidation | The rule that combines N subscriber responses into one verdict. Default: strictest wins. |
Writing a subscriber, Python
A subscriber is a process that receives HTTP POSTs containing event envelopes and returns JSON verdicts. Any language that can serve JSON can subscribe. Here is the minimum Python implementation:
from flask import Flask, request, jsonify
app = Flask(__name__)
# The only route the bus will call.
# Deployment config in subscribers.yaml points here.
@app.route("/event", methods=["POST"])
def handle_event():
event = request.get_json()
tool_name = event.get("tool_name")
tool_input = event.get("tool_input", {})
# Your policy logic goes here.
if tool_name == "Bash" and "rm -rf /" in str(tool_input):
return jsonify({
"decision": "deny",
"reason": "No destroying the planet",
"metadata": {"subscriber": "my-sub"}
})
return jsonify({"decision": "allow", "reason": "ok"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=9101)
(1) Return valid JSON within your sync-timeout budget. (2) Always include decision + reason. (3) If you error, return HTTP 200 with decision: "allow" and your error in metadata, never crash the bus.
Writing a subscriber, Node.js
The same shape, in Node.js. Works with Express, Fastify, Hono, or plain http.
import express from "express";
const app = express();
app.use(express.json());
app.post("/event", (req, res) => {
const { tool_name, tool_input } = req.body;
// Your policy logic goes here.
if (tool_name === "Bash" && JSON.stringify(tool_input).includes("rm -rf /")) {
return res.json({
decision: "deny",
reason: "No destroying the planet",
metadata: { subscriber: "my-sub" }
});
}
res.json({ decision: "allow", reason: "ok" });
});
app.listen(9102, () => console.log("subscriber up"));
Register & run your subscriber
Add your subscriber to ~/.hookbus/subscribers.yaml:
subscribers:
- name: my-sub
address: http://localhost:9101/event
mode: sync # sync = blocks. async = fire-and-forget.
timeout_ms: 2000 # sync-only budget. past this, bus defaults to allow.
categories: [governance]
Send HUP to the bus, or restart, and your subscriber is live:
docker compose restart hookbus
# or, if it supports live reload (Enterprise):
docker compose kill -s HUP hookbus
Verify your subscriber is registered:
curl http://localhost:18800/admin/subscribers \
-H "Authorization: Bearer $(cat ~/.hookbus/.token)"
Publishers
A publisher is any AI agent, assistant, SDK, or runtime wired to emit lifecycle hook events to the bus. HookBus ships shims for the major runtimes:
| Runtime | Type | Status |
|---|---|---|
| Claude Code | Hook (subprocess) | Live |
| AmpCode | Plugin (TypeScript) | Live |
| Hermes Agent | Plugin (Python) | Live |
| OpenClaw | Extension | Live |
| Codex CLI (OpenAI) | Hook | Live |
| OpenCode | CLI | Live |
| Anthropic Agent SDK | Python shim | Private beta |
| OpenAI Agents SDK | Python shim | Private beta |
| Cursor | Hook | Coming soon |
| Any HTTP client | Webhook | Always |
Each publisher's repo has its own install snippet. See the Publishers grid on the homepage.
Environment per CLI
The bus needs to know where it is and which CLI is speaking. Two host-level variables plus one per-CLI label.
| Variable | Scope | Example |
|---|---|---|
HOOKBUS_URL | host-level (shell profile OK) | http://localhost:18800 |
HOOKBUS_TOKEN | host-level (shell profile OK) | contents of ~/.hookbus/.token |
HOOKBUS_SOURCE | per-CLI, never in shell profile | claude-code, amp, hermes, … |
If you put export HOOKBUS_SOURCE=amp in ~/.bashrc, every other publisher on the same shell will inherit it and mis-label its events. Always pin HOOKBUS_SOURCE inline per-CLI (in the hook config, in the plugin source, or inline on the command line), never in a shell profile.
Envelope schema
Every event posted to POST /event follows this shape:
{
"hook": "PreToolUse", // lifecycle hook name
"tool_name": "Bash", // tool the agent is about to invoke
"tool_input": { ... }, // arbitrary JSON, agent-specific
"tool_result": { ... }, // present only for PostToolUse
"source": "claude-code", // which CLI/runtime emitted it
"session_id": "abc-123", // correlates events within a session
"timestamp": "2026-04-22T...", // ISO-8601 UTC
"hookbus_version": "0.1" // set by publisher
}
Supported hook values:
SessionStart, agent session opensUserPromptSubmit, a user prompt is about to be sent to the LLMPreToolUse, a tool call is about to executePostToolUse, a tool call just finished (tool_resultpopulated)SessionEnd, agent session closes
Response shape
Every subscriber must return a JSON object of this shape:
{
"decision": "allow" | "deny" | "ask", // required
"reason": "human-readable reason", // required
"metadata": { ... } // optional, subscriber-specific
}
Return channel (context injection)
Subscribers can push context back into the agent's next turn via reason and metadata. The publisher is responsible for surfacing this back to the model. This is how AgentProtect CRE Light injects policy reminders. This is how AgentKnowledge pushes relevant knowledge. This is how the bus becomes a governance layer and not just an observer.
Consolidation
When multiple subscribers return verdicts on the same event, the bus consolidates:
- Any
denywins, regardless of how manyallowresponses come back. - If no
denybut at least oneask, the consolidated decision isask. - Otherwise
allow. reasonstrings from all sync subscribers are concatenated into the finalreason.metadatafrom all sync subscribers is merged into a singlemetadataobject keyed by subscriber name.
Environment variables
| Variable | Used by | Default | Purpose |
|---|---|---|---|
HOOKBUS_URL | publishers | , | Bus endpoint (e.g. http://localhost:18800) |
HOOKBUS_TOKEN | publishers & subscribers | , | Bearer token read from ~/.hookbus/.token |
HOOKBUS_SOURCE | publishers (per-CLI) | , | Label for events emitted by this CLI (e.g. claude-code) |
HOOKBUS_TIMEOUT_MS | publishers | 3000 | How long a publisher waits for a bus verdict before failing closed (or allow, depending on policy) |
HOOKBUS_FAIL_MODE | publishers | closed | closed blocks on bus failure, open allows. Default is closed. |
HOOKBUS_TOKEN_FILE | bus & subscribers | /root/.hookbus/.token | Location of the shared bearer token file |
Error codes
| Code | Meaning | Where it comes from |
|---|---|---|
401 | Missing or invalid bearer token | Bus |
400 | Malformed envelope (missing hook, source, or not valid JSON) | Bus |
413 | Envelope too large (default limit 1 MB) | Bus |
504 | A sync subscriber missed its timeout budget | Bus |
503 | Bus is live but no subscribers registered | Bus |
499 | Publisher abandoned the request before verdict returned | Bus |
Troubleshooting
The publisher is failing closed, the agent cannot run any tool
Check: is the bus up? curl http://localhost:18800/health. If that returns 200, check the bearer token: echo $HOOKBUS_TOKEN must match the contents of ~/.hookbus/.token.
My subscriber registered but the bus is not hitting it
Check ~/.hookbus/subscribers.yaml. Is the address reachable from the bus container? If the bus runs in Docker and your subscriber runs on the host, use the host's LAN IP or host.docker.internal, not localhost.
Events from one publisher are showing up labelled as another
Almost certainly HOOKBUS_SOURCE leaking from a shell profile. Remove it from ~/.bashrc, ~/.zshrc, ~/.profile and pin it inline per CLI. See the callout above.
The dashboard shows no events
The dashboard streams from a ring buffer (500 events by default). If events have rolled off, they are gone. For durable history, register the AgentAuditor subscriber.
Chain verification says the audit log is tampered
That is what it is for. Export the log, inspect the first broken row, and treat it as a security event. Do not "fix" the chain by regenerating hashes.
FAQ
Do I need Docker?
No. Docker is the default because it is the fastest path to first event. The bus is a single Python package. You can run it with pip install hookbus && hookbus serve.
Does HookBus need the cloud?
No. The bus, the dashboard, every Apache 2.0 subscriber, and the Enterprise bundle all run fully on-host. The only external dependency at install time is pulling Docker images.
Can I use HookBus without AgentProtect CRE Light?
Yes. HookBus Light ships with AgentProtect CRE Light and AgentSpend as examples, but the bus has no opinion on which subscribers you register. Delete them from subscribers.yaml and the bus keeps routing to whatever else is there.
Can I use AgentProtect CRE Light without HookBus?
AgentProtect CRE Light is a HookBus subscriber. Outside HookBus it is just a Python service; you can POST events to it directly, but you lose consolidation, cross-subscriber context, and the whole point of the bus.
Is it free?
HookBus Light and all shipping subscribers under Agentic Thinking’s repos are Apache 2.0 or MIT. Free, commercial use OK, self-host forever. HookBus Enterprise and the licensed subscriber bundle (AgentAuditor, AgentKnowledge, AgentProtect CRE Enterprise) are commercial. See agenticthinking.uk/enterprise.html.
Can I build a subscriber in my language?
Yes. The subscriber contract is "serve HTTP POST, return JSON". Go, Rust, Elixir, Ruby, PHP, Bun, Deno, anything that binds a port. Submit a PR to the public registry once you ship.
Does HookBus support async / observer subscribers?
Yes. Set mode: async in subscribers.yaml. The bus fires and forgets, it does not wait for your response. Use for cost tracking, metrics, memory writes, anything that should not block the agent.
What about verdict conflicts between subscribers?
Deny wins. Always. If AgentProtect CRE Light says deny and every other subscriber says allow, the consolidated response is deny. Reasons from all subscribers are surfaced so the model knows why.
How do I verify the audit chain?
python3 audit_trail.py --verify
# or from the bus container:
docker exec hookbus-auditor python3 audit_trail.py --verify
Returns exit code 0 on a valid chain, non-zero with the first broken row if the chain was tampered.
Where is the protocol spec?
On GitHub: HOOKBUS_SPEC.md. The spec is versioned (v0.1 at time of writing) and semver’d for breaking changes.