Webhooks - wake your agents
PugBase POSTs a signed event when the plan moves. A background or cloud agent is woken by it instead of asking next_action every minute.
Why
A long-running agent that waits for another feature to land has two choices today: poll next_action, or stop and be restarted by a person. Webhooks give it a third: an HTTPS endpoint of yours receives feature.ready (or task.reopened, review.requested...) and starts the agent with that context.
Events
feature.readyevery feature this one depended on is complete - an agent can start it nowfeature.completeevery task note in a feature is donetask.donea task note reached 100% tickedtask.reopenedQA or a reviewer reopened a task, with the reasonhandoff.pushedan agent pushed a result / handoff into a task notereview.requesteda pull request was opened for reviewpingtest delivery from the settings pageSet up (owner)
- Workspace Settings > Webhooks > paste the URL of your receiver, pick the events, Add. Copy the signing secret - it is shown once.
- Press Test: PugBase sends a ping event; the row shows the HTTP status your endpoint answered with.
- Each delivery is retried twice on a network error or 5xx (not on 4xx). Deliveries are listed in the Audit log.
Verify the signature
Every request carries X-PugBase-Timestamp (ms) and X-PugBase-Signature = sha256=<hex HMAC-SHA256 of "<timestamp>.<raw body>" with your secret>. Reject a bad signature or a timestamp older than 5 minutes.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyPugbase(req, rawBody, secret) {
const ts = req.headers["x-pugbase-timestamp"];
const sig = req.headers["x-pugbase-signature"];
if (!ts || !sig || Math.abs(Date.now() - Number(ts)) > 5 * 60 * 1000) return false;
const expected = "sha256=" + createHmac("sha256", secret).update(ts + "." + rawBody).digest("hex");
return expected.length === sig.length && timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}Payload
JSON with id, event, workspaceId, at, data and a hint that says what an agent should do next - the same words next_action uses. data carries noteId / noteTitle for note events and feature for feature events.
{
"id": "6f0c...",
"event": "feature.ready",
"workspaceId": "5srXzYvCzloSZzcD7Fae",
"at": "2026-10-31T09:12:44.120Z",
"data": { "feature": "Checkout", "unblockedBy": "Catalog", "open": 6 },
"hint": "Feature \"Checkout\" is unblocked. Call next_action (or check_ready with this feature) to claim its first task."
}Starting an agent from a webhook
The receiver is yours: a small server, a serverless function, or a CI job trigger. The usual pattern is to spawn Claude Code in headless mode with the hint as the prompt - it then calls next_action itself and claims the work.
// Express receiver that wakes a headless Claude Code run
app.post("/pugbase", express.raw({ type: "*/*" }), (req, res) => {
const body = req.body.toString("utf8");
if (!verifyPugbase(req, body, process.env.PUGBASE_WEBHOOK_SECRET)) return res.status(401).end();
res.status(202).end(); // answer first - PugBase retries slow receivers
const { event, hint } = JSON.parse(body);
if (event === "ping") return;
spawn("claude", ["-p", hint + " Read the PugBase server instructions first.", "--max-turns", "60"], {
cwd: "/srv/project", env: process.env, stdio: "inherit",
});
});Limits
- https only (http is accepted for localhost while developing); private addresses are refused.
- At most 10 webhooks per workspace; a receiver that keeps failing stays registered - disable it with the switch or fix the endpoint.
- A webhook is a nudge, not the source of truth: an agent that starts from one still goes through next_action / check_ready, which can say wait.