Every SaaS with a “POST when X happens” knob can hand
events to an agent. Mint a hook token, paste the URL into the
source, the agent sees inbound messages at a hook:
JID. The primitive is route
tokens; this page covers the operational details for the
three sources you are most likely to wire up.
/hook/<token>. It accepts
only hook: tokens; presenting a web:
chat token there returns 404. issue_webhook always
returns a /hook/<token> URL.
GitHub push → agent. A repo's webhook
settings POST a push payload to /hook/<token>.
The agent sees one inbound per push at hook:<folder>/github
with the body verbatim and the X-GitHub-Event header
intact. A skill on the agent side parses the payload, decides
whether to reply on Slack, file a sub-task, or stay silent.
Linear comment → agent. Linear posts new
comments to a webhook URL. With a jid_suffix the
agent can split issues and comments into two streams —
hook:<folder>/linear/comments vs
hook:<folder>/linear/issues — and route
each independently.
Custom alert → agent. Grafana, Sentry, Healthchecks.io, or a cron job that POSTs JSON: anything that speaks HTTP. The body is delivered to the agent verbatim and the headers come with it.
Three ways, one writer.
The agent asks for it (MCP). A turn in folder acme/eng calls:
issue_webhook(source_label="github")
The MCP response carries the URL once:
{
"token": "Yp3v...Q2",
"url": "https://<host>/hook/Yp3v...Q2",
"jid": "hook:acme/eng/github"
}
The agent typically prints the URL back into chat so a human can paste it into GitHub. The raw token is not stored anywhere recoverable — if it scrolls past, reissue.
An operator from the CLI.
arizuko token issue acme/eng hook github
# prints the URL once
The CLI hits POST /v1/route_tokens/hook
OAuth-gated through proxyd. Same row, same shape as the
MCP path.
A button in dashd. The folder page in
/dash/ lists existing tokens and offers an
“Issue webhook” button per folder.
acme/eng call
issue_webhook("github"). Copy the URL.https://<host>/hook/<token>application/json.On the next push, webd hashes the token, looks up the row, and writes one inbound:
{
"jid": "hook:acme/eng/github",
"sender": "github",
"body": "{\"ref\":\"refs/heads/main\",\"commits\":[...]}",
"headers": {
"x-github-event": "push",
"x-github-delivery": "...",
"x-hub-signature-256": "sha256=..."
}
}
The body is verbatim — whatever GitHub sent — up
to the 1 MiB cap. Headers come through lowercase-keyed. The
agent reads them via the inbound message shape; signature
validation (X-Hub-Signature-256) belongs to a
skill, not the platform.
webd returns a plain response to GitHub. The agent acknowledges (if at all) on whichever channel makes sense — webhooks rarely care about an in-band reply.
Pass a jid_suffix to partition. One folder can
receive several webhook streams at different JIDs without
collision:
issue_webhook("linear", jid_suffix="comments")
→ hook:acme/eng/linear/comments
issue_webhook("linear", jid_suffix="issues")
→ hook:acme/eng/linear/issues
Each suffix gets its own URL, its own row, and its own JID.
Route rules can fire only on one (jid=hook:acme/eng/linear/comments)
and ignore the other, or observe everything from
hook:acme/eng/linear/* without firing a turn
(see #observe).
One call from MCP or REST:
revoke_route_token(jid="hook:acme/eng/github")
# or
arizuko token revoke hook:acme/eng/github
The next request to that URL returns 401. No grace period.
An agent in a different folder cannot revoke a token whose
owner_folder is not its own — the ACL
check at revoke_route_token requires admin on
the issuing folder.
The bearer model is intentional and has three corollaries.
hook:acme/eng/github; it
cannot POST into hook:acme/eng/linear/* or
any other JID. Keep sources separate.
X-Hub-Signature-256,
Stripe Stripe-Signature), validate it in a
skill on the agent side using the headers
map on the inbound. The platform never touches it.
Per-token rate limits live in webd: a higher bucket for
hook: than for web:, sized for
machine bursts. Excess returns 429. Body cap is 1 MiB.
sudo journalctl -u arizuko_<instance> --since "30s ago" -f \
| grep -iE 'hook:|/hook/'
A 401 means the token is unknown or revoked. A 429 is the rate-limit bucket. A 413 is body over 1 MiB.
web:-prefix sibling, for browser chat URLs.hook: JID reaches an agent and how #observe works.