TL;DR: Developers bypass security gates when the gate is slower, noisier, or less predictable than the cost of ignoring it. Design gates around three properties: they block only on new, high-confidence, high-severity findings introduced by the change; they return results within the time budget of the rest of CI; and they offer a fast, audited exception path. Everything else goes to a backlog, not a red build.
"Shift left" became a slogan long before most organisations worked out what it costs. Bolting SAST, SCA, secret scanning, container scanning, and IaC scanning onto every pipeline is easy. Keeping them turned on six months later — without developers adding || true, disabling required checks, or learning which admin will approve an override without asking — is the real engineering problem.
This article treats a security gate as a product with users. It covers why gates get bypassed, a tiered architecture that keeps them fast, the mechanics of baselining and diff-aware scanning, and how to design exceptions and metrics so the system stays honest.
Why developers bypass gates
Bypass is a rational response to incentives. The usual causes:
- Noise. A gate that fails on findings the developer cannot act on — false positives, issues in code they did not touch, vulnerabilities in transitive dependencies with no fixed version — teaches them the gate is not about their change. Practitioner guidance commonly warns that once false positives make up more than a small fraction of blocking findings, teams start merging around the gate instead of fixing issues (secure.com).
- Latency. A 20-minute scan on a pipeline that otherwise takes four minutes is a tax on every commit. Developers will batch changes, push directly, or campaign to make the check optional.
- Inherited debt. Turning on a scanner against a mature codebase produces hundreds or thousands of pre-existing findings. If the gate blocks on all of them, nothing merges until someone disables it.
- Unclear ownership. When a gate fails and the message is a rule ID and a link to a vendor dashboard, the developer has no idea whether to fix, suppress, or escalate.
- No legitimate escape hatch. Real emergencies happen. If the only way through is an admin override with no record, people find that admin.
Each of these has an architectural fix. None of them is "train developers to care more."
Principle 1: block on the delta, not the inventory
The most important design decision is that a pull-request gate evaluates what the change introduced, not the state of the whole repository.
Diff-aware scanning runs the analyser against the base and head of the change and reports only findings that appear in the head but not the base. Semgrep, for example, runs diff-aware scans on pull requests by default, reporting only findings introduced after a baseline commit, and supports an explicit SEMGREP_BASELINE_REF in CI systems where the baseline is not detected automatically (Semgrep docs). CodeQL, SonarQube's "new code" definition, and most commercial SAST tools offer equivalent modes.
For tools without native diff support, implement it yourself with fingerprinting: produce SARIF output on both commits, compute a stable fingerprint per finding (rule ID plus a normalised code snippet or AST hash, not a line number, since line numbers shift), and fail only on fingerprints absent from the base.
# Generic delta gate for a SARIF-producing scanner
git worktree add /tmp/base "$BASE_SHA"
scanner --sarif-output base.sarif /tmp/base
scanner --sarif-output head.sarif .
python3 sarif_delta.py base.sarif head.sarif \
--fail-on "severity>=high,confidence>=high" \
--report new_findings.mdThe pre-existing inventory still matters — it just belongs in a tracked backlog with owners and SLAs, not in every developer's pull request.
Principle 2: tier the gates by speed and confidence
Not every control belongs at the same point in the pipeline. A tiered design keeps the fast path fast:
| Tier | Where | Budget | What runs | Blocks on |
|---|---|---|---|---|
| 0 | Pre-commit / push | Seconds | Secret detection, lint-level security rules | Verified secrets |
| 1 | Pull request | Within existing CI time | Diff-aware SAST, SCA on changed manifests, IaC scan on changed files | New high-severity, high-confidence findings |
| 2 | Merge to main | Minutes | Full SAST, container image scan, SBOM generation, signing | Critical findings with a fix available |
| 3 | Scheduled / async | Hours | Full dependency re-scan against new CVEs, DAST, deep dataflow analysis | Nothing — opens tickets |
| 4 | Deploy / admission | Seconds | Signature and provenance verification, policy checks | Unsigned or unverified artifacts |
Two ideas make this work. First, the blocking set shrinks as the check gets slower and broader: tier 3 never blocks a merge, because a CVE published overnight is not the fault of whoever opens the next pull request. Second, each tier runs in parallel with functional tests, not serially after them, so security adds little or nothing to wall-clock time.
Principle 3: stop secrets at the edge
Secrets are the one category where "detect later" is genuinely too late — once a credential is pushed to a shared remote, you should assume it is compromised and rotate it. That makes secret detection the best candidate for the earliest, strictest gate.
Server-side push protection is more reliable than client-side hooks, which developers can skip. GitHub's push protection, for example, blocks pushes containing detected secrets and requires a bypass reason — false positive, used in tests, or "will fix later"; the last keeps the alert open and notifies administrators (GitHub Docs). That is the exception-path pattern in miniature: blocking by default, a documented override, and an audit trail.
Pair detection with the real fix: make static credentials unnecessary. Workload identity (OIDC federation from CI to cloud providers) and short-lived dynamic secrets remove whole classes of findings rather than detecting them.
Principle 4: severity × confidence × reachability
A CVSS score alone is a poor gating signal. A critical vulnerability in a function your code never calls is less urgent than a medium one on your authentication path. Useful gating inputs:
- Confidence — many SAST rules carry a confidence or precision rating. Block only on high-confidence rules; route the rest to review.
- Fix availability — for SCA, blocking on a vulnerability with no patched version gives the developer nothing to do. Report it, track it, do not block on it.
- Reachability — SCA tools that analyse call graphs can tell you whether vulnerable code is actually invoked. Reachable findings deserve priority.
- Exploit signals — CISA's Known Exploited Vulnerabilities catalogue and EPSS scores help separate theoretical from actively exploited issues.
Encode the policy explicitly, version it, and publish it so developers can see why a finding blocked:
# security-gate-policy.yaml
block:
sast:
min_severity: high
min_confidence: high
scope: new_findings_only
sca:
min_severity: high
require_fix_available: true
prefer_reachable: true
always_block: [in_cisa_kev]
secrets:
verified: block
unverified: warn
warn:
sast: { min_severity: medium }
sca: { min_severity: medium }
exceptions:
source: security-exceptions.yaml
max_duration_days: 90
approvers: [appsec-oncall, service-owner]Principle 5: roll out in report-only mode first
Never enable a new blocking rule cold. Run it in report-only (or advisory) mode for a few weeks, measure its firing rate and false-positive rate, tune or disable noisy rules, fix or exempt the existing hits, and only then flip it to blocking. This mirrors the advisory → soft → hard progression used in infrastructure policy engines, and for the same reason: trust is lost in the first week and hard to win back.
A useful heuristic: a rule should earn its blocking status by demonstrating, in report-only mode, that nearly every hit results in a code change rather than a suppression.
Principle 6: design the exception path deliberately
A gate without an exception path will be bypassed through an unofficial one. Build the official one so that it is:
- Fast — a developer can request an exception from the pull request itself (a comment command or a link to a pre-filled form), not a ticket queue.
- Scoped — tied to a specific finding fingerprint and repository, not "disable rule X everywhere."
- Time-bound — every exception expires, and expiry re-opens the finding.
- Audited — who requested, who approved, why, when. This record doubles as compliance evidence.
- Two-party for high severity — the requester cannot self-approve criticals.
Store exceptions as version-controlled data that the gate reads, just as you would for infrastructure policy. That keeps them reviewable and prevents silent rule edits.
Principle 7: make failures actionable in place
The failure message is the user interface. A good one appears as an inline annotation on the changed line in the pull request, states what is wrong in one sentence, shows a fix (ideally an auto-fix suggestion or a link to a paved-road library), and says how to request an exception. SARIF upload to your code host usually gets you inline annotations for free. Avoid sending developers to a separate dashboard to understand a red build.
Keeping dependency remediation from becoming gate fatigue
Software composition analysis generates the highest volume of findings in most pipelines, and most of them are fixed the same way: bump a version. Treat that as an automation problem rather than a gating problem.
Automated dependency update tools such as Dependabot and Renovate open pull requests when patched versions are released. Configure them to group related updates (all patch releases for a framework, for instance), schedule non-security updates into a weekly window, and raise security updates immediately. Auto-merge low-risk patch updates when the test suite passes, so humans review only the updates that change behaviour.
This changes the gate's job. Instead of blocking a feature pull request because a transitive dependency acquired a CVE overnight, the fix arrives as its own small, reviewable pull request, and the feature work proceeds. The gate still blocks a developer who introduces a new vulnerable dependency, which is the moment they can most cheaply choose a different version or library.
Two failure modes to watch: update pull requests that nobody merges (track their age like any other backlog), and major-version upgrades that stall because they need real engineering effort. The latter deserve explicit planning time, not an indefinitely open bot pull request.
Protect the gate itself
Gates are only meaningful if they cannot be quietly removed:
- Enforce required status checks via branch protection or rulesets at the organisation level, so a repository admin cannot drop them.
- Run security jobs from centrally managed, versioned workflow templates that repositories include rather than copy — a change to the template is reviewed by the platform or AppSec team.
- Verify at deploy time, not only at build time: an admission controller that requires signed artifacts with build provenance ensures that anything reaching production actually went through the pipeline. This is where a verifiable software supply chain turns CI gates into an enforceable guarantee.
- Alert on direct pushes to protected branches and on bypass events.
Metrics that keep the system honest
Track these per gate and per team, and review them regularly:
- Median and p95 added pipeline time attributable to security jobs.
- Block rate — share of pull requests blocked by a security gate.
- Fix rate vs suppression rate for blocked findings. A rising suppression share is an early warning of noise.
- Exception volume and age, including exceptions past expiry.
- Mean time to remediate backlog findings by severity, against your SLAs.
- Bypass events — admin overrides, disabled checks, direct pushes.
If block rate is high and fix rate is low, the gate is noisy. If block rate is near zero for months, check it is actually running.
A reference flow
- Developer pushes → server-side push protection rejects verified secrets.
- Pull request → diff-aware SAST, SCA on changed manifests, and IaC scan run in parallel with unit tests; only new high-confidence, high-severity findings with an available fix block; everything else is annotated as a warning.
- Merge → full scans, image scan, SBOM generation, signing with provenance.
- Nightly → full re-scan against newly published CVEs; new issues become tickets routed to owning teams with SLAs.
- Deploy → admission policy verifies signature and provenance; unsigned artifacts are rejected.
This fits into existing DevOps and CI/CD tooling with little new infrastructure, and pairs naturally with identity management work that replaces static credentials entirely.
Syslabs' engineering team helps teams design security pipelines like this as part of its cybersecurity consulting work.
Sources: secure.com — How to implement effective AppSec security gates in CI/CD · Semgrep documentation — Findings in CI and diff-aware scans · GitHub Docs — Push protection; Working with push protection · CISA — Known Exploited Vulnerabilities catalog · FIRST — Exploit Prediction Scoring System (EPSS)