GitHub -> Discord webhook bridge. One setup, every repo, forever.
A Node.js app that delivers rich, color-coded Discord embeds for every GitHub event. Install once via a GitHub App and it covers all your repos automatically — including future ones. Also polls public repos, orgs, and users you don't own.
- Global repo coverage - one GitHub App install covers all current and future repos
- Public repo polling - watch any public repo, org, or user without them installing anything
- Rich Discord embeds - color-coded notifications with avatars, links, and context
- HMAC-SHA256 verification - every webhook payload verified before processing
- Per-target event filters - route different events to different Discord channels
- Minimal dependencies - Express.js and Node.js stdlib only
- GitHub Event - an event occurs (push, issue, star, etc.) in any monitored repo
- Webhook Delivery - GitHub POSTs the payload to your configured webhook URL
- Signature Verification - server checks the HMAC-SHA256 signature against your secret
- Event Routing - payload is dispatched to the correct handler via
X-GitHub-Event - Discord Notification - handler formats the event into an embed and posts to Discord
For public repos you don't control, ghook polls GitHub's Events API on a configurable interval instead.
GitHub Apps give you account-level installation - unlike per-repo webhooks, a single install covers all repos, includes new ones automatically, and uses webhook secrets for auth (no private key needed).
| Event | Description | Color |
|---|---|---|
push |
Commits pushed to a branch | Green #238636 |
create |
Branch or tag created | Blue #1f6feb |
delete |
Branch or tag deleted | Red #f85149 |
watch |
Repository starred | Yellow #e3b341 |
fork |
Repository forked | Purple #a371f7 |
pull_request |
PR opened, closed, merged | Blue #58a6ff |
issues |
Issue opened, closed, reopened | Green/Red/Orange |
issue_comment |
Comment on an issue | Blue #58a6ff |
release |
Release published or prereleased | Green #238636 |
workflow_run |
GitHub Actions run completed | Green/Red/Grey/Orange |
Push event
[User Avatar] User Name
Pushed 3 commits to `main`
[`a1b2c3d`] Initial commit message
[`e4f5g6h`] Fix bug in authentication
[`i7j8k9l`] Update documentation
Repository: owner/repo
Branch: `main`
ghook • [timestamp]
Release event
[User Avatar] User Name
🚀 v1.0.0
Release notes or description here...
Repository: owner/repo
Tag: `v1.0.0`
Type: Release
ghook • [timestamp]
Star event
[User Avatar] User Name
⭐ owner/repo
Repository description here...
Stars: ⭐ 1,234
Forks: 🍴 567
Language: JavaScript
ghook • [timestamp]
- Go to GitHub Settings -> Developer settings -> GitHub Apps and click New GitHub App
- Set a name, homepage URL, and configure the webhook URL (
https://yourdomain.com/webhook) and secret - Set repository permissions:
ActionsRead,ContentsRead,IssuesRead,MetadataRead,Pull requestsRead - Subscribe to events: Push, Create, Delete, Fork, Issues, Issue comment, Pull request, Release, Watch, Workflow run
- After creating: Install App -> install on your account -> All repositories
| Permission | Required for |
|---|---|
Metadata |
Always required |
Contents |
Release events, repo metadata |
Issues |
Issue and comment events |
Pull requests |
Pull request events |
Actions |
Workflow run events |
git clone https://github.com/jedbillyb/ghook.git
cd ghook
npm install
cp .env.example .env
# Edit .env with your values
npm run dev # auto-restarts on file changesVerify it's running:
curl http://localhost:3000
# -> "GitHub -> Discord bot is running."Note: keep your .env out of version control and restrict permissions with chmod 600 .env.
# Required
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN
GITHUB_WEBHOOK_SECRET=your_strong_random_secret_here
# Optional
PORT=3000
NOTIFY_PRIVATE_REPOS=false
DISCORD_LEGACY_EMBEDS=false
WEBHOOK_FOOTER=github.com/jedbillyb/ghook
WEBHOOK_FOOTER_URL=https://github.com/jedbillyb/ghook
IGNORED_EVENTS=watch,fork
BRANCH_FILTER=main,develop,release/*
LOCALE=en
# Multi-webhook routing
DISCORD_WEBHOOK_RELEASES=https://discord.com/api/webhooks/.../...
DISCORD_WEBHOOK_CI=https://discord.com/api/webhooks/.../...
ROUTES=release:RELEASES,workflow_run:CI
# Polling - watch public repos/orgs/users (no install required on their side)
WATCH_REPOS=torvalds/linux:release,push;vercel/next.js
WATCH_ORGS=vercel:release
WATCH_USERS=gaearon
POLL_INTERVAL=60000
GITHUB_TOKEN=ghp_xxxx| Variable | Default | Description |
|---|---|---|
DISCORD_WEBHOOK_URL |
- | Primary Discord webhook URL |
GITHUB_WEBHOOK_SECRET |
- | HMAC secret set in your GitHub App |
PORT |
3000 |
Port the server listens on |
NOTIFY_PRIVATE_REPOS |
false |
Forward events from private repos |
DISCORD_LEGACY_EMBEDS |
false |
Fall back to classic embeds instead of Components V2 |
WEBHOOK_FOOTER |
github.com/jedbillyb/ghook |
Footer text on each message |
WEBHOOK_FOOTER_URL |
ghook repo URL | Footer link target (Components V2 only) |
IGNORED_EVENTS |
- | Comma-separated event names to drop (e.g. watch,fork) |
BRANCH_FILTER |
- | Comma-separated branch patterns for push/create/delete. * matches one segment |
ROUTES |
- | Map events to named webhooks: release:RELEASES,workflow_run:CI |
LOCALE |
en |
Message language. Supported: en, fr |
WATCH_REPOS |
- | Public repos to poll. ; separates targets, :events filters by event type |
WATCH_ORGS |
- | Public orgs to poll. Same syntax as WATCH_REPOS |
WATCH_USERS |
- | Public users to poll. Same syntax as WATCH_REPOS |
POLL_INTERVAL |
60000 |
Polling interval in ms |
GITHUB_TOKEN |
- | PAT for GitHub API auth - raises rate limit from 60 to 5000 req/hr |
sudo cp /opt/ghook/github-discord-bot.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now github-discord-bot
sudo journalctl -u github-discord-bot -fFROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]docker build -t ghook .
docker run -p 3000:3000 --env-file .env ghookCaddy (recommended - auto HTTPS)
yourdomain.com {
reverse_proxy localhost:3000
}
Nginx
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}npm test93 unit tests covering signature verification, Components V2 and legacy embed builders, Discord routing, event filtering, push batching, and the polling pipeline. CI runs on Node 20 and 22 against every PR and push to main.
# Health check
curl http://localhost:3000
# Simulate a webhook (signature will fail - expected)
curl -X POST http://localhost:3000/webhook \
-H "Content-Type: application/json" \
-H "X-GitHub-Event: push" \
-H "X-Hub-Signature-256: sha256=fake" \
-d '{"repository":{"full_name":"test/repo"},"commits":[]}'Invalid signature - rejected
GITHUB_WEBHOOK_SECRET doesn't match what's set in your GitHub App. Check for leading/trailing spaces.
Discord API error: 401
Discord webhook URL is invalid or deleted. Regenerate it in Discord -> Server Settings -> Integrations -> Webhooks.
Address already in use
Port 3000 is taken. Change PORT in .env or find the conflict: sudo lsof -i :3000.
No Discord notifications
Check the service is running (sudo systemctl status github-discord-bot), tail logs (sudo journalctl -u github-discord-bot -f), and check GitHub App -> webhook delivery history for errors.
Components V2 messages failing silently
Discord's Components V2 is still server-gated. Set DISCORD_LEGACY_EMBEDS=true until your server has access.
Pull requests are welcome. Keep changes focused - one fix or feature per PR. If you're adding a new event handler, follow the pattern in src/handlers/ and register it in router.js.