docs
Being reached

Webhooks

Port 8787 is the one thing published, and it takes signed requests only. A webhook wakes an agent; it never tells it what to do.

Declared, not discovered

A hook is a block in the config file

deploy/config.yaml
hooks:
  - id: deploys
    agentId: scout
    # The name of an environment variable, not the secret itself.
    secretEnv: DEPLOY_HOOK_SECRET
    # The default is "public". A hook may never be "operator".
    trust: participant
    replyUrl: https://acme.example.com/agent-replies

That makes POST /hooks/deploys exist and belong to scout. The secret is named rather than written, so the file describing what can wake your agents stays committable and diffable. replyUrl is where the agent's answer is posted back, for a system that wants one.

The installer writes a working hook and generates its secret into .env, so there is one to try before you have written any of this.

Sending one

The signature covers the timestamp and the body

sh
BODY='{"text":"the nightly build failed"}'
TS=$(date +%s)
SIG="sha256=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$DEPLOY_HOOK_SECRET" -r | cut -d' ' -f1)"

curl -X POST http://localhost:8787/hooks/deploys \
  -H "x-squad-timestamp: $TS" \
  -H "x-squad-signature: $SIG" \
  -d "$BODY"

It is compared in constant time within a freshness window. An unknown hook id answers exactly like a bad signature, and only after the body has been read, so the endpoint does not enumerate — someone holding no secret cannot learn from it which hooks exist.

What arrives

Authentic, and still written by anyone

A GitHub webhook is signed by GitHub and relays an issue body typed by a stranger. So a webhook may not carry operator trust, however well signed: the secret proves which system sent the request, never that a human meant what is inside it. The body arrives fenced, introduced as data, with a random nonce chosen after the content is written so that nothing inside can close the fence.

Events queue per agent and are folded into a single turn, so an agent woken twenty times while busy takes one turn about twenty things. A turn that fails leaves its events queued rather than acknowledging them, so a bad API key costs a retry instead of the message.

The reply is routed by the channel of the event that caused it, so an agent answering a GitHub hook cannot be steered into replying on Telegram by anything in the payload.

The other two ways in

Neither of them needs a port

Telegram and email reach out rather than being reached, so neither costs a domain, a certificate or anything published. They are also the two that can be paired to a person, which is what lets them instruct. If nothing needs to wake your agents from outside, port 8787 is a thing you never open.