To add Safeguard to GitHub Actions, you create a Safeguard secret key with the scans:trigger and scans:read scopes, store it as the repository secret SAFEGUARD_API_KEY, and add one workflow step that installs the Safeguard CLI and runs safeguard ci scan --wait --fail-on critical. Each push or pull request becomes a new version of its Safeguard project, with an SBOM and findings, and the job fails when the scan finds a critical dependency vulnerability or, with --fail-on-gate, when a policy gate fails.
There is no Safeguard GitHub Action to install. The whole integration is a plain run: step, which is why it is short to set up and easy to audit. This post walks through the documented workflow file line by line, how to cover pull requests and release branches, how to move from a severity threshold to a real policy gate with Block, Warn and Notify, and what the integration does not do.
A note on the snippets: every command, flag and YAML block below is copied from Safeguard's docs at docs.safeguard.sh as published on 25 October 2026. We did not run them for this post. If safeguard ci scan --help in the build you install disagrees with this page, the help text is right.
If you have not connected GitHub to Safeguard yet, read Connect GitHub to Safeguard: repositories, PR checks and fix PRs first. That post covers the token, the permissions and the pull request features on the connection itself. This one stays on the pipeline config.
Key takeaways
- One step, no marketplace action. Safeguard's CI integration is the Safeguard CLI running
safeguard ci scanin a shell step. Safeguard's docs say there is no plugin, Action or Pipe to install. - A key and a secret. Create a secret key under Settings › Developer with
scans:triggerandscans:read, then store it in GitHub asSAFEGUARD_API_KEY(repository or organisation secret). - No checkout by default. In the default mode Safeguard clones the repository itself and runs SAST, SBOM generation and dependency vulnerability scanning on its side. Add
actions/checkoutand--localto scan on the runner instead, where only results leave the machine. - Two ways to fail a build.
--fail-on <severity>fails on a dependency vulnerability at or above a severity.--fail-on-gate <gate>fails when a Safeguard policy gate is not Passed or Warning. Both need--wait, and they can be combined. - Gates are evaluated in the platform, not in the CLI. Policies, gates and their Block, Warn and Notify actions live in Safeguard. Policy outcomes are switched on per workspace by Safeguard, so check yours shows an Outcome column before you rely on
--fail-on-gate. - SAST is scanned but does not fail the build yet. Safeguard's CI docs say SAST findings are shown in Safeguard but are not part of
--fail-onyet. - Releases are branches in the documented setup. The docs show branch filters with wildcards such as
'release/**'. Tag and release event triggers are not documented forci scan, so we do not show them. - Everything described in this post is available today: the Safeguard CLI, SBOM generation, SCA, SAST, and policies and gates.
Why a pipeline scan still matters
Most teams already have some form of dependency scanning somewhere. The gap is usually not the scanner. It is where the scanner sits and whether anything stops a bad change.
A scan that runs nightly on the default branch tells you about a vulnerable dependency after it merged. A scan that runs in a developer's editor depends on that developer having the extension installed and reading its output. A scan in CI, on every pull request, is the one place every change passes through on its way to the main branch and to a release. That is where a gate belongs.
The standards say the same thing in their own words.
NIST SP 800-218, the Secure Software Development Framework (SSDF) version 1.1, has tasks that map directly onto a pipeline scan. PW.7.2 asks teams to perform code review or code analysis and to "record and triage all discovered issues" in the team's workflow or issue tracker. PW.8.2 covers scoping, designing and performing testing of executable code and documenting the results. PS.3.2 asks producers to collect, safeguard, maintain and share provenance data for all components of each software release, and gives a software bill of materials (SBOM) as the example. RV.1.1 asks teams to gather information on potential vulnerabilities in the software and its third-party components and investigate credible reports.
None of those tasks says "use GitHub Actions". What they describe is a repeatable process that runs on every change, keeps its results, and feeds a triage workflow. A CI step that produces an SBOM per version, records findings against that version and fails the build on a policy is one practical way to show you do PW.7.2, PW.8.2 and PS.3.2 for every release, instead of on the days someone remembers.
The OWASP Top 10 CI/CD Security Risks project is the other useful frame. It lists CICD-SEC-1 Insufficient Flow Control Mechanisms, CICD-SEC-3 Dependency Chain Abuse and CICD-SEC-4 Poisoned Pipeline Execution among its risks. A policy gate on the pull request is a flow control: code with a blocked dependency does not reach the protected branch without someone deciding it should. The same list is also a reminder that the scanning step is itself pipeline code, with access to a secret, and needs the same care as any other step. We come back to that in the hardening section.
Prioritisation standards matter here too. FIRST's Exploit Prediction Scoring System (EPSS) estimates the probability that a published CVE will be exploited in the wild in the next 30 days, published daily for every CVE. CISA describes its Known Exploited Vulnerabilities catalog as "the authoritative source of vulnerabilities that have been exploited in the wild." A gate that fails on every medium finding will be bypassed within a week. A gate tuned to what is likely to be exploited, or already is, tends to survive. Prioritise vulnerabilities with reachability, EPSS and KEV in Safeguard covers how Safeguard ranks findings; this post covers how to turn a policy into a build result.
What the integration actually is
Before the steps, a clear picture of the moving parts, because it explains every option later.
The Safeguard CLI. A single static binary for macOS, Linux and Windows. Safeguard's docs describe it as needing no runtime alongside it: no Node, no Python, no JVM. In CI it is installed with a one-line installer.
safeguard ci scan. The subcommand built for pipelines. Safeguard's CI/CD Integration page puts it simply: one step in your pipeline runs safeguard ci scan. It authenticates with a secret key, works out which repository, branch and commit it is running for, and asks Safeguard to scan that commit.
A project and its versions. Each push becomes a new version of its Safeguard project, named <branch>-<commit> (the docs give main-a1b2c3d as an example). The first run creates the project, so there is nothing to create in the app beforehand.
Where the scan runs. By default the scan runs on Safeguard: Safeguard clones the repository (which must be connected as an integration, or public on github.com, gitlab.com or bitbucket.org) and runs SAST, SBOM generation and dependency vulnerability scanning. With --local, the scan runs on the GitHub runner against a checkout in the job, with no integration needed, and produces the SBOM and dependency vulnerabilities. Only results leave the runner.
The gate. Policies and gates are evaluated by the Safeguard platform, not by the CLI. The CLI's job in a gate is to wait for that evaluation and turn the outcome into an exit code that GitHub understands.
The exit code. GitHub marks a step as failed when its command exits non-zero. Everything --wait, --fail-on and --fail-on-gate do comes down to choosing when safeguard ci scan exits non-zero.
That is the whole integration. No webhook to configure in the workflow, no marketplace action, no container to pull. If you want a broader view of how Safeguard reaches your code (repository connection, CLI or local runner), read Repo connection, CLI or local runner: choosing how Safeguard touches your code.
Step by step: the documented workflow
Step 1. Create the secret key in Safeguard
In Safeguard, go to Settings › Developer and create a secret key with two scopes:
scans:trigger, so the key can start a scanscans:read, so the key can read the scan's result and the gate outcome
The CI/CD Integration page adds that you can lock the key to one project. Do that for anything beyond a trial. A key locked to a project cannot be used to trigger or read scans for any other project, which limits what a leaked key is worth. If the key is locked to one project and the workflow resolves to another, the scan stops with PROJECT_NOT_ALLOWED, which is the behaviour you want.
Step 2. Store it in GitHub as SAFEGUARD_API_KEY
In the GitHub repository, open Settings › Secrets and variables › Actions › New repository secret and add the key with the name SAFEGUARD_API_KEY. Safeguard's GitHub Actions guide notes that an organisation secret works too.
The secret name matters only because the workflow refers to it. If you use a different name, change the env: line in step 3 to match. The CLI reads the value from the SAFEGUARD_API_KEY environment variable of the step.
Two GitHub behaviours to know before you go further, both from GitHub's own documentation on using secrets in Actions:
- With the exception of
GITHUB_TOKEN, secrets are not passed to the runner when a workflow is triggered from a forked repository. - Secrets are not available to workflows triggered by Dependabot events.
Both affect pull requests, and both are covered in the troubleshooting section below.
Step 3. Add the workflow file
Save this as .github/workflows/safeguard.yml. It is the workflow from Safeguard's GitHub Actions guide, unchanged:
name: Safeguard
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Safeguard scan
env:
SAFEGUARD_API_KEY: ${{ secrets.SAFEGUARD_API_KEY }}
SAFEGUARD_INSTALL_DIR: ${{ runner.temp }}/safeguard-bin
run: |
set -eu
curl -fsSL https://cli.safeguard.sh/install | bash
"$SAFEGUARD_INSTALL_DIR/safeguard" ci scan --wait --fail-on critical
Line by line:
on.push.branches: [main]scans every push tomain.on.pull_request.branches: [main]scans every pull request whose target ismain. On a pull request the head branch is scanned, and the version is named after its head commit.runs-on: ubuntu-latestis a GitHub-hosted Linux runner. The CI docs say every system runs the CLI in a plain shell step on a Linux or macOS runner withbashandcurl.SAFEGUARD_API_KEYpasses the secret into the step's environment, where the CLI reads it.SAFEGUARD_INSTALL_DIRtells the installer to put the binary in the runner's temporary directory instead of a system path. The installation docs list this as an installer variable; without it the installer picks its own location.set -eumakes the shell stop on the first failing command and on unset variables, so an install failure cannot fall through to a "passing" step.- The install line downloads and runs the installer. Safeguard's installation docs say it verifies the download against the release's published checksums before installing.
ci scan --wait --fail-on criticalasks Safeguard to scan this commit, waits for it to finish, and exits non-zero if a critical dependency vulnerability is found.
The guide is explicit that no checkout step is needed in this mode: Safeguard clones the repository. Push the file, and the run shows the scan's result and a link to it in Safeguard.
Step 4. Choose the branches, including release branches
The two branches: lists are the only place you choose coverage.
on.push.brancheslists the branches whose pushes are scanned.on.pull_request.brancheslists the target branches of pull requests that are scanned.- Delete the
pull_requestblock to scan pushes only.
Wildcards work. The guide's example is [main, 'release/**'], which covers main and every branch under release/. That is the documented way to scan release builds: if your team cuts releases from branches such as release/2.4, every push to those branches becomes its own Safeguard version, named after the branch and commit, with its own SBOM.
To cover release branches as well as main, put the guide's wildcard example, [main, 'release/**'], in place of [main] in both the push and the pull_request lists. The rest of the file stays as in step 3.
What about tags and GitHub releases? GitHub's events documentation lists release among the events that can trigger a workflow, and push filters can match tags. Safeguard's GitHub Actions guide does not document either for ci scan, and it does not say how a version would be named when there is no branch. We are not going to guess. If you ship from tags, the conservative pattern that the docs do support is to scan the branch or commit the tag points at before you tag it, and to make that scan a required check (step 7). If you need tag triggers, ask Safeguard support how ci scan names those versions before relying on it.
Step 5. Pick how the build fails
The default line fails on critical dependency vulnerabilities. The guide's options table lists the documented alternatives:
| Change | How |
|---|---|
| Report only | Drop --fail-on critical |
| Another threshold | --fail-on high (or medium, low) |
| Fail on a policy gate | Add --fail-on-gate all |
| Scan on the runner | Add - uses: actions/checkout@v4 before the step and --local to the command |
| A specific project | Add --project-id <id> |
| Pin the CLI version | Add SAFEGUARD_VERSION: "<version>" to env |
And the CI/CD Integration page defines exactly when each flag fails the step:
| Flag | Fails the step when |
|---|---|
--wait | the scan fails or takes longer than --timeout (default 30m) |
--fail-on <severity> | with --wait: a dependency vulnerability at or above critical, high, medium or low |
--fail-on-gate <gate> | with --wait: a policy gate (its name, id, or all) is not Passed or Warning |
A few consequences worth spelling out.
Without --wait, nothing can fail on findings. The step triggers the scan and moves on. That is useful if you want scans recorded for every push without holding the pipeline, but it is not a gate.
--fail-on counts dependency vulnerabilities only. The CI docs say SAST findings are scanned and shown in Safeguard but are not part of --fail-on yet. If you want code findings to block a merge today, that has to come from a policy gate that covers them, or from review.
--fail-on-gate is the policy route. Instead of a single severity threshold baked into YAML, the gate's linked policies decide. That keeps the pipeline file stable while the security team tunes policy in one place.
They combine. The policy outcomes page says --fail-on <severity> keeps working as before and can be combined with --fail-on-gate. A common shape is --fail-on critical as a floor that cannot be relaxed by a policy edit, plus --fail-on-gate for the organisation's actual rules.
Step 6. Turn on a real policy gate
--fail-on-gate needs something to evaluate. In Safeguard, policies and gates have distinct jobs, per the Policies & Gates docs:
- Policies are rules that evaluate your SBOMs and generate findings: acceptable risk thresholds, required controls.
- Gates are checkpoints that enforce policies. The docs say they block deployments when policies fail, integrate with CI/CD pipelines, and require approvals for exceptions.
Create a policy. The docs describe a policy builder: open Policies, click Create Policy, define conditions in the visual builder or YAML, set severity and actions, then save and enable. The documented condition types include vulnerability severity, CVSS score, EPSS score, license, component age and maintenance. Pre-built templates include No Critical Vulnerabilities, License Compliance and No Abandoned Packages. If you would rather write the rule in a sentence, Plain English security policies to release gates in Safeguard walks through that route.
Set each policy's Action. Every policy has one Action, which decides what a failed policy does to the gate:
| Action | When the policy fails |
|---|---|
| Block | The gate fails |
| Warn | The gate still passes, with a warning that names the policy |
| Notify | The gate still passes, and Safeguard sends a notification to the workspace's configured channels |
Notify is not noisy by design. The policy outcomes page says you are notified when a policy goes from passing (or never evaluated) to failing, and again when a failing policy finds new matching issues. A policy that keeps failing with the same matches is not notified again.
Link the policies to a gate. A gate with no linked policies is Not evaluated, which --fail-on-gate treats as a failure (below). Link at least one.
Check policy outcomes are on. This is the step people miss. Safeguard's policy outcomes page says outcomes are turned on per workspace by Safeguard. If your Gates and Policies tables do not show an Outcome column yet, ask your Safeguard contact to turn it on. While it is off, policies and gates behave as before, and the gate route in CI is not what you want to depend on.
Point the workflow at the gate. Replace the scan line with the gate flag. Using a name or id targets one gate; all targets every gate:
safeguard ci scan --wait --fail-on-gate <gate name or id | all>
That form is from the policy outcomes page. In the workflow from step 3, it means adding --fail-on-gate all (or a gate name) to the existing ci scan line, as the options table shows.
How the gate outcome becomes an exit code
This is the part worth reading twice, because it decides what your developers see.
A policy's own outcome is one of four values, per the docs:
- Failed: the policy's search found matching issues.
- Passed: the search ran to the end and found no matching issues.
- Error: the policy could not be evaluated, for example because its search is missing, failed or timed out. The reason is shown with it. An Error is never counted as a pass.
- Not evaluated: the policy has not been evaluated on this gate yet.
New policies have no threshold, so any match fails them. A policy created earlier with a threshold above zero keeps it and fails only when matches reach that number.
The gate's outcome follows from its linked policies, in this order:
- No policies linked: Not evaluated.
- Any Block policy failed: Failed.
- Otherwise, any policy could not be evaluated: Error, with the reason.
- Otherwise, any policy not evaluated yet: Not evaluated.
- Otherwise, any Warn policy failed: Warning. The gate passes, and the warning names the policy.
- Otherwise: Passed. Failed Notify policies are listed as notified.
safeguard ci scan --wait --fail-on-gate waits for the scan and for the gate's policies to be evaluated on the new data, then:
- exits 0 when the gate is Passed or Warning, printing warnings and notified policies in the job log;
- exits non-zero when the gate is Failed, and prints the Block policies that failed;
- exits non-zero when the gate is Error or Not evaluated, or its policies were not evaluated before
--timeout, with a message that says why.
So a gate fails closed. A broken policy, an empty gate or a slow evaluation stops the build rather than letting it through quietly. That is the right default for a release branch. It also means a new gate with no policies linked will fail every build until you link one, which is a common first-day surprise.
Step 7. Make the job a required check
A failing job only blocks a merge if GitHub is told to treat it as required. GitHub's protected branches documentation says that after enabling required status checks, all required status checks must pass before collaborators can merge changes into the protected branch.
In the repository, add a branch protection rule (or ruleset) for main and for your release/** pattern, enable required status checks, and select the Safeguard job. The job's check name comes from the workflow and job names in the YAML (Safeguard and scan in the documented file).
Without this, the Safeguard job goes red on a pull request and the merge button still works.
Step 8. Handle exceptions without editing the pipeline
A gate that blocks with no way through will be removed. Safeguard's answer is exceptions, recorded in the platform rather than in YAML. The Policies & Gates docs describe the request flow:
- Click Request Exception.
- Select the policy and finding.
- Provide a business justification.
- Set an expiration date.
- Submit for approval.
The workflow behind it: the request is created by the developer, approvers are notified, an approver reviews and decides, the exception is granted or denied, and an audit trail is recorded.
The point is where the decision lives. Dropping --fail-on-gate from a workflow file to unblock one release leaves no trace of who decided and why, and it unblocks every later run too. An exception with an expiry date is scoped, attributable and temporary. If you need a pattern for a true emergency, decide it with your approvers in advance and route it through the same exception flow; do not build a second path in YAML.
Step 9. Read the result
On each run, the job log shows the scan's result and a link to it in Safeguard. From there you see the version (<branch>-<commit>), its SBOM and its findings. With --fail-on-gate, the log also lists failed Block policies, warnings and notified policies, so a developer can tell from the log alone whether they need to fix something or ask for an exception.
In Safeguard, Policies › Gates and Policies › Policies show the Outcome column with Passed, Failed, Warning, Not evaluated or Error and when each was last evaluated. Expanding a gate shows each linked policy's Action, outcome, effect on the gate and matches, plus the gate's evaluation history page by page.
Because every push is a version with its own SBOM, your release branches build up a record of what shipped in each release. SBOM generation and analysis in Safeguard covers what is in those SBOMs and how they line up with minimum element guidance.
Hosted scan, runner scan or build-machine scan
There are three documented ways to run Safeguard from a GitHub workflow. They produce different things, so choose deliberately.
ci scan (default) | ci scan --local | safeguard scan on the runner | |
|---|---|---|---|
| Where it runs | On Safeguard | On the GitHub runner | On the GitHub runner |
| Needs | Repository connected as an integration, or public on github.com | A checkout in the job; no integration | The CLI plus two open source scanning tools on the PATH (or TOOLS_DIR) |
| Checkout step | Not needed | actions/checkout before the step | Needed |
| What it scans | SAST, SBOM, dependency vulnerabilities | SBOM, dependency vulnerabilities | SBOM matched against vulnerability databases |
| What leaves the runner | Nothing; Safeguard clones the repo | Only results | Nothing unless you upload it |
| Creates a Safeguard version | Yes | Yes | No |
| Fails the build | --fail-on, --fail-on-gate | --fail-on, --fail-on-gate | No; gate on the files it writes |
| Good for | Connected repos on GitHub | Self-hosted or firewalled Git, or no repo access for Safeguard | Keeping raw JSON results as artifacts |
The default: scan on Safeguard
This is the documented workflow above. It is the only mode that includes SAST, and it needs no checkout. It requires that Safeguard can clone the repository: connected through the GitHub integration, or public. If the project has no repository Safeguard can clone, for example a project created by uploading an SBOM, the CLI stops with PROJECT_NOT_RESCANNABLE, and the fix is to connect the repository or use --local.
--local: scan on the runner
Add actions/checkout@v4 before the Safeguard step and --local to the command. The CI docs say that with --local only the results leave the runner, which is why it also works for self-hosted and firewalled Git servers. It reads dependencies from lockfiles and manifests in the checkout; the docs give package-lock.json, go.sum, requirements.txt and pom.xml as examples. For the full list by ecosystem, check the CLI support matrix rather than assuming your lockfile is read.
The trade-off is coverage: the docs list SBOM and dependency vulnerabilities for --local, not SAST. Choose it when policy or data residency rules out Safeguard cloning your code, and accept that code analysis then needs another route. Both modes produce a Safeguard version and both support --fail-on and --fail-on-gate.
safeguard scan: results as files
The CI page also documents a third path, scanning fully on the build machine with safeguard scan, which writes files you can keep as artifacts or upload to a code scanning tab:
safeguard scan --dir . --output out/
jq -e '[.data.matches[].vulnerability.severity | ascii_downcase]
| index("critical") | not' out/*/vuln.json
Here the CLI does not fail on findings. The usage docs say safeguard scan exits 0 when the scan ran, whatever it found, 1 when the command failed, and 2 when results were written but the vulnerability scan did not actually run, so a script never mistakes that for a clean pass. There is no severity threshold flag on safeguard scan. The jq line in the docs is what turns a critical finding into a failing exit code.
Results include vuln.json, license.json and collection.json in a folder per scan. On a fresh runner, the docs say you must put the two open source scanners it uses on the PATH, or point TOOLS_DIR at a directory holding them; the interactive terminal downloads them on first use, but CI containers usually have never opened it. The docs page names both tools.
Use this path when you want the raw JSON in the workflow, for example to archive with GitHub's artifact storage. It does not create a Safeguard version and does not evaluate a policy gate, so it is a complement to ci scan, not a replacement for it.
Running the step safely
The Safeguard step holds a secret and runs code from the internet. Treat it like any other privileged step.
Pull requests from forks. Safeguard's troubleshooting note is direct: pull requests from forks fail, because GitHub gives fork pull requests no secrets. You can scan pushes only, or use pull_request_target with care. The guide warns that pull_request_target runs with your secrets, so never run the fork's code in that job. For the default mode that is less risky than it sounds, because there is no checkout and Safeguard clones the repository itself. For --local, which needs a checkout, combining it with pull_request_target would put a fork's files next to your secret. Do not do that. If you maintain a public repository with outside contributors, scanning pushes after review is the conservative choice.
Dependabot pull requests. GitHub's docs say secrets are not available to workflows triggered by Dependabot events, which is the same failure in a different form. Plan for those pull requests to be scanned after they land on a branch you push to, or follow GitHub's own guidance for Dependabot secrets.
Pin the CLI. The installer resolves the current version unless told otherwise. Add SAFEGUARD_VERSION: "<version>" to the step's env: so a new release does not change a build under you. The CI page gives the same advice for other systems. Update the pin on purpose, the way you would update any other dependency.
The installer pipe. The documented install line pipes a download into bash. Safeguard's installation docs say the installer verifies the binary against the release's published checksums.txt. If your own review rules forbid piping a download into a shell, the installation docs list the per-platform release archives and the per-release checksums file, so you can download, verify and run as separate steps. Write that version yourself from the installation page; we are not adding an unpublished snippet here.
Least privilege for the job token. GitHub's security hardening guide recommends granting GITHUB_TOKEN the minimum required permissions and setting the default to read access for repository contents. The documented Safeguard step does not use GITHUB_TOKEN at all, so it needs nothing beyond the read default. Set the default in your repository or organisation settings.
Pin third-party actions. The --local option adds actions/checkout@v4. GitHub's hardening guide recommends workflow dependency lockfiles where a repository can adopt them, and pinning actions to a full-length commit SHA where it cannot. Apply that to this line as you would anywhere else. Are third-party GitHub Actions a supply chain entry point? covers why.
Scope the key. Lock the Safeguard key to the project, as in step 1, and rotate it if it might have been exposed. A revoked or expired key shows up as INVALID_SECRET_KEY.
Monorepos and more than one project
ci scan resolves the Safeguard project from the repository. Two documented situations need a nudge:
PROJECT_AMBIGUOUSorINTEGRATION_AMBIGUOUS. More than one project or integration fits the repository. Pass--project-id(or--integration-idon the first run).- You want a specific project on purpose. The options table's "A specific project" row is
--project-id <id>.
In the app, the GitHub card under Integrations › Source Control has a Set up CI/CD button that opens the setup with your branches and severity filled in, and a project's own Set up CI/CD does the same with that project's id. That is the easiest way to get a correct --project-id without copying ids by hand.
Troubleshooting
The CI/CD Integration page lists the messages you are most likely to see:
| Message | What to do |
|---|---|
AUTHENTICATION_REQUIRED or INVALID_SECRET_KEY | SAFEGUARD_API_KEY is missing, or the key is invalid, expired or revoked. Check the secret's name and that the step can read it. |
MISSING_SCOPE | The key lacks scans:trigger or scans:read. Create one with both. |
PROJECT_NOT_ALLOWED | The key is locked to another project. |
PROJECT_AMBIGUOUS or INTEGRATION_AMBIGUOUS | More than one project or integration fits. Pass --project-id (or --integration-id on the first run). |
PROJECT_NOT_RESCANNABLE | No repository Safeguard can clone. Connect it under Integrations, or use --local. |
SCAN_NOT_DISPATCHED or SCAN_NOT_STARTED | The platform could not start the scan. Retry; if it persists, contact support with the build URL. |
| The step never runs | The branch is not in the branch list, or the pull request comes from a fork and gets no secrets. |
Two more that are not error codes:
- The job fails with a gate message but nothing looks wrong. Read whether the gate was Failed, Error or Not evaluated. Error and Not evaluated fail the build on purpose; the usual causes are a gate with no policies linked, a policy whose saved search is missing, or evaluation not finishing before
--timeout. - The job times out.
--waitfails when the scan takes longer than--timeout, which defaults to30m. Large repositories on their first scan are the usual case.
Requirement, what the step does, what you still own
| Requirement or goal | What the Safeguard step does | What you still own |
|---|---|---|
| SSDF PS.3.2: provenance data per release, for example an SBOM | Each push to a scanned branch becomes a version with an SBOM in Safeguard | Choosing which branches are release branches, and sharing SBOMs with customers |
| SSDF PW.7.2 and PW.8.2: analyse and test, record and triage issues | Records findings against each version; default mode includes SAST | Triage, fixing, and deciding what blocks |
| SSDF RV.1.1: gather and investigate vulnerability information | Matches components against vulnerability data on every scan | Reading advisories that are not yet in any database, and vendor software you do not build |
| OWASP CICD-SEC-1: flow control | Fails the job on severity or a gate | Making the job a required check under branch protection |
| Prioritise likely exploitation (EPSS, KEV) | Policy conditions include EPSS score; Safeguard shows EPSS and KEV data when you prioritise findings | Tuning thresholds so the gate is strict without being ignored |
| Auditable exceptions | Exceptions with justification, expiry, approval and audit trail | Who approves, and how fast |
| Pipeline hardening (OWASP CICD-SEC-4) | Runs with one scoped secret and no GITHUB_TOKEN use | Fork and Dependabot policy, action pinning, installer review |
What Safeguard does not do
Honest limits, so you can plan around them.
- There is no Safeguard GitHub Action. Safeguard's docs say there is no plugin, Action or Pipe to install. Anything presented as an official Safeguard action in the marketplace is not documented by Safeguard, and the integration is the shell step above.
- SAST does not fail the build yet. SAST runs in the default mode and its findings show in Safeguard, but the CI docs say they are not part of
--fail-onyet. In--localmode there is no SAST at all. - Release tags and GitHub release events are not documented for
ci scan. The guide covers branch filters, with'release/**'as its wildcard example. If you need tag-triggered scans, confirm version naming with Safeguard first. - The CLI does not evaluate policies. Gates are evaluated in the platform. If policy outcomes are not turned on for your workspace,
--fail-on-gateis not something to build on until Safeguard enables them. - No KEV gate condition is documented. The policy docs list severity, CVSS, EPSS, license, component age and maintenance conditions. Use KEV to prioritise in Safeguard, not as a documented gate rule, unless your workspace shows otherwise.
- The documented CI step does not sign or attest anything. It creates a version with an SBOM and findings. Signing release artifacts is a separate step in your release process.
- Fork pull requests and Dependabot runs get no secrets. That is a GitHub rule, not a Safeguard one, and it means those pull requests are not scanned by this workflow unless you change triggers with care.
- Pipeline file review is not part of this integration. Safeguard's CI/CD Security docs page describes checks on workflow files, but says that feature is not offered in the app at the moment. Hardening your workflows is still your job.
- It does not fix anything by itself in CI. Fix pull requests come from the platform side through autonomous remediation, which ships switched off with every category set to Manual. See Autonomous remediation in Safeguard.
Common mistakes
Forgetting --wait. Without it, --fail-on and --fail-on-gate cannot fail anything, because the step does not wait for a result. A green build with no gate is the most common way a team believes it has a gate it does not have.
Not making the job required. A red Safeguard job that is not a required status check is advice, not a gate. Add it to branch protection for main and your release branch pattern.
Linking no policies to the gate. A gate with no linked policies is Not evaluated, and --fail-on-gate fails on Not evaluated. The first build after adding the flag fails for a reason that has nothing to do with your code.
Starting with --fail-on low. It feels thorough and it trains everyone to ignore the job. Start at critical, watch for a few weeks, then move to a policy gate tuned to exploitation signals.
Relying on SAST in --fail-on. It is not counted yet. If a code finding must block, put it in a policy and gate on it, or require review.
Using pull_request_target with --local. That pairs a fork's checkout with your secret. Keep --local on push and pull_request.
Leaving the CLI unpinned on release branches. A release branch that rebuilds weeks later should run the same CLI. Pin SAFEGUARD_VERSION and update it on purpose.
Editing YAML to get past a block. Removing the gate flag to ship one release leaves no record and stays removed. Use an exception with an expiry date.
Copying flags from another command. safeguard scan and safeguard ci scan take different flags and behave differently on findings. --fail-on belongs to ci scan; safeguard scan has no severity threshold.
Scanning a project Safeguard cannot clone. Projects created from uploaded SBOMs cannot be rescanned by the default mode. Connect the repository or use --local.
How this fits with the rest of your pipeline
The CI step is one point in a longer chain. Scan depth choices for each stage are covered in Fast, deep or progressive scans: which depth for each pipeline stage; note that Safeguard's GitHub Actions guide does not document a scan depth flag for ci scan, so treat that post as guidance for the CLI's own scans. Container images your workflow builds are a separate target with their own per-environment setup, covered in Track container images per environment in Safeguard.
A sensible order for a team adding Safeguard to GitHub Actions for the first time:
- Add the documented workflow on
mainwith--fail-on critical, as a non-required check, for a week. - Make it required on
main. - Write two or three policies (critical vulnerabilities, license, abandoned packages), link them to a gate with Block on the first and Warn on the others, and confirm policy outcomes are on.
- Add
--fail-on-gatealongside--fail-on critical. - Extend the branch lists to
'release/**'and require the check there too. - Pin
SAFEGUARD_VERSION.
Get the CLI from https://safeguard.sh/download and keep the full reference open at https://docs.safeguard.sh/docs while you set it up.
FAQ
Is there a Safeguard GitHub Action in the marketplace?
No. Safeguard's CI/CD docs say there is no plugin, Action or Pipe to install. The integration is a shell step that installs the CLI and runs safeguard ci scan.
What permissions does the Safeguard API key need for GitHub Actions?
A secret key created under Settings › Developer with the scopes scans:trigger and scans:read. You can lock it to one project. Without both scopes the scan stops with MISSING_SCOPE.
Do I need actions/checkout before the Safeguard step?
Not in the default mode, because Safeguard clones the repository itself. You need actions/checkout only when you add --local to scan on the runner.
How do I fail a pull request on high severity vulnerabilities?
Change the scan line to --fail-on high and keep --wait. Then make the job a required status check in branch protection so the failure blocks the merge.
How do I fail the build on a Safeguard policy gate?
Add --fail-on-gate all, or a gate's name or id, to the ci scan line with --wait. The job exits non-zero when the gate is Failed, Error or Not evaluated, and passes on Passed or Warning. Policy outcomes must be on for your workspace.
Does Safeguard scan pull requests from forks?
Not with the documented workflow, because GitHub does not pass secrets to fork pull requests. Scan pushes after review, or use pull_request_target with care and never run the fork's code in that job.
Can I scan release branches and tags?
Release branches yes, with wildcards such as 'release/**' in the branch lists. Tag and release event triggers are not documented for ci scan, so confirm with Safeguard before relying on them.
Do SAST findings fail the GitHub Actions build?
Not yet. The CI docs say SAST findings are scanned and shown in Safeguard but are not part of --fail-on. SAST runs only in the default mode, not with --local.
Does my source code leave GitHub when I use Safeguard in CI?
In the default mode Safeguard clones the repository to scan it. With --local, the scan runs on your runner and only results leave it, which is why that mode suits self-hosted or firewalled Git servers.
How do I stop a new CLI release from changing my builds?
Add SAFEGUARD_VERSION: "<version>" to the step's env:. The installer then installs that release instead of the newest one.
Whichever gate you settle on, the next question is what to do about the findings it blocks. The block below is Safeguard's standard answer for that, and it applies to every dependency your workflow scans.
Fix it without waiting for upstream
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation categories include dependencies, licenses, security posture and findings, and each one is set separately. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds.
3. Gold Registry: start from components that are already fixed
Gold Registry provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs and a completed license check, with signed build provenance (signed with Safeguard's key and verifiable with cosign, SLSA Build Level 3 as the design target) and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. Ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | Safeguard policies and gates in CI |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
The first demo is a real Safeguard CLI session: it installs the CLI, opens the interactive terminal, and asks it to fix a lodash CVE in a repository. It reads the finding and advisory, asks before any command that changes something, edits package.json to lodash 4.17.21, runs npm install and npm test with approval, prints a summary and pushes nothing, then shows /permissions, /model and /scan. It is the same CLI your workflow installs, used on a developer's machine to fix what the gate blocked.
Click through it at your own pace, or open it full screen.
The second demo sets up the local runner on your own machine, approves it with its device code, then scans from the terminal. It is the interactive counterpart of --local in CI: the scan runs where your code is, and only results go to Safeguard.
Click through it at your own pace, or open it full screen.
Quick check
Your workflow runs safeguard ci scan --fail-on critical without --wait. Will a critical finding fail the job?
No. The severity and gate flags only fail the step together with --wait. Without it, the step triggers the scan and does not wait for the result.
A gate has no policies linked and the workflow uses --fail-on-gate all. What happens?
The build fails. A gate with no linked policies is Not evaluated, and ci scan exits non-zero on Not evaluated as well as on Failed and Error. Link at least one policy.
Sources
- Safeguard docs: GitHub Actions
- Safeguard docs: CI/CD Integration
- Safeguard docs: Safeguard CLI
- Safeguard docs: CLI Usage
- Safeguard docs: CLI Installation
- Safeguard docs: CLI Support Matrix
- Safeguard docs: Policies & Gates
- Safeguard docs: Policy outcomes and actions
- Safeguard docs: CI/CD Security
- Safeguard docs: Generate SBOM
- NIST SP 800-218, Secure Software Development Framework v1.1
- NIST SP 800-218 full text (PDF)
- OWASP Top 10 CI/CD Security Risks
- FIRST: Exploit Prediction Scoring System (EPSS)
- CISA: Known Exploited Vulnerabilities Catalog
- CISA: Software Bill of Materials
- GitHub Docs: Using secrets in GitHub Actions
- GitHub Docs: Security hardening for GitHub Actions
- GitHub Docs: About protected branches
- GitHub Docs: Events that trigger workflows
Last reviewed: 25 October 2026. Product capabilities change; check docs.safeguard.sh for the current state.