Agent skill
Argo CD Webhooks Skill
Receive and authenticate Argo CD notification webhooks. Use when setting up an endpoint for the Argo CD Notifications webhook service, adding a secret header in argocd-notifications-cm, handling GET or POST requests with an empty or templated body, or debugging 401s for sync-succeeded and health-degraded notifications.
Install this skill
npx skills add hookdeck/webhook-skills --skill argo-cd-webhooks
The Argo CD Notifications webhook service (service.webhook.<name> in argocd-notifications-cm) sends a generic HTTP request when a trigger fires, for example a sync succeeds or an app's health degrades. Requests are not signed, and there is no vendor event field: the body is whatever your template renders. You authenticate a request with a header you add to the webhook service (headers), and your endpoint compares it with the value you stored.
When to Use This Skill
- How do I receive Argo CD notification webhooks?
- Does Argo CD sign webhook requests? (No: use a shared secret header)
- How do I add a secret header in
argocd-notifications-cmand verify it? - Why does my Argo CD webhook arrive with no body? (The template's
bodyis empty, or Next.js dropped a GET body) - How do I handle sync-succeeded or health-degraded notifications?
Setup Note
Argo CD webhooks are not signed. Add a secret header to the webhook service (service.webhook.<name>.headers in argocd-notifications-cm) and enter it here. Argo CD sends GET unless the notification template sets a method, so GET and POST are both allowed.
Hookdeck's Argo CD source pre-selects GET and POST without managing the method, so if a template uses PUT or PATCH, allow that method on the source too. basicAuth on the Argo CD service sends Authorization: Basic <base64(username:password)> on every request; to use it, enter header name Authorization and the full Basic <base64> value.
Verification (core)
const crypto = require('crypto');
// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
const ha = crypto.createHash('sha256').update(String(a)).digest();
const hb = crypto.createHash('sha256').update(String(b)).digest();
return crypto.timingSafeEqual(ha, hb);
}
function verifyArgoCdRequest(
headers,
secret = process.env.ARGO_CD_WEBHOOK_SECRET,
headerName = process.env.ARGO_CD_WEBHOOK_HEADER || 'Authorization'
) {
if (!secret) return false; // never accept when unconfigured
const received = headers[headerName.toLowerCase()]; // exact, whole value (no Bearer parsing)
return Boolean(received) && safeEqual(received, secret);
}
- The header name is user-chosen (
service.webhook.<name>.headers). Read it fromARGO_CD_WEBHOOK_HEADER(defaultAuthorization); the expected value isARGO_CD_WEBHOOK_SECRET, the whole header value including anyBearerprefix you typed. Do not strip or parse the prefix. - Return
401for a missing header or a mismatch,400for a body that isn't a JSON object,200once handled. An empty body is a valid200. GET requests carry the template body too. - A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Use HTTPS and rotate the credential if it leaks.
- Accept
GET,POST,PUT,PATCHandDELETEon the route and check the credential first for every method. Read the body raw (express.raw({ type: '*/*' })) and parse it only after the check.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Configuration Example
data:
service.webhook.deploy-events: |
url: https://your-app.example.com/webhooks/argo-cd
headers:
- name: Authorization
value: $webhook-secret # key in argocd-notifications-secret
template.sync-succeeded-webhook: |
webhook:
deploy-events:
method: POST
body: |
{"trigger": "on-sync-succeeded", "app": "{{.app.metadata.name}}",
"sync_status": "{{.app.status.sync.status}}", "health": "{{.app.status.health.status}}"}
trigger.on-sync-succeeded: |
- when: app.status.operationState.phase in ['Succeeded']
send: [sync-succeeded-webhook]
See references/setup.md for the Secret and the subscription annotation.
Illustrative Payload
Argo CD has no event field. The examples handle an illustrative payload whose trigger field is written by your own template, so rename it to suit your setup.
trigger (illustrative) | Sent When |
|---|---|
on-sync-succeeded | The sync operation phase is Succeeded |
on-sync-failed | The sync operation phase is Error or Failed |
on-health-degraded | The app's health status is Degraded |
Environment Variables
ARGO_CD_WEBHOOK_HEADER=Authorization # header name you added to the webhook service
ARGO_CD_WEBHOOK_SECRET=Bearer your-shared-secret # exact, whole header value
Local Development
Argo CD can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 argo-cd --path /webhooks/argo-cd
Use the printed URL as the url of the webhook service.
Resources
- references/overview.md - What Argo CD notification webhooks are, methods and payload shape
- references/setup.md - Secret, webhook service, template and trigger configuration
- references/verification.md - Header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests