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:

  1. Payload URL: https://<your-host>/webhooks/github
  2. Content type: Select application/json
  3. Secret: Paste the identical value stored in GITHUB_WEBHOOK_SECRET
  4. Events: Choose Just the push event (or select specific events based on your automation needs)
  5. 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_events table in src/storage/database.py to detect and skip duplicate deliveries
  • Publishes a WebhookEvent to the internal EventBus defined in src/events/bus.py for 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_SECRET environment variable to configure Settings.github_webhook_secret in src/config/settings.py.
  • Register the webhook URL /webhooks/github in your GitHub repository settings with content type application/json.
  • The FastAPI endpoint in src/api/server.py validates the X-Hub-Signature-256 header using verify_github_signature() from src/api/auth.py.
  • Invalid signatures trigger an immediate 401 Unauthorized response without further processing.
  • Verified events are deduplicated via SQLite and published to the EventBus for 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →