Skip to content

Command line reference

hookspool [--home DIR] [--config FILE] [--json] <command> [options]
hookspool --version

Every command is a subcommand of one program, and every service is the same program with a different command. The CLI is checked against this page by tests/test_docs.py: a command or option that the code has and this page lacks, or the other way round, fails the suite.

Global options and environment

option meaning
--home DIR state directory. Default: $HOOKSPOOL_HOME, then [spool] home in the config, then /var/lib/hookspool
--config FILE the config file. Default: $HOOKSPOOL_CONFIG, then <home>/config.toml
--json machine-readable output for the commands that print a table or a sentence
--version print hookspool <version> and exit
variable meaning
HOOKSPOOL_HOME state directory (below --home, above the config and the default)
HOOKSPOOL_CONFIG path of the config file (below --config)
HOOKSPOOL_DB path of the spool file, overriding [spool] db
HOOKSPOOL_LOG json for one JSON object per log line; anything else is the text form
HOOKSPOOL_ALERT_DRY set to anything but 0 to record alerts instead of running alerts.argv (tests, rehearsals)
HOOKSPOOL_BOBBIN, HOOKSPOOL_BOBBIN_INSTANCE, HOOKSPOOL_BOBBIN_DIR set by the dispatcher for a bobbin's processor (python -m hookspool.processor <instance>, ARCHITECTURE 14.3.4): the bobbin, the instance and the bobbin directory. Never set by hand, and never read from the dispatcher's own environment
HOOKSPOOL_SETTINGS, HOOKSPOOL_SECRET_REFS, HOOKSPOOL_DELTA_ROUTES also set for a processor: its typed settings, the references (never values) of the secrets it may read, and the destinations of its event types (its output map), each as JSON

Precedence for paths is always: option, then environment, then config file, then default.

A missing config file means the defaults (no sources). A config file that exists but does not parse is an error: running on defaults would hide a broken deploy.

Exit codes

code meaning
0 done
1 not found, or nothing to do (an empty list, replay of a batch that is not dead)
2 configuration or schema error (also: the intake could not bind its port)
3 dispatch.lock is held: a dispatcher is already running

Services

hookspool intake

Runs the HTTP intake: a server bound to the loopback address in [intake] listen, serving /hooks/<source>, /healthz and /metrics. It never applies schema migrations; on a spool older than the code it exits 2 and says to run hookspool migrate. It reloads the config when the file's modification time changes (checked every 5 seconds).

hookspool dispatch

Runs the dispatcher, the only process that delivers. It takes an exclusive lock on <home>/dispatch.lock; a second dispatcher exits 3. It applies pending migrations at start, runs retention once an hour and writes a heartbeat that status and /healthz report.

hookspool run

The intake and the dispatcher in one process, for development and tiny hosts. Production runs the two commands as separate services so that either can be restarted without the other.

Looking

hookspool status

Prints one JSON document: whether the spool is writable, the dispatcher heartbeat, batch counts by state, the oldest pending age, each destination's breaker state and each source's last event age. It is the same document /healthz serves. degraded lists soft problems in words ("dispatcher has not run", dead batches, an open breaker, a silent source, a stale poll).

polls has one entry per poll instance (poll bobbins): every_s, next_at and next_in_s (when the next tick is due), last_tick_at, delivered_at, delivered_tick_at and delivered_age_s (the last tick the poller finished), pending (a tick is still waiting or dead, so the next window is skipped), skipped (how many windows were skipped that way) and stale (no tick delivered for three times every_s; the issue poll:<instance>:stale is open).

hookspool list

Batches, newest first, one line each, without payloads.

option meaning
--state STATE one of open, sealed, leased, retry, delivered, dead, dropped
--source SOURCE only this source
--destination DESTINATION only this destination; quarantine lists quarantined requests
--limit N how many rows (default 50)

Exit 1 when nothing matches.

hookspool show

One batch: its state, window, counts, timestamps and last error.

option meaning
batch the batch id (positional)
--events list the batch's events: id, type, size, dedup id, and whether it is excluded
--payload include the payloads. They may hold personal data or secrets; a warning is printed to a terminal
--attempts include the delivery attempts: outcome, class, status, duration, error

hookspool metrics

Prints the same text that GET /metrics serves (Prometheus format), without needing the intake to be up.

hookspool export

Writes batches with their events and attempts as JSON lines to standard output: one batch per line. Each event carries its payload and, when stored, its raw body (base64), so treat the output as sensitive. It is a forensic and portable copy, not a restore format. See Health and repair for backups.

option meaning
--state STATE only batches in this state (same values as list)
--since T only batches whose first event arrived at or after T (unix seconds)

Repairing

hookspool replay

Sends a dead batch back to retry, due now, with its attempt count reset. Its content stays exactly as it was frozen. A batch replayed this way is delivered ahead of newer batches of its key and so can arrive out of order; the command says so.

On a quarantine batch (a request that a plugin could not classify) replay re-runs classification through the plugin as it is now: each stored request becomes a normal event, and the quarantine batch is marked delivered with the result reclassified. If the plugin still fails, nothing changes.

option meaning
batches batch ids (positional, any number)
--dead every dead batch that matches the filters below (quarantine batches are not included)
--destination D with --dead: only this destination
--source S with --dead: only this source
--since T with --dead: only batches whose first event is at or after T (unix seconds)

hookspool retry

Takes a batch that is waiting out a backoff (retry) and makes it due now. The same command is how you skip the wait after you fix the thing that was failing.

option meaning
batch the batch id (positional)

hookspool split

Cuts a sealed, retry or dead batch into smaller sealed batches of the same key. The original becomes dropped with the result split into .... Use it when a destination refuses a batch as too large. Give exactly one option.

option meaning
batch the batch id (positional)
--max-events N at most N events per new batch
--max-bytes B at most B bytes of payload per new batch
--at EVENT_ID two batches: events before EVENT_ID, and EVENT_ID onward

hookspool exclude

Marks events so they are omitted from the batch's document. The event stays stored and listed by show --events; the batch's counts and content hash are recomputed. Use it to remove the one payload that a destination chokes on. Works on open, sealed, retry and dead batches.

option meaning
batch the batch id (positional)
event_ids one or more event ids (positional)
--yes required; confirms that you mean it

hookspool drop

Moves batches to dropped, the operator's discard. Nothing in hookspool ever does this on its own. Dropped batches are pruned after retention.dropped_days.

option meaning
batches batch ids (positional, any number)
--yes required
--reason TEXT recorded with the batch

hookspool pause

Stops delivery to a destination. Events keep arriving, coalescing and sealing; they wait in the spool. Pausing is per destination, never per route or source.

option meaning
destination for example hermes:github-ci (positional)
--reason TEXT shown in status

hookspool resume

Lifts a pause (and clears a circuit breaker that was open), so the destination delivers again.

option meaning
destination the destination name (positional)

hookspool issues

Failures are grouped into issues, one per destination and cause. issues lists them; the other actions change one.

option meaning
action list (default), ack, resolve or ignore (positional)
id the issue id, for ack, resolve and ignore (positional)
--state STATE for list: only open, acked, resolved or ignored issues

ack says you have seen it. resolve closes it (it reopens and alerts again if the cause recurs). ignore is permanent silence for that cause: it never alerts or reopens, so use it sparingly.

hookspool enqueue

Puts one event in the spool by hand, with the same validation as the intake. It is how a maintenance sweep reports through hookspool and how you test a route without a sender.

option meaning
--source S required: the source name
--key K required: the coalesce key
--destination D where to deliver. Default: the --route's destination, else the source's action
--route R a configured route name
--event-type T an event type label
--dedup-id ID a dedup id, so a repeat inside the source's window is reported as a duplicate
--file F JSON payload file. Default: standard input

Checking and maintaining

hookspool check

Validates the config: unknown keys, bad values, the byte limits that must nest, secret references (readable, and mode), plugins (imported and bound), destinations, and the Hermes subscriptions file. It also runs SQLite's quick check on an existing spool. Prints ok and exits 0, or prints the errors and exits 2. The service units run it before starting.

hookspool reload

Runs check and, if it passes, touches the config file so that the running intake and dispatcher both notice and reload together. An invalid file keeps the previous config in the running services.

hookspool migrate

Applies pending schema migrations. It takes the dispatcher's lock, so it refuses (exit 3) while a dispatcher runs.

option meaning
--dry-run print the current schema version and the pending migrations, change nothing

hookspool prune

Runs retention now instead of waiting for the hourly pass. It prints how many delivered batches, dropped batches, events, attempts, dedup rows and issues it deleted. Dead batches are never pruned.

option meaning
--vacuum also run VACUUM to return space to the filesystem. Needs free space of about the size of the spool file

hookspool bobbin

Bobbins (see Architecture, section 14.3). The subcommand is one of list, show, expand, new and check:

form does
bobbin list the bobbins found (built in, then each [spool] bobbin_path directory): name, version, where it comes from, summary, and the [bobbins.<instance>] tables that use it
bobbin show <vendor>.<product> one bobbin: its provider and the provider's pages, its secrets, settings (type, required, default), outputs, span, whether the sender retries, the reverse-proxy note, and the rule packs a bobbin ships (google.gmail, see Mail rules)
bobbin expand [<instance>] the ordinary tables one instance (or every instance) expands to, as TOML. Secret references stay references and HOOKSPOOL_SECRET_REFS is shown by secret name only: no secret value is ever read or printed. Exit 2 with every problem when the bobbins do not expand
bobbin new <vendor>.<product> a new bobbin directory, <dir>/<vendor>/<product>/ (--dir, default the current directory; a new vendor directory in a Python package gets an empty __init__.py): a copy of the hookspool.webhook bobbin with its settings frozen into a fixed [source] template (--set chooses them), a [provider] table, an accepted and a rejected capture signed for the scheme, README, SETUP.md and AGENTS.md with their frontmatter. It passes bobbin check once its TODO placeholders are filled in
bobbin check <dir> run the conformance kit on one bobbin directory and print its scorecard: each check, pass / fail / skip, and every problem as file:line with how to fix it. Exit 0 when every required check passes, 1 when one fails, 2 when <dir> is not a bobbin. It reads no config and no real secret. CI runs this same command on every changed bobbin (Reviewing a bobbin)

name is the bobbin (<vendor>.<product>) for show, the instance for expand, the new bobbin's <vendor>.<product> name for new and the bobbin directory for check.

option meaning
--dir D new: the parent directory; the bobbin goes in D/<vendor>/<product>/ (default: the current directory)
--set NAME=VALUE new: freeze a webhook setting (scheme, header, prefix, key, dedup, ...) into the template; the value is TOML (--set 'key=["id"]') or a bare string, an empty value removes the key, and a key that takes only a string keeps the text as written (--set prefix=1 is the string 1); repeatable. callback_url (hmac-sha1-base64-url) is used for the captures and the kit instance but stays a required setting of the new bobbin, not part of its template. A value the template refuses stops new with the template's message
--code new: a source.py instead of the template
--processor new: include a processor.py and a [processor] table
--poll new: a poll bobbin for a service with no push at all: a [poll] table instead of [source], a poller.py that makes a conditional GET, a captures/poll-example.json of stub API answers, and the documents with the poll frontmatter. It takes no --code, --processor or --set
--markdown list: print the catalogue table of docs/bobbins/README.md (name, linked to its README, and the manifest's summary), generated from the manifests
--report text\|md\|json check: the scorecard as aligned text (default), GitHub Markdown, or JSON (schema hookspool.bobbin-check/1)
--strict check: advisory checks (provider-links, scope) fail the run too
--base REF check: the git ref the change is compared with; the advisory scope check lists files changed outside the bobbin
--online check: let the advisory provider-links check fetch each [provider] URL (HEAD, then GET; 10 s; 2xx/3xx is fine). Off by default, so a local run sends nothing; CI passes it

hookspool source

Source tools.

option meaning
test the only subcommand (positional)
name the source name, as in [sources.<name>] (positional)
--request FILE required: a .http capture (format in Writing a bobbin)
--now T unix seconds to verify at. Default: the capture's # now: line, else the current time

source test replays a captured request through the source's verify, handshake, ignore, key, dedup and attrs stages offline. It never writes to the spool. It prints a JSON report whose result is queued, ignored, challenge or unverified (a queued report names the route and destination the event would take, and in tees each tee route that would get a copy), and exits 0 for the first three, 1 for unverified, 2 when the capture or the plugin cannot be loaded.