Agent skill

Cloudsmith Webhooks Skill

Receive and verify Cloudsmith webhooks. Use when setting up Cloudsmith webhook handlers, debugging X-Cloudsmith-Signature verification, or handling package events like package.created, package.synced and package.security_scanned.

Install this skill

npx skills add hookdeck/webhook-skills --skill cloudsmith-webhooks


When to Use This Skill

  • How do I receive Cloudsmith webhooks?
  • How do I verify the X-Cloudsmith-Signature header?
  • How do I handle package.created, package.synced or package.security_scanned events?
  • Why is my Cloudsmith webhook signature verification failing?
  • Why are my Cloudsmith webhook deliveries unsigned?

Setup note

Set an HMAC signature key on the Cloudsmith webhook. Without one, deliveries are unsigned and fail verification.

Verification (core)

Cloudsmith signs the raw request body bytes with HMAC-SHA1, keyed with the webhook's HMAC Signature Key (UTF-8, used as-is), and sends the bare lowercase hex digest (40 characters) in X-Cloudsmith-Signature. There is no prefix and no timestamp; the whole header value is the digest. Validate the format before decoding, then compare in constant time.

Node:

const crypto = require('crypto');

function verify(rawBody /* Buffer */, signature, secret) {
  if (!signature || !secret || !/^[0-9a-fA-F]{40}$/.test(signature)) return false;
  const expected = crypto.createHmac('sha1', secret).update(rawBody).digest();
  const received = Buffer.from(signature, 'hex'); // safe: format validated above
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

Python:

import hashlib, hmac, re

def verify(raw_body: bytes, signature: str | None, secret: str | None) -> bool:
    if not signature or not secret or not re.fullmatch(r"[0-9a-fA-F]{40}", signature):
        return False
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha1).digest()
    return hmac.compare_digest(bytes.fromhex(signature), expected)

Read the body as raw bytes (express.raw({ type: '*/*' }), await request.arrayBuffer(), await request.body()) and parse JSON only after the check. A missing, empty, malformed or mismatching signature gets 401.

For complete handlers with route wiring, event dispatch, and tests, see:

Event Types

Dispatch on the body's meta.event_id. The event is not in a header.

meta.event_idTriggered when
pingYou send a test from the webhook's page
package.createdA package is uploaded
package.syncingA package starts syncing
package.syncedA package is fully synchronised and ready
package.security_scannedA security scan finishes
package.quarantinedA package is quarantined
package.failedA package fails to sync
package.releasedA package is released
package.restoredA package is restored
package.tags_updatedA package's tags change
package.deletedA package is deleted

meta.event_id is reliably there only for the default JSON Object payload format. The JSON Array format wraps the object in [...], Form Encoded sends payload=<url-encoded JSON>, and a Handlebars template sends whatever it emits. Keep the default format unless you handle the others.

For payload examples, see Cloudsmith webhooks

Important Headers

HeaderDescription
X-Cloudsmith-SignatureBare hex HMAC-SHA1 of the raw body (only when a signature key is set)
Secret Header / Secret ValueOptional custom header you define on the webhook, sent with every delivery

Environment Variables

CLOUDSMITH_WEBHOOK_SECRET=your_hmac_signature_key   # "HMAC Signature Key" of the webhook; it can be replaced but not read back

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 cloudsmith --path /webhooks/cloudsmith

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 11, 2026

View on GitHub →