Workstation-local event bus and blocking wait/emit primitive: let any agent or script wait on or emit CI, pytest, Docker, and filesystem events without polling. GitHub Actions is source #1.
Reason this release was yanked:
Superseded by 0.1.1 (adds MCP Registry listing metadata); no functional difference
Project description
waitbus — the workstation-local, cross-harness status bus: D-Bus for agents, with a replay log and a wait primitive
One wait verb that blocks on, and queries across, every local source at once. When any agent, CI job, test, or container on your machine finishes or fails, every other tool on the box hears it: your Claude Code, your Cursor, your scripts, your CI. Clients that support server notifications get a push; the rest get one blocking waitbus wait. No waitbus cloud, no account, no telemetry — all processing stays on your machine.
Below: a Pydantic AI agent and a LangGraph agent — two different frameworks, two separate OS processes — both wait on one local waitbus bus. One fails; the peer on the other framework, plus a live waitbus top view, react to the single failure broadcast.
Real frameworks and real waitbus subscribe/emit; the agents' LLMs are deterministic fakes and the failure is an injected event, so the clip runs fully offline. Reproduce with make hero; higher-quality MP4 at hero.mp4. For a gentler start, see the single-agent waitbus demo in Try it in 60 seconds.
waitbus is the workstation-local, cross-harness status bus that lets the tools on one machine hear each other. It does three things: ingest events, broadcast them, and replay them.
Ingest: a blocking wait/emit primitive lets any agent or script
(across Cursor, Claude Code, and any tool on the box) wait on, or emit, events
from five built-in sources (GitHub Actions CI, pytest sessions, Docker
container lifecycle, filesystem changes, and Prometheus Alertmanager) plus any
plugin source registered under the waitbus.sources.v1 entry-point group.
Broadcast: events land in SQLite the moment they arrive, and a broadcast
daemon fans each row to every connected consumer within about a
millisecond on the local socket (measured; see benchmarks/BENCHMARKING.md). Your
agent blocks on waitbus wait (including across sources at once with
--all-of/--first-of) with zero agent-side polling and idle CPU until the thing it
cares about happens, and waitbus on <predicate> -- <command> runs a command
the moment it does (in the CLI process, not the daemon, via execve with no
shell; event values arrive as namespaced WAITBUS_* environment variables and
a $WAITBUS_EVENT_FILE, never as argv, so event content cannot inject flags
or overwrite PATH). Because every agent on the box shares that one bus, it
doubles as a same-machine coordination backplane: one agent emits, the others
wake.
Replay: a durable replay log (since=) means a consumer that was
offline catches up instead of missing the moment, and waitbus events analyze
queries what the bus stored. It is the local opt-in
event-broadcast model the OS already uses (D-Bus signals, inotify, journald),
with the replay log and a wait predicate added on top. (GitHub Actions was
the first source wired, which is why the examples lead with CI, but waitbus is
not CI-only.) The dependency stack is lean: typer, msgspec, platformdirs,
pydantic-settings, prometheus_client, stamina, and the mcp SDK, with no
heavy TUI or crypto dependency. The daemon idles at roughly 40 MB RSS measured
(p50 on the benchmark host, most of it the CPython interpreter baseline) over
a SQLite event store and an AF_UNIX SOCK_STREAM broadcast bus with
length-prefix framing. Runs on Linux (systemd-user) and macOS (launchd).
Who this is for
This is for you if…
- You run a heterogeneous agent fleet — Claude Code and Cursor and background scripts — on one workstation and want them to hear each other's finishes and failures.
- You wait across sources — "block until the tests pass and the build is
done and CI is green," in one predicate, in a script
(
waitbus wait --all-of ...). Nothing else expresses this. - You want zero-LLM-token, zero-poll waiting as a daily habit across many local things, and you'll run a daemon to get it.
- You care that the daemon on your box is trustworthy — reproducible builds, offline, no cloud, no account, no telemetry.
This is NOT for you if…
- You wait on one GitHub repo's CI and nothing else. Use
gh run watch— it's zero-setup, and for that single job we don't beat it by enough to justify a daemon. - You only need to react to a file change. Use
inotifywait/entr— they're far faster on raw filesystem latency (our own benchmark says so) and need no daemon. - You want cloud, cross-machine, or team coordination. Out of scope for the local core by design.
- You want addressed agent-to-agent messaging as the product (threads,
inboxes, routing). The headline here is broadcast source-ingestion; the
request()/respond()facet exists, but a dedicated message queue serves that center of gravity better.
Try it in 60 seconds
uvx waitbus demo # one command, no install needed
waitbus demo allocates a temporary state directory and boots the
broadcast daemon, then runs two phases. Phase 1 — the point: an
agent blocks on waitbus wait (the same egress engine the real command
uses) with zero polling and idle CPU; the moment a github
workflow_run event lands, the wait returns and the demo prints the
real measured event-to-unblock latency. Phase 2 — breadth: the same
primitive delivers every source — pytest_session, docker_container,
and fs_change events fan out to a live subscriber. Nothing on your
machine outside the temporary directory is touched.
A recorded MP4 + GIF walkthrough lives at
docs/demo/.waitbus-demo/demo.mp4 and demo.gif
(the tape script is demo.tape). Re-rendering is reproducible
via make demo from that directory (requires
VHS ≥ 0.10.0, ttyd, and
ffmpeg); the Makefile enforces the VHS version floor and refuses
to render against an older binary that could silently change tape
semantics.
The four events the demo emits are synthesized in-process — there is no real HTTP listener, no real pytest run, no real Docker daemon, no real watchdog. A banner before each emit makes this explicit, mirroring the
waitbus statsoutput. To wire waitbus against real GitHub webhook deliveries, follow the Quick start below.
Quick start
uv tool install waitbus # install the package
waitbus init # one-time setup: state dirs, SQLite schema, scaffold files
waitbus install-credentials github-webhook-secret # encrypt + stage HMAC secret via systemd-creds
waitbus install-systemd # Linux: copy + enable the 8 systemd-user units
waitbus install-launchd # macOS: copy + bootstrap the 4 LaunchAgent plists
waitbus read-events watch # live tail of incoming events
install-systemd is Linux-only; install-launchd is macOS-only.
Each command refuses to run on the other platform and points at the
right one.
Once events are flowing, block any script or agent on the next
matching event with waitbus wait -- any source, any field, exit code
carries the verdict:
# pytest: wait for any session to finish (zero setup -- local source)
waitbus wait --source pytest --match 'fields.event_type="pytest_session"' --timeout 10m
# Docker: wait for a container to exit (zero setup -- local source)
waitbus wait --source docker --match 'fields.event_type="docker_container"' --timeout 30s
# Filesystem: wait for a watched path to change (zero setup -- local source)
waitbus wait --source fs --match 'fields.event_type="fs_change"' --timeout 5m
# GitHub CI: wait on a commit's terminal conclusion (needs webhook wiring -- see below)
waitbus wait --sha 7f3a1b2 --timeout 5m
If you have used watchexec or entr to
re-run a command when a file changes, waitbus wait is the same idea with a wider
net: it blocks on a file change too, but it can also wait on a CI run, a pytest
session, a container exit, or another agent's event, and hands the verdict back
through the exit code. The bus underneath also
fans every one of those events out to every other tool on the box.
Architecture
%%{init: {'theme':'neutral','themeVariables':{'fontSize':'16px','fontFamily':'sans-serif'},'flowchart':{'nodeSpacing':28,'rankSpacing':46,'padding':14}}}%%
flowchart TB
WH["<b>Webhooks</b><br/>GitHub · Alertmanager"]
IP["<b>In-process</b><br/>pytest · docker · fs<br/>agents · plugins"]
LS["<b>listener serve</b><br/>HMAC verify · etag-poll"]
DB[("<b>SQLite event store</b><br/>seq PK<br/><code>INSERT OR IGNORE</code>")]
BD["<b>broadcast daemon</b><br/>AF_UNIX fan-out<br/>doorbell wake · length-prefixed frames"]
CLI["<b>CLI</b><br/><code>wait · on · top · read-events</code>"]
MCP["<b>MCP server</b><br/><code>mcp serve</code>"]
SDK["<b>Python SDK</b><br/><code>subscribe · wait_for</code>"]
WH -->|webhook| LS --> DB
IP -->|emit| DB
DB -->|doorbell| BD
BD --> CLI
BD --> MCP
BD --> SDK
CLI -.->|since= replay| BD
SDK -.->|since= replay| BD
classDef src fill:#ffffff,stroke:#5a6672,stroke-width:1.5px,color:#1a1a1a
classDef store fill:#f3ecdc,stroke:#a9772f,stroke-width:2px,color:#1a1a1a
classDef core fill:#dbe7f3,stroke:#2f6690,stroke-width:2.5px,color:#1a1a1a
classDef sink fill:#f4f6f8,stroke:#3d4752,stroke-width:1.5px,color:#1a1a1a
class WH,IP,LS src
class DB store
class BD core
class CLI,MCP,SDK sink
Two ingress classes land events in the same store: remote webhooks (GitHub,
Alertmanager) arrive over HTTPS and pass through waitbus listener serve, while
in-process sources (pytest, docker, fs, plugins, and any agent) call the
public emit() API to write the row directly — no listener, no network hop.
An ETag polling fallback runs on a 45-second timer for repos you cannot receive
webhooks for (upstreams, forks). Every path writes through the same
INSERT OR IGNORE, so redeliveries are idempotent, and every row rings the
broadcast doorbell so subscribers wake within about a millisecond regardless of which
ingress produced it.
How waitbus relates to MCP Tasks (task=True) and to the local agent-to-agent
buses (agent-message-queue, claude-code-inter-session): see
docs/COMPETITIVE_LANDSCAPE.md.
Installation
As a Python CLI tool
uv tool install waitbus # or: pipx install waitbus
waitbus init # one-time setup: state dirs, SQLite schema, scaffold files
waitbus install-credentials github-webhook-secret # encrypt + stage HMAC via systemd-creds
waitbus install-credentials alertmanager-hmac # optional: alertmanager / watchdog HMAC
waitbus install-systemd # Linux: copy + enable the 8 systemd-user units
waitbus install-launchd # macOS: copy + bootstrap the 4 LaunchAgent plists
Each command is idempotent. Re-running install-credentials <name> rotates
the credential (re-configure the GitHub webhook and Alertmanager after
rotating, then systemctl --user restart waitbus-listener.service).
After install you have one console script on PATH: waitbus. All
daemon entry-points and admin commands are reachable via sub-commands:
| Sub-command | Purpose |
|---|---|
waitbus init |
Bootstrap state dirs, SQLite schema, scaffold files |
waitbus install-systemd |
Linux: copy + enable systemd-user units |
waitbus install-launchd |
macOS: copy + bootstrap LaunchAgent plists |
waitbus install-credentials |
Encrypt a credential via systemd-creds and stage it for LoadCredentialEncrypted= |
waitbus doctor |
Validate install (paths, binaries, credential store, units, /metrics) |
waitbus status |
Operational dashboard: DB row count, daemon liveness |
waitbus verify-plugin |
Validate .claude-plugin/plugin.json |
waitbus listener serve |
HTTP webhook receiver (loopback :9000) |
waitbus broadcast serve |
AF_UNIX broadcast hub daemon |
waitbus etag-poll run |
GitHub API ETag poll worker |
waitbus mcp serve |
MCP server (stdio) re-emitting broadcast events as notifications |
waitbus read-events watch |
Subscribe to broadcast bus, stream events live |
waitbus read-events list |
Print recent events from the local cache |
waitbus pr-monitor tick |
Roll job events into per-PR state |
waitbus watchdog-check run |
Ingestion-silence detector |
And, depending on platform, either 8 systemd-user units in ~/.config/systemd/user/ (Linux):
waitbus-listener.service waitbus-broadcast.socket
waitbus-broadcast.service waitbus-etag-poll.service
waitbus-etag-poll.timer waitbus-watchdog.service
waitbus-watchdog.timer waitbus-forward@.service
…or 4 LaunchAgent plists in ~/Library/LaunchAgents/ (macOS):
dev.waitbus.listener.plist dev.waitbus.broadcast.plist
dev.waitbus.etag-poll.plist dev.waitbus.watchdog.plist
The macOS plists carry __BIN_DIR__ / __LOG_DIR__ / __RUNTIME_DIR__
placeholders that waitbus install-launchd resolves to the
operator's actual paths before writing. Agents are loaded via
launchctl bootstrap gui/$UID <plist> (the modern replacement for
the deprecated launchctl load -w).
As a Python library
from waitbus import subscribe, wait_for, EventFrame
# Block until one matching event arrives (returns the frame, or None on timeout).
frame: EventFrame | None = wait_for('fields.event_type="pytest_session"',
source="pytest", timeout=600)
if frame is not None:
print(frame.event_type, frame.fields)
# Or stream every matching event as it lands.
for frame in subscribe(source="docker"):
print(frame.event_type, frame.fields)
The stable public API is emit, subscribe, asubscribe, wait_for,
request, respond, EventFrame, and the plugin hooks register_source,
register_condition, register_evaluator (full signatures in
docs/CONSUMER_API.md). To produce events, the
waitbus emit CLI is the simplest path; see examples/emitters/ for the
emit() Python form. Everything under a leading underscore (waitbus._db,
waitbus._paths, …) is a private internal and may change between releases.
Runtime dependencies (the full mandatory set, same as the lean stack
above): typer (umbrella CLI), msgspec (wire framing), platformdirs
(state and runtime path resolution), pydantic-settings (config
loader), prometheus_client (metrics), stamina (retry policy), and
the mcp SDK (MCP server). The two consumer-facing extras
(waitbus[analyze], waitbus[fs]) pull duckdb / watchdog lazily and
are the only optional packages. All other internal modules are pure
stdlib — no network at import time, no C extensions, safe to import
in any Python 3.11+ environment. The package root logger uses
NullHandler so library consumers never see leaked log records.
As an MCP server (PyPI + uvx)
waitbus mcp serve is a stdio MCP server (JSON-RPC 2.0 over the spec at
https://spec.modelcontextprotocol.io/). What a given client can do with it
depends on which MCP features that client implements — so here is the actual
capability split:
- Pull — any MCP client that supports Tools. Call waitbus tools to query
status and to block-wait for events;
tail_eventsis a bounded long-poll, so even waiting needs no polling loop. This is the broadly portable path. - Push — clients that support resource subscription. waitbus emits the
spec-standard
notifications/resources/updatedforwaitbus://repo/...subscribers. Whether your client surfaces that push (rather than only calling tools) depends on the client; most coding-agent MCP clients today are pull-only — the published client matrices track Tools / Resources / Prompts, not a notification-surfacing column. - Inline render — Claude Code. waitbus also emits an Anthropic-private
notifications/claude/channelthat Claude Code renders inline; other clients ignore the unknown method harmlessly per JSON-RPC 2.0.
Verified with — an executable test exercises the path end-to-end, not just
"speaks the spec": Claude Code (the MCP emit path, tests/test_mcp_e2e.py);
and, via the public Python SDK (waitbus.wait_for / subscribe), a
Pydantic AI and a LangGraph agent (tests/test_agent_integration_*.py);
plus the language snippets and the bash waitbus wait wrapper. Any other
stdio-MCP client can use the pull path above — that is config, not a tested
end-to-end guarantee.
The command is always uvx --from waitbus waitbus mcp serve; only WHERE each client
stores its MCP config differs. Those locations and formats move across client
versions, so treat the table below as a starting point and check your client's
current MCP docs:
{
"mcpServers": {
"waitbus": {
"command": "uvx",
"args": ["--from", "waitbus", "waitbus", "mcp", "serve"]
}
}
}
| Client | MCP config location (verify against the client's current docs) |
|---|---|
| Claude Code | ~/.claude/.mcp.json (or per-project ./.mcp.json) |
| Claude Desktop | ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows) |
| Cursor | ~/.cursor/mcp.json (or .cursor/mcp.json per project) |
| Cline (VS Code) | ~/.config/Cline/cline_mcp_settings.json (Linux), or per-workspace .vscode/cline_mcp_settings.json |
| Codex CLI (OpenAI) | ~/.codex/config.toml (under a [mcp_servers.waitbus] table — Codex uses TOML, not JSON) |
| Gemini CLI (Google) | ~/.gemini/settings.json (under the mcpServers key) |
| Continue | ~/.continue/config.json (under the mcpServers key) |
Aider is intentionally absent: as of 2026-05 Aider ships no native MCP client (tracked upstream in aider-AI/aider FR #4506; the only MCP path is the separate AiderDesk companion app). Point Aider-style or non-MCP agents at waitbus through the Python SDK (
waitbus.wait_for) or thewaitbus waitCLI instead.
uv must be on PATH (curl -LsSf https://astral.sh/uv/install.sh | sh).
See platform support.
As a Claude Code plugin
claude --plugin-dir ~/.local/share/waitbus/plugin
Or use the marketplace entry once it is published to the MCP Registry (see
CHANGELOG.md for current status). The plugin enables /waitbus slash
commands and the waitbus skill inside any Claude Code session.
Usage
Reading events
# Live tail — one line per event; monitoring-friendly cadence
waitbus read-events watch
# Query the last hour in JSON (pass extra flags after the sub-command)
waitbus read-events list --since 1h --json
# Roll job events into per-PR state (watch PRs 7 and 9)
waitbus pr-monitor tick --pr 7 --pr 9
Key features
-
Sub-second
workflow_jobfailure detection. Catches matrix-cell failures within sub-second of GitHub delivering the webhook (wall-clock from webhook receipt, not from the CI failure itself) — no waiting for the parentworkflow_run.completedto arrive. Individual job failures surface immediately even when other matrix cells are still running. -
SQLite event store with idempotent writes.
INSERT OR IGNOREonevent_id(ULID, assigned at ingest time). Webhook redeliveries and ETag-poll duplicates are silently discarded; the DB stays consistent without a dedup queue. -
AF_UNIX
SOCK_STREAMbroadcast bus with length-prefix framing. Each wire frame is a 4-byte big-endian length followed by the JSON payload — portable across Linux and macOS (whereSOCK_SEQPACKETis unavailable). Non-blocking sockets with per-subscriber lag counters; slow subscribers are closed afterLAG_LIMITconsecutive drops and reconnect via EOF-triggered cursor reset. -
HMAC-SHA256 webhook signature verification. Secret staged once via
waitbus install-credentials github-webhook-secret(encrypted bysystemd-creds, decrypted by systemd into$CREDENTIALS_DIRECTORYat unit-start). Missing or invalid signatures are rejected with 401 before the payload is read. -
SIGTERM drain guarantee. The broadcast daemon stops accepting new subscribers on SIGTERM, drains all in-flight frames to connected subscribers, closes them with EOF, unlinks its socket files, and exits 0. No events are dropped on
systemctl restart waitbus-broadcast. -
ETag polling fallback. A 45-second timer polls repos listed in
watched_repos.txtusingIf-None-Matchto avoid burning GitHub API rate limits. Coexists with webhook-driven repos. -
Prometheus metrics endpoint.
http://127.0.0.1:9000/metricsexposes ingestion-event and ingestion-error counters in Prometheus text format. The watchdog timer detects and records ingestion silence.
Configuration
Config file (optional): ~/.config/waitbus/config.toml
[prometheus]
owner = "my-org"
repo = "my-repo"
Environment variable overrides:
| Variable | Purpose |
|---|---|
WAITBUS_PROM_OWNER |
Override Prometheus alert label: owner |
WAITBUS_PROM_REPO |
Override Prometheus alert label: repo |
WAITBUS_STATE_DIR |
Override the state directory (SQLite DB, scaffolds, cursors). Default resolved via platformdirs. |
WAITBUS_RUNTIME_DIR |
Override the runtime directory (broadcast + doorbell AF_UNIX sockets). Default resolved via platformdirs. |
State directory resolution
Path resolution lives in waitbus._paths and follows the same
first-hit-wins precedence as the rest of the config chain:
WAITBUS_STATE_DIR/WAITBUS_RUNTIME_DIRenv vars (operator-controlled).platformdirs.PlatformDirs("waitbus")per-platform defaults:- Linux: state at
~/.local/state/waitbus/(honoursXDG_STATE_HOME); sockets at/run/user/$UID/waitbus/(honoursXDG_RUNTIME_DIR). - macOS: state at
~/Library/Application Support/waitbus/; sockets at a stabletempfile.gettempdir()-derived path (platformdirs' macOSuser_runtime_diris Apple-evictable cache, unsuitable for AF_UNIX sockets).
- Linux: state at
The shipped systemd units use StateDirectory=waitbus and
RuntimeDirectory=waitbus, so on Linux systemd creates the
directories with 0700 ownership before the daemons start — no
operator mkdir required.
Credentials (staged via waitbus install-credentials <name>):
| Credential name | Required | Purpose |
|---|---|---|
github-webhook-secret |
Yes | HMAC secret for GitHub webhook signature verification |
alertmanager-hmac |
No | HMAC secret for Alertmanager / watchdog webhook verification |
broadcast-token |
No | Optional bearer token for broadcast subscriber auth |
Each credential is encrypted by systemd-creds encrypt --name=<name> and
written to /etc/credstore.encrypted/waitbus.<name>.cred. systemd
decrypts it into $CREDENTIALS_DIRECTORY/<name> at unit-start; the daemon
reads the plaintext from there. waitbus doctor reports whether each
encrypted file is present without decrypting it. It exits 1 if any required
piece is missing.
Security
See SECURITY.md for the full HMAC threat model, platform assumptions, and vulnerability reporting process.
Same-UID trust model. The broadcast bus has no inter-process
confidentiality: any process running as your user can read every event on the
bus and can emit events under any sender name. waitbus is a single-user
workstation tool, not a security boundary between processes you run. Peer
authentication is by kernel-verified UID (SO_PEERCRED), so a different user
cannot connect — but same-user isolation is out of scope by design.
To verify the release artifacts:
gh attestation verify $(python -c "import waitbus; print(waitbus.__file__)") \
--repo astrogilda/waitbus
See SECURITY.md for the full provenance verification procedure including SLSA, PEP 740, and SBOM checks.
Support
For bug reports, feature requests, and questions, see SUPPORT.md — it lists the security, abuse/conduct, and general channels and sets single-maintainer, best-effort expectations. Security vulnerabilities go through the private process in SECURITY.md, never a public issue.
Operating
# Start the core daemons
systemctl --user start waitbus-listener.service
systemctl --user start waitbus-broadcast.socket
# Start the background timers
systemctl --user start waitbus-etag-poll.timer
systemctl --user start waitbus-watchdog.timer
# Health check — exits 1 on any missing required config or stopped unit
waitbus doctor
# Watch webhook deliveries arrive in real time
waitbus read-events watch
Enable units to start automatically on login:
systemctl --user enable waitbus-listener.service waitbus-broadcast.socket \
waitbus-etag-poll.timer waitbus-watchdog.timer
Rotate the webhook HMAC secret:
# Encrypt the new value with systemd-creds and overwrite the staged file.
openssl rand -hex 32 | waitbus install-credentials github-webhook-secret
# Then update the GitHub repository webhook secret and Alertmanager config.
systemctl --user restart waitbus-listener.service
Headless server (SSH-only / no graphical session)
On a headless box without a graphical login session, two additional pieces are required for the systemd-user instance to run continuously even when no interactive session is open:
-
Keep the user manager alive across logouts. Run once:
loginctl enable-linger <username>
Without this, the systemd user manager and all its units stop when the last interactive session ends.
-
No keyring unlock required. Credentials are stored as
systemd-creds-encrypted files under/etc/credstore.encrypted/and decrypted by systemd at unit-start using a host-bound key (TPM2 when available,/var/lib/systemd/credential.secretotherwise). There is no D-Bus session requirement and nopam_gnome_keyring.sodependency, so the daemon stack runs unmodified on a fully headless box.
Verifying the linger setting works. Open a fresh SSH session (do NOT start the units manually first) and run:
systemctl --user is-active waitbus-listener
The result should be active. If the unit is not active without a manual
systemctl --user start, the linger setting is not in effect.
XDG_RUNTIME_DIR on linger-enabled hosts. When linger is enabled,
systemd sets XDG_RUNTIME_DIR=/run/user/$(id -u) automatically and creates
the broadcast socket path beneath it. No operator action is required; the
path is available from the first systemctl --user call in any session.
Wiring up repositories
Local sources need no wiring. pytest, Docker, and filesystem events are
produced on the box itself -- once the daemons are running (Quick start above),
waitbus wait --source pytest|docker|fs works with zero per-repo setup. The two
subsections below are only for GitHub CI, which arrives over a webhook and
therefore needs a forwarder.
Event-driven (preferred for repos you own):
gh webhook forward \
--repo=<owner>/<repo> \
--events=workflow_run,workflow_job \
--url=http://127.0.0.1:9000/webhook \
--secret="<the same plaintext value you piped to install-credentials>"
gh webhook forward creates an ephemeral GitHub-side webhook and tunnels
deliveries to the local listener. The secret must match the plaintext you
staged via waitbus install-credentials github-webhook-secret.
For a systemd-supervised forwarder (the shipped waitbus-forward@.service
template), no operator action is needed: the unit declares
LoadCredentialEncrypted=github-webhook-secret:... and reads the decrypted
value from $CREDENTIALS_DIRECTORY/github-webhook-secret at unit-start.
ETag polling (repos you can read but cannot receive webhooks for):
Append owner/repo lines to watched_repos.txt under the resolved
state directory (waitbus doctor prints the path; on Linux without
WAITBUS_STATE_DIR overrides, that is ~/.local/state/waitbus/watched_repos.txt).
The 45-second timer picks them up automatically.
For SSH-only boxes, see Headless server
for the loginctl enable-linger requirement.
Platform support
| Component | Linux + systemd | macOS + launchd | Notes |
|---|---|---|---|
| Python listener | Yes | Yes | Loopback-bound, stdlib only |
| SQLite event store | Yes | Yes | Single-file DB, no server |
| Broadcast daemon | Yes | Yes | AF_UNIX SOCK_STREAM + length-prefix framing |
| Doorbell primitive | os.eventfd |
SOCK_STREAM ping | Inline-dispatched at the bind site |
| Peer-credential UID check | SO_PEERCRED |
getpeereid() via ctypes |
LOCAL_PEERPID deliberately not used (see SECURITY.md) |
| Install entry | waitbus install-systemd |
waitbus install-launchd |
Each refuses to run on the other platform |
| Process supervisor | systemd-user units | launchd LaunchAgents | 8 systemd units, 4 launchd plists, all driven via waitbus |
| MCP server | Yes | Yes | stdio MCP — runs in any spec-compliant client |
| Claude Code plugin | Yes | Yes | Plugin manifest in .claude-plugin/ |
Both platforms run the full daemon stack. Linux uses systemd-user units
with socket activation; macOS uses launchd LaunchAgents with manual
socket bind. The peer-credential UID check, doorbell primitive, and
install entry-point each dispatch inline at the leaf call site — there
is no IPCBackend-style abstraction layer, since those three leaf
sites are the only platform-divergent code.
Details in SECURITY.md.
Native Windows is not supported. waitbus's trust model rests on an
AF_UNIX socket plus a SO_PEERCRED same-UID peer check, which has no
Win32 equivalent. Windows users run waitbus under WSL2, which is a real
Linux kernel: pip install waitbus (or uv tool install waitbus) inside a
WSL2 distribution works exactly as it does on native Linux. See
docs/ARCHITECTURE.md for the platform rationale.
Zero default egress, no telemetry. The broadcast daemon, every
subscriber (waitbus wait, waitbus read-events, waitbus mcp serve), and
the emit() path make no outbound network calls — nothing is phoned
home. The only network surface is the optional inbound GitHub webhook
listener (loopback 127.0.0.1:9000, HMAC-verified), plus local OS keyring
access for credential storage. Outbound traffic happens only when you
explicitly opt into the ETag polling fallback (which calls the GitHub API
for repos you list). See SECURITY.md for the full network
surface.
Contributing
See .github/CONTRIBUTING.md for the per-surface dev guide, loading the plugin locally, version sync, and release flow.
License
waitbus is released under the MIT License. See LICENSE for the full text.
More
- CHANGELOG.md — version history and migration notes
- SECURITY.md — HMAC threat model, platform assumptions, reporting process
- .github/CONTRIBUTING.md — per-surface dev guide, loading the plugin locally
- docs/emitters/recipes.md — shell and docker emit recipes, plus the CloudEvents envelope mapping for external producers
- docs/emitters/claude-code-hook.md — emit a bus event on Claude Code session lifecycle hooks
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file waitbus-0.1.0.tar.gz.
File metadata
- Download URL: waitbus-0.1.0.tar.gz
- Upload date:
- Size: 749.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
be68021e8d16098bfe719ac5ac3dddf5996047795e3eb677cdd6cfb2b6b40aa0
|
|
| MD5 |
1296f41f5e3a648db7c5812db1e605f5
|
|
| BLAKE2b-256 |
9efc9b9c6b873eadae5b80be0a8194addbb97f3027e48724bffa430303374504
|
Provenance
The following attestation bundles were made for waitbus-0.1.0.tar.gz:
Publisher:
release.yml on astrogilda/waitbus
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
waitbus-0.1.0.tar.gz -
Subject digest:
be68021e8d16098bfe719ac5ac3dddf5996047795e3eb677cdd6cfb2b6b40aa0 - Sigstore transparency entry: 1841632270
- Sigstore integration time:
-
Permalink:
astrogilda/waitbus@793a96ff617b7d0d76af27f5dcc642ce553a84be -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/astrogilda
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@793a96ff617b7d0d76af27f5dcc642ce553a84be -
Trigger Event:
push
-
Statement type:
File details
Details for the file waitbus-0.1.0-py3-none-any.whl.
File metadata
- Download URL: waitbus-0.1.0-py3-none-any.whl
- Upload date:
- Size: 406.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
522fde2afb364feac876addd592699e44d2dc6eaab464c1193ba02a11827c803
|
|
| MD5 |
d548ec246cc58486d94df03cfc9401f3
|
|
| BLAKE2b-256 |
bac7256e0baac94de1cc1678fca43863367d5d5e28e8c21ca599093df4d051c3
|
Provenance
The following attestation bundles were made for waitbus-0.1.0-py3-none-any.whl:
Publisher:
release.yml on astrogilda/waitbus
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
waitbus-0.1.0-py3-none-any.whl -
Subject digest:
522fde2afb364feac876addd592699e44d2dc6eaab464c1193ba02a11827c803 - Sigstore transparency entry: 1841632479
- Sigstore integration time:
-
Permalink:
astrogilda/waitbus@793a96ff617b7d0d76af27f5dcc642ce553a84be -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/astrogilda
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@793a96ff617b7d0d76af27f5dcc642ce553a84be -
Trigger Event:
push
-
Statement type: