Create a Slack app, turn on incoming webhooks, authorize one channel, and copy the webhook URL into your CI secret store. Then add a step that sends a JSON body with a text field to that URL. Treat the URL as a credential and keep alerts short, linked and rare.
Setup in five steps
- Create a Slack app for your workspace and enable incoming webhooks.
- Add a new webhook and pick the channel it may post to. Slack's documentation notes that for a private channel you must already be a member.
- Copy the webhook URL into your CI system as a secret.
- Add a CI step that sends an HTTP POST with a JSON payload.
- Test it from a branch before it runs on your main pipeline.
The step is small:
- name: Notify Slack
if: failure()
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
curl --fail -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"Security scan failed: $RUN_URL\"}" \
"$SLACK_WEBHOOK_URL"
The URL is a secret
Slack's own guide says the webhook URL contains a secret, that you should not share it online including in public version control, and that Slack actively searches for leaked secrets and revokes them. Anyone holding the URL can post to your channel, so a leaked one lets an attacker send convincing fake alerts. Our post on whether a Slack incoming webhook URL is a secret goes deeper. If you suspect exposure, revoke the webhook in the app settings and create a new one.
Mind the rate limit
Slack's rate limit page lists incoming webhooks at roughly one message per second, with short bursts allowed. If you exceed the limit, Slack returns HTTP 429 with a Retry-After header telling you how many seconds to wait. A scan that loops over 300 findings and posts each one will hit this quickly, and the channel becomes unreadable long before the limit matters.
Post one summary per run instead. A good alert has four parts: what happened, where, how bad, and a link to the details. For wording ideas see writing a security alert in chat that people act on.
Decide what is worth a message
| Event | Suggested channel behaviour |
|---|---|
| Gate blocked a release | Post to the team channel with a link |
| New finding on an internet facing service | Post to the security channel |
| Routine findings | Weekly digest, not a message |
| Scan job itself failed | Post, because silence looks like a clean result |
The last row matters most. A scan that fails quietly looks the same as a scan that found nothing.
Guardrails
- Keep the webhook per channel and per purpose, so revoking one does not break the rest.
- Never put secrets, tokens or customer data in the message body.
- Build the message from fixed text plus a link, not from raw finding text that an outsider could influence.
- Add a retry that respects Retry-After, and stop after a few attempts.
Try it yourself
Click through it at your own pace, or open it full screen.
Watch how continuous monitoring surfaces new findings, then decide which of them would deserve a chat message.
Safeguard offers policies and gates for your pipeline, and a free scan at gold.safeguard.sh/scan if you want to see findings before you design alert routing.
Quick check
Can I post to several channels with one webhook?
No. Each incoming webhook is tied to the channel you authorized, so create one per channel.
What happens if I send too many messages?
Slack returns HTTP 429 and a Retry-After header. Wait that many seconds before trying again, and send fewer, richer messages.
Sources
Last reviewed: 19 October 2026.