OpenClaw ships with a tiny HTTP server that can accept any JSON a platform throws at it. GitHub throws a lot. This article walks through wiring GitHub webhooks straight into an OpenClaw agent, parsing the payload, and triggering useful automations—no polling, no bash glue, no third-party SaaS in the middle.
Why GitHub → OpenClaw beats cron polling
Polling a repository every few minutes adds latency, costs API quota, and sooner or later trips the X-RateLimit wire. Webhooks, by contrast, push the event the moment it happens. OpenClaw already keeps a Node.js process alive (the daemon), so adding one more route costs basically nothing. You get real-time responses: welcome a newcomer the second their PR lands, add labels before a maintainer even reads the issue, publish release notes while the tag is still warm.
Prerequisites and quick install
Versions that work
- Node.js 22 or later (OpenClaw refuses to boot on 21.x and below.)
- OpenClaw ≥ 0.34.0 (the release that introduced
agent.webhook.on()) - A repository where you can create webhooks (owner or maintainer rights)
Local vs ClawCloud
- Local — perfect for hacking:
npm create openclaw, then expose to the internet withngrok http 3000orcloudflared tunnel. - ClawCloud — skips the tunneling. Create an agent, grab the permanent HTTPS endpoint from the "Webhook" tab. Done.
Fresh install in 90 seconds
# Node 22 via volta or asdf — your pick
volta install node@22
# Create an OpenClaw workspace
npm create openclaw@latest my-bot
cd my-bot && npm install
# Start the daemon (runs gateway + webhook server)
npm run dev
The dev script launches the gateway on http://localhost:3000, mounts the webhook surface at POST /api/webhook, and logs every incoming payload to stdout.
Expose the OpenClaw webhook endpoint
Option 1: ngrok (free but rotates URL)
ngrok http 3000 # outputs https://random.ngrok-free.app
Copy the HTTPS forwarding URL. Append /api/webhook; that full path is what GitHub will call.
Option 2: tunnel via Cloudflare (stable subdomain)
cloudflared tunnel create openclaw
cloudflared tunnel route dns openclaw bot.example.com
cloudflared tunnel run openclaw --url http://localhost:3000
The broker gives you https://bot.example.com/api/webhook. No monthly fees, stays up until you kill the tunnel.
Option 3: ClawCloud (already public)
In the ClawCloud dashboard: Agents → your-agent → Webhook. You get something like https://g1.claw.run/agent/abcd1234/webhook. There is nothing to expose; skip to the next section.
Register the GitHub webhook
Create the hook
- Open the repository → Settings → Webhooks → Add webhook.
- Payload URL: paste the endpoint you produced above.
- Content type:
application/json - Secret: generate a strong token, e.g.
pwgen 32 1. We’ll store it in OpenClaw env vars. - Select Let me select individual events and tick:
- Push
- Pull request
- Issues
- Release
- Save.
GitHub fires a ping event instantly. If OpenClaw logs it, the pipe is open.
Verify signature (recommended)
GitHub includes an X-Hub-Signature-256 header. The value is sha256=…. Add the secret to .env:
GITHUB_WEBHOOK_SECRET='u07Nd3lt7e9gA4tG4nZkP1Fp7vt3TlmG'
Inside your bot code:
import crypto from 'node:crypto';
agent.webhook.on('raw', async (req, body) => {
const sig = req.headers['x-hub-signature-256'];
if (!sig) throw new Error('Missing signature');
const hmac = crypto.createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET)
.update(body)
.digest('hex');
if (sig !== `sha256=${hmac}`) throw new Error('Invalid signature');
});
Fail fast; don’t process forged payloads.
Parse the payload inside OpenClaw
OpenClaw exposes a thin event emitter:
agent.webhook.on(eventType, handler)
The eventType is the raw GitHub header X-GitHub-Event. You can use * to catch them all, but explicit is cleaner. Example:
agent.webhook.on('issues', async ({ action, issue, repository }) => {
if (action !== 'opened') return;
await agent.memory.put(`issue-${issue.number}-opened-at`, new Date().toISOString());
// trigger tools, send Slack alert, etc.
});
You get the deserialized JSON plus the request headers. No need to JSON.parse().
Three real automations you can copy-paste
1. Auto-label new issues by keyword
Users rarely read your CONTRIBUTING guide. Let OpenClaw tidy the board:
import { Octokit } from 'octokit';
const gh = new Octokit({ auth: process.env.GITHUB_PAT });
const LABEL_MAP = {
bug: [/error|stack trace|crash/i],
docs: [/typo|documentation|readme/i],
build: [/webpack|vite|rollup|eslint/i]
};
agent.webhook.on('issues', async ({ action, issue, repository }) => {
if (action !== 'opened') return;
const labels = Object.entries(LABEL_MAP)
.filter(([, regexArr]) => regexArr.some(r => r.test(issue.body)))
.map(([label]) => label);
if (labels.length === 0) return;
await gh.rest.issues.addLabels({
owner: repository.owner.login,
repo: repository.name,
issue_number: issue.number,
labels
});
agent.log.info(`Labeled #${issue.number} with ${labels.join(', ')}`);
});
Tip from the community: cache the regex compilation if you run thousands of repos on one agent.
2. Welcome first-time contributors on PR open
The GitHub UI marks first-time contributor, but a friendly comment beats a gray badge.
agent.webhook.on('pull_request', async ({ action, pull_request: pr, repository }) => {
if (action !== 'opened') return;
if (!pr.author_association || pr.author_association !== 'FIRST_TIME_CONTRIBUTOR') return;
const msg =
'👋 Thanks for your first PR! The CI will run in ~10 min. '\
+ 'A maintainer will review once green. Meanwhile, please ensure the PR '\
+ 'description answers the template questions. Happy hacking!';
await gh.rest.issues.createComment({
owner: repository.owner.login,
repo: repository.name,
issue_number: pr.number,
body: msg
});
});
You can replace the comment with a Slack or Discord ping by swapping gh.rest.issues.createComment for the Composio Slack tool.
3. Generate release notes when a new tag is pushed
Drafting release notes is busywork. Use OpenClaw’s LLM tooling to summarise merged PRs.
import { Configuration, OpenAI } from 'openai';
const openai = new OpenAI(new Configuration({ apiKey: process.env.OPENAI_KEY }));
agent.webhook.on('release', async ({ action, release, repository }) => {
if (action !== 'published') return; // we only care once the tag is live
// Collect merged PRs since the previous release.
const since = agent.memory.get('last-release-date') || '1970-01-01T00:00:00Z';
const { data: prs } = await gh.rest.pulls.list({
owner: repository.owner.login,
repo: repository.name,
state: 'closed',
sort: 'updated',
direction: 'desc',
per_page: 100
});
const merged = prs.filter(pr => pr.merged_at && pr.merged_at > since);
const bullets = merged.map(pr => `* ${pr.title} (#${pr.number})`).join('\n');
const prompt = `Summarise the following changes in 8-12 bullet points:\n${bullets}`;
const ai = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: prompt }]
});
const notes = ai.choices[0].message.content;
await gh.rest.repos.updateRelease({
owner: repository.owner.login,
repo: repository.name,
release_id: release.id,
body: notes
});
agent.memory.put('last-release-date', release.published_at);
agent.log.info(`Updated release notes for ${release.tag_name}`);
});
This snippet uses agent.memory for state. On ClawCloud that memory is persisted in Postgres; locally it’s a JSON file under .claw/data.
Debugging and next steps
Watch the request log
TAIL_LOG=true npm run dev
The TAIL_LOG flag adds colored dumps of every incoming header and the first 2 kB of body. Helpful when a payload silently 500s.
Replay events
GitHub stores delivery history. In the Webhooks UI click a request, then Redeliver. No need to push dummy commits.
Common pitfalls
- Endpoint returns 200 but nothing happens → forgot to
awaitinside the handler, the function exited. - Multiple webhooks configured and getting duplicates → GitHub sends one per secret; prune old hooks.
- Ngrok URL changed and GitHub keeps failing → update the Payload URL or pay for a fixed ngrok subdomain.
- Octokit 3.x vs 4.x: methods moved under
gh.rest.*. Make sure your code matches the version.
Where to go from here
- Chain events into multi-step workflows with OpenClaw’s
agent.sequence(). - Persist context: store issue history in
agent.memoryand let the LLM answer duplicate questions. - Secure endpoints: behind Cloudflare Auth or use HMAC signature check shown earlier.
- Share your recipes: github.com/openclaw/recipes is the community dump of battle-tested handlers.
Webhooks turn GitHub into an event stream; OpenClaw turns events into actions. You now have everything you need to teach your agent some manners, save yourself review time, and keep your repo tidy without lifting a finger.