A snyk test command can find forty vulnerabilities in a dependency tree and still exit with status 0, letting a pipeline merge straight to production. That is not a bug — it is two flags working exactly as documented. --severity-threshold and --fail-on are the pair of controls that decide which findings a Snyk CLI scan surfaces and which of those findings are actually allowed to break a build. Teams that treat them as interchangeable, or set one without the other, end up with either a pipeline that blocks on every low-severity transitive dependency or one that silently waves through criticals with no available fix path. Understanding exactly what each flag filters, in what order, and what exit code the CLI returns afterward is the difference between a gate that reflects real risk tolerance and one that just generates noise or false confidence. Here is how the mechanics actually work.
What does --severity-threshold actually filter?
--severity-threshold sets a floor on which vulnerability severities are included in a scan's output at all — anything below it is dropped from the results before any pass/fail decision is made. Snyk CLI recognizes four severity tiers: low, medium, high, and critical. Running snyk test --severity-threshold=high reports only high and critical findings; medium and low vulnerabilities are excluded from the report entirely, not just hidden from the failure logic. If no threshold is set, the CLI defaults to low, meaning every detected vulnerability is reported.
Severity in Snyk's output is derived from the vulnerability's underlying CVSS score but is not a raw pass-through of NVD data. Snyk's security research team can adjust a published severity rating up or down based on exploit maturity, real-world exploitability, and reachability signals in Snyk's own vulnerability database (Snyk Intel). That means the same CVE can show high in Snyk's CLI output while NVD lists it as medium, or vice versa — a detail that matters if a team's compliance policy assumes CVSS and Snyk severity are always identical.
How does --fail-on decide whether the build actually breaks?
--fail-on operates after severity filtering and answers a different question: not "is this vulnerability severe enough to show," but "is this vulnerability fixable enough to justify blocking the pipeline." It accepts three values: all, upgradable, and patchable. --fail-on=upgradable causes the CLI to exit with a failure only when at least one reported vulnerability can be resolved by bumping a dependency version. --fail-on=patchable narrows that further to vulnerabilities Snyk can resolve via its own patch mechanism. --fail-on=all, the default when the flag is omitted, fails the build on any vulnerability that meets the severity threshold, fixable or not.
This is the mechanism that lets a team say, in effect: "block merges for critical vulnerabilities we can actually do something about right now, but don't block on critical vulnerabilities that have no upstream fix yet." A command like snyk test --severity-threshold=critical --fail-on=upgradable will scan the full dependency tree, keep only critical findings, and then fail the build only if one of those criticals has an available upgrade path — a critical vulnerability with no fixed version yet published will still show up in the report but will not stop the pipeline.
What exit codes does the CLI return, and how does CI read them?
Snyk CLI communicates pass/fail state to CI systems purely through process exit codes, which is why the flags matter mechanically and not just cosmetically. A snyk test invocation returns exit code 0 when no vulnerabilities meeting the active severity threshold and fail-on criteria are found, 1 when such vulnerabilities are found, 2 when the CLI itself failed to run the test (bad arguments, network failure, unsupported manifest), and 3 when no supported projects were detected in the scanned path. Most CI runners (GitHub Actions, GitLab CI, Jenkins, CircleCI) treat any non-zero exit code from a shell step as a failed step by default, which is what actually halts the pipeline — Snyk itself doesn't "stop" anything, it just returns a number and the CI system's own fail-on-nonzero-exit convention does the rest.
This distinction matters operationally: if a pipeline script wraps snyk test in a way that swallows the exit code — piping to a logger, using it inside an if block without re-raising, or running it with continue-on-error set — the scan still runs and still reports findings, but the exit code never reaches the step that would fail the job. The gate exists only as long as the exit code is allowed to propagate.
What happens when both flags are combined in a real pipeline?
Combining them creates a two-stage filter: severity threshold narrows the population of findings, then fail-on decides which of those remaining findings are failure-worthy. For example, snyk test --severity-threshold=medium --fail-on=all reports every medium, high, and critical vulnerability and fails on any of them regardless of fix availability — a strict posture typically used on release branches. By contrast, snyk test --severity-threshold=high --fail-on=upgradable reports only high and critical findings and fails only when an upgrade exists, a common posture for feature-branch or pull-request checks where teams want visibility without blocking on unfixable findings.
Order of evaluation matters for how teams reason about false negatives: a critical vulnerability with a CVSS score of 9.8 but no available upgrade will never fail a build gated with --fail-on=upgradable, no matter how the severity threshold is set. It will appear in the JSON or SARIF output (via --json or --sarif flags) and can still be surfaced in dashboards, but it will not, by itself, stop a merge. Teams that assume "critical always blocks" without checking the fail-on value in their pipeline config are often surprised to find unfixable criticals shipping to production while the pipeline shows green.
Where do teams typically misconfigure this gate?
The most common failure mode is setting --severity-threshold without ever setting --fail-on, which defaults to all and means every reported finding — fixable or not — blocks the build; teams then either drown in unfixable-vulnerability tickets or start disabling the check entirely in frustration. The second most common mistake is the reverse: running with --fail-on=upgradable on a production release gate under the assumption that it's a strict setting, when in practice it silently permits unfixable critical and high vulnerabilities to merge. A third, more subtle issue is inconsistent flag values across pipeline stages — a lenient PR check using --severity-threshold=high --fail-on=upgradable and a stricter main-branch check using --severity-threshold=low --fail-on=all — which means a vulnerability can pass every PR gate a developer sees and only fail once merged, producing confusing after-the-fact pipeline failures with no clear owner.
Because both flags are set per-invocation on the command line (or in a .snyk policy file, which can also set ignore rules with expiry dates), drift between repositories is easy: one service team sets --severity-threshold=critical, another leaves it at the default low, and there is no organization-wide view of which repos are actually gated at what bar unless someone audits every CI config file by hand.
How Safeguard Helps
Snyk's CLI flags do exactly what they're documented to do — they just do it one repository, one pipeline invocation, at a time, with no built-in way to see whether the severity and fail-on posture set in service-a's GitHub Actions file matches the posture in service-b's Jenkinsfile, or whether either one actually matches the organization's stated risk policy. Safeguard is built to sit above that layer: it ingests findings and policy configuration from software composition analysis and SAST/SCA tools across a fleet of repositories and normalizes them into a single view of what's actually gating merges versus what's merely reported.
Concretely, that means Safeguard can flag repositories where --fail-on=upgradable (or an equivalent lenient gate) is quietly permitting unfixable critical vulnerabilities to reach main, surface pipelines where CLI exit codes are being swallowed by shell wrappers or continue-on-error settings before they ever reach the CI runner's pass/fail logic, and track policy drift between branches or services so a security team isn't relying on someone manually diffing YAML across dozens of repos to know their actual enforcement posture. For teams already relying on Snyk CLI as their scanning engine, Safeguard doesn't replace that gate — it verifies the gate is configured the way the team believes it is, everywhere it's supposed to run, and alerts when it isn't.