How to Set Up and Authenticate GitHub Webhooks Using HMAC-SHA256 in Claude-Code-Telegram
Configure the GITHUB_WEBHOOK_SECRET environment variable and verify the X-Hub-Signature-256 header in your FastAPI server to securely authenticate incoming GitHub webhooks.
The Claude-Code-Telegram repository provides a FastAPI-based webhook server that ingests GitHub events securely using HMAC-SHA256 signature verification. This implementation ensures that only payloads signed with your shared secret are processed, preventing unauthorized requests from triggering your automation pipeline.
Configure the GitHub Webhook Secret
The server reads the shared secret from the GITHUB_WEBHOOK_SECRET environment variable, which is exposed through the Pydantic Settings model in src/config/settings.py. This secret must match the value configured in your GitHub repository settings.
Add the secret to your environment file:
# .env
GITHUB_WEBHOOK_SECRET=your_generated_secret_here
The application accesses this value via Settings.github_webhook_secret at runtime. Keep this value confidential and never commit it to version control.
Register the Webhook on GitHub
Navigate to your repository’s Settings → Webhooks and click Add webhook:
- Payload URL:
https://<your-host>/webhooks/github - Content type: Select application/json
- Secret: Paste the identical value stored in
GITHUB_WEBHOOK_SECRET - Events: Choose Just the push event (or select specific events based on your automation needs)
- Click Add webhook
GitHub will now sign every payload using HMAC-SHA256 with your secret before transmitting it to your server.
Verify Signatures Using HMAC-SHA256
The verification flow occurs in the FastAPI endpoint defined in src/api/server.py.
The Endpoint Handler
When GitHub delivers a webhook, it sends a POST request to /webhooks/{provider}. The handler extracts the X-Hub-Signature-256 header and the raw request body, then delegates verification to the authentication module.
Signature Validation Logic
In src/api/auth.py, the verify_github_signature() function reproduces the HMAC digest and performs a constant-time comparison:
# src/api/auth.py (lines 34-41)
expected_signature = (
"sha256="
+ hmac.new(secret.encode("utf-8"), payload_body, hashlib.sha256).hexdigest()
)
return hmac.compare_digest(expected_signature, signature_header)
The function returns True only if the computed signature matches the header value exactly. The use of hmac.compare_digest() prevents timing attacks during the comparison.
If verification fails, the server returns 401 Unauthorized and logs a warning message. If successful, the payload proceeds to processing.
Process Verified Events
After successful authentication, the server:
- Parses the JSON payload (with fallback to raw body on parsing errors)
- Checks the SQLite
webhook_eventstable insrc/storage/database.pyto detect and skip duplicate deliveries - Publishes a
WebhookEventto the internalEventBusdefined insrc/events/bus.pyfor downstream handling by Claude agents
The deduplication mechanism ensures that retried webhook deliveries do not trigger duplicate automation runs.
Test the Webhook Integration
Start the API server using run_api_server() from src/api/server.py, which launches Uvicorn on the port specified by settings.api_server_port.
Test your configuration locally using curl to generate a valid signature:
payload='{"test":"data"}'
secret=$GITHUB_WEBHOOK_SECRET
sig=$(echo -n "$payload" | openssl dgst -sha256 -hmac "$secret" -binary | xxd -p)
curl -X POST "http://localhost:8080/webhooks/github" \
-H "Content-Type: application/json" \
-H "X-Hub-Signature-256: sha256=$sig" \
-d "$payload"
A correctly signed request returns {"status":"accepted"} with HTTP 200. Requests with missing or invalid signatures return 401 Unauthorized.
Summary
- Store your GitHub webhook secret in the
GITHUB_WEBHOOK_SECRETenvironment variable to configureSettings.github_webhook_secretinsrc/config/settings.py. - Register the webhook URL
/webhooks/githubin your GitHub repository settings with content type application/json. - The FastAPI endpoint in
src/api/server.pyvalidates theX-Hub-Signature-256header usingverify_github_signature()fromsrc/api/auth.py. - Invalid signatures trigger an immediate 401 Unauthorized response without further processing.
- Verified events are deduplicated via SQLite and published to the
EventBusfor asynchronous handling by your automation agents.
Frequently Asked Questions
Where is the GitHub webhook secret configured in the codebase?
The secret is defined in the environment variable GITHUB_WEBHOOK_SECRET and loaded through the Settings class in src/config/settings.py as github_webhook_secret. The application reads this value at startup to use during signature verification.
Which HTTP header contains the HMAC-SHA256 signature from GitHub?
GitHub sends the signature in the X-Hub-Signature-256 header with the format sha256=<hex_digest>. The FastAPI endpoint extracts this value as x_hub_signature_256 and passes it to verify_github_signature() along with the raw request body and configured secret.
How does the server prevent processing duplicate webhooks?
The server stores a deduplication record in the SQLite webhook_events table via src/storage/database.py immediately after successful verification. Subsequent deliveries with identical identifiers are detected and ignored, ensuring idempotent processing even when GitHub retries failed deliveries.
What happens if signature verification fails?
When verify_github_signature() returns False or the header is missing, the endpoint returns 401 Unauthorized and logs a security warning. The request body is not parsed, no database records are created, and no events are published to the EventBus, protecting your automation from spoofed or tampered payloads.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →