← AppSec Signal

How to Set Up PR-Blocking Security Gates Without Destroying Developer Velocity

Maya Chen 11 min read
CI pipeline diagram showing PR security gate with reachability filter before block decision

PR-blocking security gates are one of the more politically charged topics in AppSec. Get them wrong in one direction and engineers bypass your scanner within two weeks. Get them wrong in the other direction and genuinely exploitable CVEs ship to production without review. The failure mode we see most often is the first kind: a CVSS-threshold gate configured to block on high-severity findings, which in practice blocks on every PR that touches dependencies, because almost every dependency tree has something with a CVSS 7+.

After that gate has blocked a few urgent deploys, the workaround patterns emerge: the security scanner gets disabled in the feature branch pipeline, or an exception list grows to include everything until it's effectively empty, or engineering leadership calls AppSec and asks them to "calibrate" the thresholds up to 9.0 so the gate stops firing constantly. By then the gate is providing minimal security value and has cost significant political capital.

The problem isn't that PR blocking is a bad idea. It's that CVSS scores are the wrong input for the block decision.

What the Block Decision Should Be Based On

A PR should be blocked on a security finding if and only if: the PR introduces or changes code that creates a confirmed-reachable call path to a vulnerable function, AND that vulnerability's severity is high or critical, AND a patch is available.

Each of those three conditions matters:

  • Introduces or changes the reachability: If the CVE was already present and reachable before this PR, it's not a new risk introduced by this PR. Block it at the triage queue level, not the PR gate level. PR gates should catch new risk introductions, not audit the entire existing security posture on every commit.
  • Confirmed reachable: The call path from at least one entry point to the vulnerable function must exist in the post-merge call graph. CVSS score alone is not sufficient.
  • Patch available: Blocking a PR for a vulnerability with no available fix forces engineers to choose between shipping the feature and shipping nothing. That's not a security decision — it's a no-op. Track unfixable reachable CVEs in a separate queue with manual review, but don't use them as PR blockers.

The New-Reachability Delta Approach

The most precise PR gate implementation compares the reachability profile of the post-merge branch against the base branch, and only flags CVEs where the PR increased the reachable set. This is a diff-based approach.

Concretely: if CVE-2024-12345 in [email protected] was already reachable in the base branch, it doesn't appear in the PR gate output for a PR that bumps an unrelated package. The security team already knows about it; it's in the triage queue. Adding it to every PR that happens to share the same lockfile is pure noise.

If a PR adds a new route handler that calls into a function that calls into lodash's vulnerable code path — and that path wasn't reachable before — that's a new finding scoped to this PR. Block that one.

The implementation in Patchlynx's CI integration:

# .github/workflows/security.yml
- name: Patchlynx reachability scan (PR delta)
  uses: patchlynx/scan-action@v1
  with:
    mode: pr-delta
    base-ref: ${{ github.base_ref }}
    block-on: reachable-high-critical-with-fix
    report-unreachable: false
    comment-on-pr: true

The mode: pr-delta flag tells the scanner to compute the diff between base and head. block-on: reachable-high-critical-with-fix constrains the block condition to the three criteria above. report-unreachable: false keeps the PR comment clean — unreachable findings go to the dashboard, not the PR thread.

What Goes into the PR Comment

The PR comment format matters a lot for adoption. Engineers who see a well-formatted, actionable security comment trust the tool. Engineers who see a wall of CVE IDs and boilerplate stop reading after the second PR.

A good PR security comment for a blocking finding looks like this:

PATCHLYNX: 1 new reachable finding — PR blocked

CVE-2024-09999 | [email protected] | CVSS 8.2 | HIGH
Status: CONFIRMED REACHABLE (introduced by this PR)
Path: POST /api/users/validate → controllers/users.js:validate
  → express-validator/src/validation-chain.js:run
  → [vulnerable function]
Fix: Upgrade to [email protected] (no API changes)
GHSA advisory: GHSA-xxxx-xxxx-xxxx

The engineer sees: what it is, why it's blocked (confirmed reachable, not just high CVSS), the specific call path, and the fix. That's 2 minutes of reading and a 10-minute fix in most cases. Engineers start trusting the gate because every block is real and actionable.

Handling the No-Fix-Available Case

The trickiest scenario is a confirmed-reachable high CVE where no patched version exists. This happens with recently disclosed vulnerabilities before the maintainer has shipped a fix, or with abandoned packages.

We don't recommend blocking PRs in this case. The engineer can't fix what doesn't have a fix. The correct response is:

  1. Report the finding in the PR comment as a non-blocking warning with the full call path trace.
  2. Automatically create a tracking ticket in Jira (or your equivalent) with the finding linked to the PR that introduced the reachability.
  3. Set a review deadline: when a fix is published, the ticket triggers a notification and becomes a P1 patch item.

This approach keeps the PR unblocked while creating an audit trail that proves the team was aware of the finding. For compliance purposes, that audit trail is often more valuable than a block that would have been overridden by exception anyway.

Tuning for False Positive Tolerance

Even with reachability-scoped gates, you'll encounter false positives from dynamic dispatch edge cases (described in our previous post). Over time you'll identify patterns specific to your codebase where the static analysis over-approximates. The right response is suppression rules with documented rationale, not raising the threshold globally.

Patchlynx supports suppression rules in a .patchlynx.yml config:

suppress:
  - cve: CVE-2024-09876
    package: [email protected]
    reason: "Dynamic dispatch edge - event handler only invoked
             by internal scheduler, never by HTTP entry point.
             Verified 2026-01-15. Review by 2026-04-15."
    expires: 2026-04-15

Two requirements we enforce for suppression rules: a documented reason and an expiry date. Suppressions that expire get re-evaluated automatically. A suppression that made sense when the codebase was structured one way may need revisiting after a refactor. Permanent suppressions accumulate without review and gradually erode your gate's coverage.

Rolling Out Gates Without Blowback

If your team currently has no PR security gate, or has one that everyone ignores, rolling out reachability-scoped gates still requires communication. The order that tends to work:

  1. Run the scanner in report-only mode for two weeks. Share the results with engineering leads. Show them the signal-to-noise ratio: "here are the 5 confirmed-reachable CVEs in your repos, here are the 240 advisories we're filtering out."
  2. Enable PR comments (non-blocking) for another week. Let engineers see the format and calibrate expectations.
  3. Enable blocking on new reachable critical findings only. Not high — critical. This will rarely fire on a real codebase and builds trust that the gate is selective.
  4. After a month of near-zero blocks at the critical tier, enable blocking at high as well. By this point engineers have seen that the tool doesn't cry wolf.

We're not saying this sequence is mandatory. If you have organizational authority to gate-and-explain after the fact, skip steps 1–2. But at smaller teams where AppSec doesn't have pre-existing trust capital with engineering, this rollout pattern avoids the "security team broke our deploys" narrative that kills security tooling adoption.

The goal isn't 100% block coverage on every possible CVE. The goal is that every PR block is defensible, actionable, and accurate — so that when a block fires, it gets fixed instead of bypassed.