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

ConceptWhat it is
PublisherAnything that emits an event. Your agent, your CLI hook, your SDK wrapper.
EventA JSON envelope describing what the agent is about to do (or just did).
BusHookBus itself. Receives events, fans out to subscribers, consolidates responses.
SubscriberAnything that receives events. Sync subscribers return verdicts. Async subscribers observe.
DecisionOne of allow, deny, ask. Deny wins across all subscribers.
ConsolidationThe 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)
Three rules for subscribers

(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:

RuntimeTypeStatus
Claude CodeHook (subprocess)Live
AmpCodePlugin (TypeScript)Live
Hermes AgentPlugin (Python)Live
OpenClawExtensionLive
Codex CLI (OpenAI)HookLive
OpenCodeCLILive
Anthropic Agent SDKPython shimPrivate beta
OpenAI Agents SDKPython shimPrivate beta
CursorHookComing soon
Any HTTP clientWebhookAlways

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.

VariableScopeExample
HOOKBUS_URLhost-level (shell profile OK)http://localhost:18800
HOOKBUS_TOKENhost-level (shell profile OK)contents of ~/.hookbus/.token
HOOKBUS_SOURCEper-CLI, never in shell profileclaude-code, amp, hermes, …
Do not export HOOKBUS_SOURCE globally

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 opens
  • UserPromptSubmit, a user prompt is about to be sent to the LLM
  • PreToolUse, a tool call is about to execute
  • PostToolUse, a tool call just finished (tool_result populated)
  • 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:

  1. Any deny wins, regardless of how many allow responses come back.
  2. If no deny but at least one ask, the consolidated decision is ask.
  3. Otherwise allow.
  4. reason strings from all sync subscribers are concatenated into the final reason.
  5. metadata from all sync subscribers is merged into a single metadata object keyed by subscriber name.

Environment variables

VariableUsed byDefaultPurpose
HOOKBUS_URLpublishers,Bus endpoint (e.g. http://localhost:18800)
HOOKBUS_TOKENpublishers & subscribers,Bearer token read from ~/.hookbus/.token
HOOKBUS_SOURCEpublishers (per-CLI),Label for events emitted by this CLI (e.g. claude-code)
HOOKBUS_TIMEOUT_MSpublishers3000How long a publisher waits for a bus verdict before failing closed (or allow, depending on policy)
HOOKBUS_FAIL_MODEpublishersclosedclosed blocks on bus failure, open allows. Default is closed.
HOOKBUS_TOKEN_FILEbus & subscribers/root/.hookbus/.tokenLocation of the shared bearer token file

Error codes

CodeMeaningWhere it comes from
401Missing or invalid bearer tokenBus
400Malformed envelope (missing hook, source, or not valid JSON)Bus
413Envelope too large (default limit 1 MB)Bus
504A sync subscriber missed its timeout budgetBus
503Bus is live but no subscribers registeredBus
499Publisher abandoned the request before verdict returnedBus

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.