โ† Back to writing

From Scanning Every Commit to Listening for One Webhook

Jul 2026 ยท 9 min read

The original setup for vulnerability remediation at MassMutual was the obvious one: scan every commit, on every PR, across every registered repo. It works, in the sense that it catches vulnerabilities. It also means re-running a full dependency scan on commits that touch a README, or a test fixture, or a CSS tweak โ€” none of which could possibly introduce a new vulnerability โ€” because the trigger was "a commit happened," which is only a proxy for the thing you actually care about: "a new vulnerability now exists."

Snyk already knows the answer to the question that actually matters. It emits a webhook the moment it detects a new issue. Scanning proactively on every commit was duplicating work a tool upstream was already doing, just less precisely and far more often. That reframing โ€” trigger on the real event, not a proxy for it โ€” is the entire idea behind Snyk Auto-Fix.

The shape of the service

Auto-Fix is a small Express + Octokit service with one job: listen for Snyk webhooks, and turn actionable ones into pull requests, automatically, without a scan ever running against code that didn't need one.

Verifying the trigger before trusting it

A service that automatically opens pull requests on someone else's say-so is a service that needs to be very sure about whose say-so it's actually acting on. Every incoming webhook is verified against an HMAC-SHA256 signature in the x-hub-signature header before anything else happens. Skip that check and you've built a system where anyone who can guess your webhook URL can make PRs open across every registered repo โ€” not catastrophic on its own, but exactly the kind of low-effort abuse vector that a shared secret check closes for free.

Batching, so one burst of issues becomes one PR, not five

Snyk doesn't always report issues one at a time โ€” a single dependency bump upstream can surface several related vulnerabilities in the same repo within seconds of each other. Issues are buffered per repo over a configurable window (30 seconds by default) before processing, so a burst becomes one batch, and one batch becomes one PR, instead of five competing PRs touching the same package.json. Anything already seen โ€” a duplicate delivery, or an issue a team has explicitly suppressed in the Snyk UI โ€” gets filtered before it ever reaches the PR-creation path, because nagging a team about something they already decided not to fix is worse than saying nothing.

Two kinds of fix need two different levels of trust

A direct dependency bump โ€” the vulnerable package is listed straight in package.json โ€” gets a plain npm install --save pkg@safeVersion, with one guard: if the currently pinned version already satisfies the fix version, skip it rather than silently downgrading something a human pinned deliberately. A transitive dependency โ€” vulnerable only two or three levels down someone else's dependency tree โ€” gets an overrides (or resolutions, for yarn) entry instead, since there's no direct line in the manifest to bump.

Neither path is trusted equally regardless of what it changes. Every fix is classified into one of three risk tiers: a minor or patch bump within the existing semver range is safe enough for a normal PR with a plain snyk-auto-fixlabel. A major version bump, or a fix version that falls outside the parent package's stated acceptable range, gets opened as a draft PR labeled snyk-needs-reviewinstead โ€” automation can tell you a fix is technically available; it can't tell you whether a major version bump is going to break something downstream, and pretending otherwise is how you end up with automated PRs nobody trusts enough to merge quickly.

A preflight check runs before any of this is dispatched, looking for peer-dependency and engine conflicts the bump would introduce โ€” there's no point opening a PR that's going to fail CI for a reason that had nothing to do with the vulnerability itself.

The dispatch boundary: small blast radius by design

The one architectural decision I'd point to first if asked to justify this system's design: Auto-Fix itself never runs npm install and never pushes a commit. It dispatches a workflow_dispatchevent to a GitHub Actions workflow that already lives in the target repo, and that workflow โ€” running under the target repo's own permissions, its own review requirements, its own CI โ€” does the actual install, commit, and gh pr create. It then calls back to Auto-Fix with the PR number so the central service can track it.

That indirection is deliberate, not incidental. A central service that holds direct write credentials to every registered repo is a single point of compromise with an enormous blast radius โ€” if it's ever compromised, it can push to anything it has access to. A central service that can only aska repo's own workflow to run is a much smaller surface: the worst it can do is trigger a workflow that still has to pass through the target repo's existing CI, branch protection, and review process before anything merges. The automation gets the convenience of "open the PR for me" without inheriting the keys to every repo it touches.

PRs are a claim, not a fact, until something checks back in

The system doesn't open a PR and consider its job done. Stale PRs โ€” open past a configurable age with no action โ€” get flagged and eventually closed automatically. And if Snyk later reports that an issue behind an already-open PR has been resolved some other way (a removedIssuesfield in a later webhook), that PR gets auto-closed too, because a fix PR for a problem that no longer exists is just noise sitting in someone's review queue.

The general shape of the lesson

The real change here wasn't "we automated PR creation" โ€” plenty of tools do that. It was recognizing that the previous system was scanning on a proxy signal ("a commit happened") when the actual signal ("a new vulnerability exists") was already available as an event, if you were willing to listen for it instead of polling around it. Once the trigger was right, the rest of the design โ€” batching, risk tiers, the dispatch boundary โ€” followed from asking "how much should this system be trusted to act alone," question by question, rather than from a single up-front architecture diagram.