How to expose OpenClaw's webhook endpoint to the public internet so GitHub (or any other service) can trigger your agent.
- OpenClaw installed and running as a LaunchAgent on macOS
- A domain registered with Cloudflare Registrar (DNS is already managed by Cloudflare — no extra nameserver step needed)
cloudflaredinstalled:brew install cloudflared
cloudflared tunnel loginThis opens a browser. Authorize your domain. A credentials file is written to ~/.cloudflared/.
cloudflared tunnel create openclawThis creates a tunnel and writes a credentials JSON to ~/.cloudflared/<UUID>.json. Note the tunnel ID in the output.
Create ~/.cloudflared/config.yml:
tunnel: <your-tunnel-id>
credentials-file: /Users/<your-username>/.cloudflared/<your-tunnel-id>.json
ingress:
- hostname: openclaw.yourdomain.com
service: http://localhost:18789
- service: http_status:404Replace <your-tunnel-id>, <your-username>, and yourdomain.com with your actual values.
cloudflared tunnel route dns openclaw openclaw.yourdomain.comThis creates a CNAME in Cloudflare DNS pointing openclaw.yourdomain.com at the tunnel.
Run the tunnel in the foreground to test:
cloudflared tunnel run openclawThen in another terminal:
curl -v https://openclaw.yourdomain.comYou should get a 200 with the OpenClaw Control UI HTML. If so, the tunnel is working.
sudo cloudflared service installThe tunnel now starts automatically on boot. You don't need to run step 5 in production.
openclaw config set hooks.enabled true --json
openclaw config set hooks.token "your-long-random-secret"
openclaw config set hooks.path "/hooks"OpenClaw needs a transform module to convert GitHub's webhook payload into a message for the agent.
mkdir -p ~/.openclaw/hooks/transformsCreate ~/.openclaw/hooks/transforms/github.mjs:
export default function transform({ payload, headers }) {
const event = headers['x-github-event'] || 'unknown';
const action = payload.action || '';
const repo = payload.repository?.full_name || 'unknown repo';
const title = payload.issue?.title || payload.pull_request?.title || '';
return {
message: `GitHub ${event}${action ? '.' + action : ''} on ${repo}${title ? ': ' + title : ''}`,
name: 'GitHub',
};
}openclaw config set hooks.mappings '[{
"match": { "path": "github" },
"action": "agent",
"agentId": "main",
"deliver": true,
"transform": { "module": "github.mjs" }
}]' --jsonopenclaw gateway restartcurl -s -o /dev/null -w "%{http_code}" -X POST https://openclaw.yourdomain.com/hooks/github \
-H "content-type: application/json" \
-H "x-openclaw-token: your-long-random-secret" \
-H "X-GitHub-Event: issues" \
-d '{"action":"opened","repository":{"full_name":"your-org/your-repo"},"issue":{"title":"test issue"}}'Expected: 200
Then check OpenClaw logs to confirm the agent was triggered:
openclaw logs --followIn each repo: Settings → Webhooks → Add webhook
- Payload URL:
https://openclaw.yourdomain.com/hooks/github - Content type:
application/json - Secret: leave blank (OpenClaw uses its own token, not GitHub's HMAC — see note below)
- Events: select Issues and Pull requests (or whichever events you want)
Note on auth: GitHub webhooks can't add an
Authorization: Bearerheader — they useX-Hub-Signature-256HMAC instead. OpenClaw doesn't verify GitHub's HMAC; it uses its own token viax-openclaw-token. Since Cloudflare Tunnel requires HTTPS and the token is in a header (not a query param), this is reasonably secure. For stricter verification, put a relay (like Rehook) between GitHub and OpenClaw to verify the HMAC before forwarding.
Cloudflare strips Authorization headers on tunnel traffic. Always use x-openclaw-token instead of Authorization: Bearer when sending to the tunnel URL. Locally (bypassing the tunnel) both headers work.
The transform function signature receives { payload, headers, url, path }. The headers object uses lowercase keys (e.g. x-github-event, not X-GitHub-Event).
match.path in a mapping matches the URL sub-path after /hooks/ — so match: { path: "github" } matches POST /hooks/github. It does not match on payload content.
Token SecretRef: If your token shows as __OPENCLAW_REDACTED__ in openclaw config get, it's stored as a SecretRef. The actual value is what you set when you ran openclaw config set hooks.token. Use that same value in your curl/webhook headers.
Re-authenticating Claude CLI: If the agent runs fail with a billing/auth error after a gateway restart, re-run:
claude auth login
openclaw models auth login --provider anthropic --method cli --set-default
openclaw gateway restart