Skip to main content

Webhook triggers

A trigger is a secret URL on this machine. Each event that an outside service sends to it either sends a message to one existing conversation or starts a new conversation in a project. Events reach the machine directly through Tailscale Funnel, so overdeck.ai never sees them and you need no overdeck.ai account.
Available from the next release. Manage triggers with pan triggers or Settings → Anywhere → Triggers.

What a trigger does

Every trigger has one target:
  • Conversation target. Each event is sent as one message to an existing conversation, the same way the composer sends one. The conversation must still be open; an ended or archived conversation answers 410.
  • Project target. Each event starts one new conversation in a registered project. The conversation’s first message is your prompt template followed by the event. A project target needs a prompt template. You can also set the model, harness and effort for the conversations it starts; unset values follow the normal defaults.
A trigger delivers every event type it receives, including GitHub’s ping event. It has no repository or event-type filter, because the URL is the authorization and GitHub and Stripe already let you choose which events to send. A trigger can only send a message or start a conversation. It never answers a question, picks a menu option or approves a permission (see Limits and refusals).

Create a trigger

From the terminal:
--conversation takes a conversation name or its numeric id; the trigger stores the name. See pan triggers for every flag. From the dashboard: open Settings → Anywhere, then Add trigger in the Triggers panel. Either way you get the trigger’s URL once:
The last path segment is the URL secret. Overdeck keeps only its SHA-256 hash, so it cannot show the URL again. If you lose it, remove the trigger and add a new one. When a trusted address on a .ts.net host is saved (Settings → Anywhere), Overdeck fills in the real host; otherwise it prints the <machine>.<tailnet> placeholder for you to replace. pan triggers add also prints a local test URL, http://127.0.0.1:<dashboard port>/hooks/…, which works on this machine without Funnel:

Signing secrets

A trigger’s verification kind is none, github or stripe. With github or stripe, the dashboard checks the event’s signature over the exact request bytes before it does anything else with the body. A trigger with github or stripe and no signing secret refuses every event with 503. GitHub. When you create a github trigger without --signing-secret, Overdeck generates a secret and shows it once with the URL. In the repository’s Settings → Webhooks → Add webhook form, paste the URL into Payload URL and the secret into Secret. Either content type works. GitHub signs each event with X-Hub-Signature-256. Stripe. Stripe makes the secret, so create the trigger first, create the endpoint in Stripe with the trigger’s URL, then copy the endpoint’s whsec_… secret into the trigger:
set-secret reads the secret from one line of standard input (or pass --secret). Stripe signs each event with Stripe-Signature. Overdeck accepts any matching v1 signature, so a rolled secret keeps working during Stripe’s overlap window, and it refuses an event whose timestamp is more than 300 seconds before or after this machine’s clock. No route or command shows a signing secret after you set it. To change one, set it again.

Publish with Tailscale Funnel

Tailscale Funnel publishes one local address on the public internet through Tailscale’s relays. Overdeck publishes only the /hooks path, on port 8443. Prerequisites:
  1. Tailscale is installed on this machine and signed in to your tailnet.
  2. HTTPS certificates are on for the tailnet (admin console → DNS → HTTPS Certificates).
  3. The tailnet policy gives this machine the funnel node attribute.
  4. Your user may run Tailscale serve commands. Run this once:
Turn Funnel on for the trigger route (replace 3011 if your dashboard API port is different; pan triggers add and the Settings panel print the command with your port):
Turn it off:
The target URL repeats /hooks because Funnel strips the mount path before it proxies the request. Port 8443 keeps Funnel off port 443, where Tailscale Serve may publish your dashboard to the tailnet; Funnel is allowed per host and port, so funneling 443 would also publish that dashboard. The public URL form is:
Overdeck does not run the tailscale command itself.

What the agent sees

The event never reaches the agent as raw text. Overdeck wraps it in a fixed envelope:
  • The message always starts with fixed text, so a body that starts with /handoff or any other command never runs as one.
  • The nonce is random for each event, so the body cannot fake the end marker.
  • source is github or stripe only when that signature was verified, else unsigned.
  • Control characters in the body (carriage returns, Ctrl-C, escape sequences) become �.
  • At most 44 KiB of the body is included. A longer body is cut, marked truncated=yes, and ends with [truncated: <n> more bytes not shown]. The cap keeps the whole message under the 50,000-character limit that every conversation message has. The signature is still checked over the full body, up to 1 MiB.

Limits and refusals

  • Rate limits. A project trigger starts at most 6 conversations per rolling hour; a conversation trigger sends at most 60 messages per rolling hour. The count lives in the dashboard’s memory and resets when the dashboard restarts.
  • Pending input. While the target conversation has an open question, plan approval, permission prompt or menu, the trigger answers 503 instead of sending, so an event can never answer it.
  • CPU pressure. While the host’s CPU is saturated, every trigger answers 503.
  • Nothing is queued. A refused event is not stored. GitHub does not retry failed deliveries (use Redeliver in the webhook’s Recent Deliveries); Stripe retries non-2xx deliveries for up to three days.
  • “Started” means the conversation was created and its launch requested; the agent becoming ready is not part of the HTTP answer.

Security

  • The URL is a bearer secret. Anyone who has it can send events. Treat it like a password, and remove the trigger if it leaks.
  • Funnel requests reach only POST /hooks/*. Tailscale marks every funneled request with the Tailscale-Funnel-Request header and removes any copy a client sends. The dashboard answers such a request with 404 on every other method and path (even with a credential), never treats it as a local caller, refuses it a dashboard session, and refuses its WebSocket upgrades.
  • overdeck.ai never sees Funnel events. They travel from the sender through Tailscale’s relays to this machine.
  • Unknown URLs cost nothing. The dashboard checks the trigger id and URL secret before it reads any of the body, and it stops reading a body as soon as it passes 1 MiB, even when the sender gives no Content-Length.
  • Logs name the trigger id and the outcome only, never the body or a secret.
  • The trigger store is ~/.overdeck/triggers.json, mode 0600. It holds the URL secret’s hash and the signing secrets in plain text, because the machine must compute the HMAC.
  • A directory inbox that holds events while this machine is offline is planned separately (PAN-2356).