Last reviewed: 2026-09-03

Direct answer

A repository webhook should never launch a coding agent merely because it reached the correct endpoint. Treat the webhook receiver as a security boundary: preserve the exact request body, verify the provider-specific signature, apply replay and duplicate controls, allow only approved repositories and event actions, and create a durable queue record before acknowledging the delivery.

A sound coding agent webhook security design separates five decisions that are easy to blur together:

  1. Authenticity: Did the configured repository provider send this request?
  2. Integrity: Are the received bytes identical to the bytes that provider signed?
  3. Freshness and uniqueness: Is the delivery recent where a signed timestamp is available, and has its delivery identifier already been processed?
  4. Authorization: Is this repository, event type, action, and operating mode allowed to start an agent?
  5. Durability: Was one recoverable job committed before the endpoint reported success?

The signature answers only the first two questions. A correctly signed issue, push, or merge-request event can still be outside your automation policy. Conversely, a legitimate redelivery can repeat a previously accepted event. The receiver must therefore finish every gate before any agent workspace, model call, or side-effecting tool is created.

Who this is for

This guide is for platform engineers and repository maintainers who automatically start coding agents from GitHub, GitLab, or a compatible webhook producer. It is most relevant when an event can consume compute, read private code, open a pull request, or invoke tools.

It is less relevant to fully manual workflows in which a person reviews an event and deliberately starts every run. Even then, the same boundary is useful if a webhook populates the review queue.

Key takeaways

  • Verify the signature against the original request bytes before parsing or transforming the payload.
  • Use a constant-time signature comparison and fail closed when required headers are missing.
  • Apply a signed timestamp freshness rule when the provider supplies one, but always deduplicate a stable delivery identifier.
  • Allowlist event types, actions, repository identities, and permitted run modes after authentication.
  • Claim the delivery identifier atomically so concurrent retries cannot enqueue two agents.
  • Acknowledge only after a durable job exists; keep the HTTP handler fast and run the agent asynchronously.
  • Log decisions and identifiers, not request bodies, signature values, endpoint secrets, or sensitive URLs.

Sources checked

  • GitHub guidance for validating webhook deliveries says to validate the X-Hub-Signature-256 HMAC-SHA256 value against the payload before further processing. It also recommends UTF-8 handling and constant-time comparison rather than ordinary equality.
  • GitHub webhook best practices recommends subscribing only to needed events, checking the event type and action, using X-GitHub-Delivery for uniqueness, and moving work to an asynchronous queue so the endpoint can respond within ten seconds.
  • GitLab webhook documentation recommends signing tokens for new webhooks. Its current contract signs the message identifier, timestamp, and raw JSON body with HMAC-SHA256, and it tells receivers to check timestamp freshness and compare signatures in constant time.
  • The Standard Webhooks specification provides the provider-neutral model behind the GitLab signing contract: authenticate the payload together with its timestamp and unique message identifier, then use the identifier for idempotent processing.

These sources agree on the central control—authenticate before processing—but their wire contracts differ. Implement provider adapters rather than assuming one header or signing format works everywhere.

Contract details to verify

Preserve the signed bytes

Read the request body once as bytes and retain it unchanged until verification finishes. Do not let middleware parse JSON and serialize it again first. Whitespace, Unicode handling, or key-order changes can produce different bytes and make a valid signature fail. GitHub explicitly warns against modified payloads and calls for UTF-8 handling; GitLab defines its signed message using the raw JSON request body.

Apply a documented request-size limit before allocating excessive memory, but do not normalize an accepted body. Parse the verified bytes only after authentication succeeds. If a proxy, gateway, or framework can decompress or rewrite a body, add an integration test at the externally reachable endpoint rather than testing only the application function.

Keep provider verification explicit

Route each configured webhook to an adapter with a known provider and signing contract. For GitHub, verify the HMAC-SHA256 value from X-Hub-Signature-256 over the payload contents. For GitLab signing tokens, verify the signature over the message identifier, timestamp, and raw body. Use the provider-supported format and a constant-time comparison. Missing, malformed, or nonmatching signature data must stop the request before parsing can trigger work.

A simplified receiver should follow this order:

receive raw body, headers, and configured route policy
enforce method and local size policy
select the provider adapter from trusted route configuration
verify the required signature over the original bytes
validate signed timestamp freshness when the adapter supports it
parse the authenticated payload
validate event, action, repository, and run-mode policy
atomically claim the delivery identifier
commit one durable agent job
return the configured success response

Do not infer the provider from an unverified payload field. A dedicated route or trusted server-side configuration should determine which contract applies.

Combine freshness with idempotency

GitLab and Standard Webhooks include a timestamp in the signed material. Define an acceptable age and future-clock-skew window, monitor clock synchronization, and reject deliveries outside that local policy. Because the timestamp is part of the signed message, changing it should also invalidate the signature.

GitHub’s cited contract instead emphasizes X-GitHub-Delivery as a unique event identifier and notes that a requested redelivery keeps the original value. Store the identifier under a uniqueness constraint before enqueueing work. A check followed later by a separate insert is vulnerable to a race: two requests can both observe no record and both launch agents.

Use a small delivery state machine such as received, enqueued, completed, and retryable_failure. Retain deduplication records long enough to cover your documented redelivery and incident-recovery window. There is no universal retention period in the checked sources, so make that duration an explicit operational choice.

Authorize the event after authentication

Check the exact event type and action against an allowlist. Also bind the route to approved repository identities and the least-privileged run mode. A valid signature proves delivery authenticity and payload integrity; it does not prove that every authentic event should receive an agent.

Normalize only the fields the scheduler needs, such as provider, repository identifier, event type, action, ref, and delivery identifier. Keep user-authored issue or comment text as untrusted data even after webhook authentication. For the next layer, review permission and secret boundaries for coding agents and repository prompt-injection defenses .

Make acknowledgement depend on durable enqueueing

GitHub expects a successful response within ten seconds and recommends asynchronous processing. Keep signature, replay, policy, and enqueue checks in the receiver, but move repository checkout and agent execution to a worker.

Choose response semantics deliberately and test them with your provider. One defensible local contract is:

  • Reject missing or invalid authentication without enqueueing.
  • Reject malformed authenticated payloads without enqueueing.
  • Acknowledge a previously committed delivery as an idempotent no-op.
  • Acknowledge an authenticated but intentionally ignored event without creating work.
  • Return a retryable server error if the durable queue commit fails.

Do not return success before the queue transaction commits. Do not hold the webhook connection open while an agent runs.

Happy path and error path

In the happy path, an approved repository sends an allowed event. The endpoint captures the raw bytes, verifies the provider signature, checks freshness where available, and parses the payload. A database transaction inserts the delivery identifier and one minimal job record. The queue commit succeeds, the receiver returns success, and a worker later creates the agent run under separately configured permissions.

In an authentication error path, the signature header is missing or does not match. The endpoint records a sanitized rejection reason, returns the configured authentication failure, and creates neither a delivery claim nor a job.

In a replay path, the signature is valid but the delivery identifier already has an enqueued or completed record. The endpoint records a duplicate decision and acknowledges it without launching another agent.

In a queue-outage path, verification and policy checks pass but the durable commit fails. The receiver records a retryable failure and returns a server error. On redelivery, the transaction either completes the previously uncommitted job or recognizes an already committed job; it never creates two.

Log decisions without logging secrets

A useful sanitized event record can contain:

received_at
provider
endpoint_route_id
delivery_id_hash
event_type
action
repository_id
signature_result
freshness_result
dedupe_result
policy_result
payload_size_bytes
queue_result
agent_job_id
response_status
latency_ms

Prefer an internal route identifier over the full webhook URL, and hash a delivery identifier in broadly accessible logs if the raw value is not needed there. Never log the signing material, received signature, raw request body, authorization headers, or user-controlled content by default. Keep detailed payload evidence in a more restricted system only when a documented need and retention policy justify it.

Failure modes

  • Parsing before verification: JSON middleware changes the signed representation, causing legitimate requests to fail or encouraging an unsafe fallback that skips verification.
  • Using ordinary equality: A normal string comparison ignores GitHub and GitLab guidance to use constant-time comparison for signature checks.
  • Silently accepting a weaker contract: GitHub identifies its SHA-1 header as legacy, while GitLab says its plain-text secret-token header offers weaker guarantees than the newer signing-token contract. Migration compatibility should be explicit, observable, and temporary.
  • Treating authenticity as authorization: A real event from an unapproved repository, branch, or action starts a powerful agent because policy checks were omitted.
  • Deduplicating after enqueue: Two simultaneous deliveries both create jobs before either writes the identifier record.
  • Acknowledging too early: The endpoint returns success and then loses the in-memory job during a crash, so the event never reaches an agent.
  • Acknowledging too late: The endpoint waits for checkout or agent completion, exceeds the provider’s webhook response window, and invites unnecessary redelivery.
  • Ignoring clock behavior: A strict freshness window rejects valid signed deliveries when receiver clocks drift. A wide, unmonitored window weakens replay protection.
  • Assuming delivery deduplication equals business deduplication: GitLab documents that matching group and project webhooks can both fire. If the business rule permits only one agent for the underlying repository change, add a separate, stable coalescing key after authenticating both deliveries.
  • Logging the evidence you meant to protect: Raw bodies can contain repository or user data, while signature values and endpoint configuration do not belong in ordinary application logs.
  • Using an IP allowlist as the only proof: GitHub presents IP allowlisting as an additional defense and notes that its ranges can change. Keep it updated if used, but retain cryptographic verification as the primary authenticity check.
  • Letting proxy behavior differ from tests: Unit tests pass against pristine bytes while production middleware rewrites or decodes the body before the verifier sees it.

FAQ

Is a valid signature enough to launch an agent?

No. It establishes provider authenticity and payload integrity under the configured signing contract. You still need event, action, repository, and run-mode authorization, followed by duplicate protection and a durable queue commit.

Can the receiver parse JSON before checking the signature?

Not safely when the provider signs the raw payload. Capture the original bytes, verify them, and only then parse. Re-serialization can alter whitespace, ordering, or Unicode representation.

Do I need both a timestamp check and a delivery identifier?

Use both when the signed provider contract supplies both. The timestamp constrains how old a delivery may be; the identifier prevents the same acceptable delivery from being processed twice. For a contract without the cited signed timestamp, do not invent one and assume it is authenticated—rely on the provider’s supported identifier and your documented adapter behavior.

What should happen when the same delivery arrives again?

Perform an atomic lookup or insert. If a durable job already exists, return the response your provider contract uses for an acknowledged no-op and do not enqueue again. If an earlier attempt failed before committing, let the state machine complete exactly one job.

Should ignored events return success?

Often that is the cleanest local policy because the event was received correctly but intentionally excluded. The important point is to distinguish ignored_by_policy from authentication_failed and queue_failed, then test how the selected provider handles each response.

Should I use an IP allowlist too?

It can be useful as defense in depth. GitHub recommends allowing its published delivery ranges and refreshing them because they can change. An allowlist does not replace signature verification, freshness controls, or idempotency.

Can an authenticated payload be passed directly into the agent prompt?

No. Authentication says who delivered the payload, not that every user-authored field is safe instruction. Normalize scheduling fields, keep repository text as quoted data, and apply the agent’s own instruction and permission boundaries before execution.

Reader next step

Implement the receiver as a small, testable gate before connecting it to an agent runner. Build fixtures for a valid delivery, a one-byte body change, a missing signature, a stale signed timestamp, the same identifier twice, concurrent duplicate requests, an allowed event with a disallowed action, Unicode payloads, and a queue outage followed by redelivery.

Run those fixtures through the real proxy and framework path with agent execution disabled. Confirm that sanitized logs explain each decision without recording payloads or signing data. Then enable one approved repository in the least-privileged mode, verify that one delivery produces one durable job, and add the control to your coding-agent runbook quality gate .