Admit signed webhook events to an agent pipeline without trusting the payload.
Webhook Guard is for operators who let billing, CRM, deployment, support, or automation systems trigger autonomous workers. It verifies HMAC-SHA256 signatures, rejects stale events, records accepted event IDs in SQLite, and quarantines malformed, forged, expired, or replayed envelopes before downstream automation sees them.
Honest scope
The admission decision is deterministic and works offline. The optional local
LLM CLI sees only verified metadata (source, event_id, event_type, and
content_type) and may suggest a queue. It never sees the webhook body and
cannot change an accept/reject verdict.
| Capability | Deterministic core | Optional LLM |
|---|---|---|
| HMAC verification | Yes | No |
| Timestamp window | Yes | No |
| Replay protection | Yes | No |
| Quarantine and audit report | Yes | No |
| Conservative queue selection | Yes | No |
| Metadata-only routing suggestion | Fallback | Yes |
| HTTP server or reverse proxy | No | No |
| Provider-specific signature formats | Customize | No |
This pack consumes local JSON envelope files. Your existing HTTP gateway must
write those files after preserving the exact request body. See CUSTOMIZE.md.
Who it is for
- LLM and automation operators accepting external event triggers.
- Small teams needing a fail-closed ingress gate without a hosted dependency.
- Integrators testing webhook authenticity and replay behavior offline.
- Security reviewers who need a compact, inspectable admission ledger.
Quick start
Install on Debian or Ubuntu:
sudo bash install.sh
Or run in place without root:
export WEBHOOK_GUARD_SECRET='replace-with-at-least-16-random-bytes'
python3 agent.py --help
python3 -m unittest -q test_agent
Create and sign a local example:
NOW="$(date +%s)"
export NOW
python3 -c 'import json,os; print(json.dumps({
"source":"billing",
"event_id":"evt-demo-001",
"event_type":"payment.succeeded",
"timestamp":int(os.environ["NOW"]),
"body":"{\"amount\":1200,\"currency\":\"USD\"}"
}))' > /tmp/webhook.json
SIG="$(python3 agent.py sign --input /tmp/webhook.json)"
export SIG
python3 -c 'import json,os; p="/tmp/webhook.json"; d=json.load(open(p)); d["signature"]=os.environ["SIG"]; open(p,"w").write(json.dumps(d))'
python3 agent.py verify --input /tmp/webhook.json --no-record
For a durable replay ledger, omit --no-record. A second verification of the
same (source, event_id) returns exit code 2 with verdict replay.
Event envelope
{
"source": "billing",
"event_id": "evt-123",
"event_type": "payment.succeeded",
"content_type": "application/json",
"timestamp": 1784912400,
"body": "{\"amount\":1200,\"currency\":\"USD\"}",
"signature": "sha256=0123456789abcdef..."
}
The signed bytes are:
ASCII(timestamp) + "." + exact UTF-8 body bytes
Use body_base64 instead of body for binary data. Exactly one body field is
required.
Architecture
provider webhook
|
v
existing HTTP gateway
|
| exact body + signature + timestamp + event ID
v
local inbox/*.json
|
v
+-----------------------+
| Webhook Guard |
| 1. bound input size |
| 2. verify HMAC |
| 3. check clock window |
| 4. reserve event ID |
+----------+------------+
|
+----+----+
| |
accepted/ quarantine/
| |
safe route audit only
suggestion
|
downstream agent
Commands
python3 agent.py status
python3 agent.py sign --input envelope.json
python3 agent.py verify --input envelope.json --db webhook_guard.sqlite3
python3 agent.py run --once --inbox inbox --accepted accepted --quarantine quarantine
python3 agent.py run --llm-routing
verify exits 0 only for an accepted event, 2 for a rejected event, and 1 for
local configuration or input errors.
Security boundaries
- Secrets are read from an environment variable or file, never from CLI flags.
- Comparisons use
hmac.compare_digest. - The replay check and insert occur under
BEGIN IMMEDIATE. - Invalid events never create replay-ledger entries.
- The body is never sent to an LLM.
- Nothing in this pack opens a network socket or executes webhook content.
- Quarantine is an audit destination, not a retry queue.
Start with CHECKLIST.md, then adapt the gateway and source mappings in
CUSTOMIZE.md.