To use the Safeguard CLI in CI/CD, install the single binary in a pipeline step, run safeguard scan --dir . to write JSON results under ./output, check vuln.json with jq or let safeguard ci scan fail the build on the platform, and use safeguard exec for one non-interactive assistant turn, such as a summary of critical findings.
You can make this switch without giving up the AI plan you already have. Bring the Claude, ChatGPT or Codex plan you already pay for and use it inside the Safeguard CLI. Safeguard adds no extra charge for that usage; your plan's own usage limits and terms still apply. And if your team lives in a chat app, connect Claude or ChatGPT to the Safeguard MCP server and ask about the same findings from the chat you already use.
This guide is for the person who owns the pipeline: which command fits which job, a setup that works on any CI system with bash and curl, examples for GitHub Actions and GitLab CI, jq recipes for the JSON the scan writes, and the common mistakes. Every flag comes from the Safeguard docs linked at the end.
Four commands, four jobs in a pipeline
The Safeguard CLI gives a pipeline four useful entry points. They do different things, and most teams end up using two or three of them.
| Command | Where the work happens | Needs a checkout | Credentials in CI | What you get back | Fails the build when |
|---|---|---|---|---|---|
safeguard ci scan --wait --fail-on critical | On Safeguard: the platform clones the repo, scans the push as a new version of its project and evaluates it | No | SAFEGUARD_API_KEY secret with scans:trigger and scans:read | Results in the platform | Findings reach the --fail-on severity |
safeguard scan --dir . --output out/ | On the build machine, using syft and grype | Yes | None needed for a local folder; --git-token or registry credentials for private targets | SBOM plus vuln.json, license.json, collection.json and others, in a folder per scan | The command fails (exit 1) or the vulnerability scan did not run (exit 2), if you check for it |
safeguard validate | On the build machine | Yes, or a built SBOM artifact | None | A validation result, and a report file with -o | Depends on how you check the result |
safeguard exec "..." | One assistant turn started from the build machine | Yes, it refuses to run outside a git repository unless you pass --skip-git-repo-check | See the note on sign-in below | Text, or machine-readable output with --output-format json | You decide, by reading the output |
safeguard scan has no severity threshold flag. Exit code 0 means the scan ran, whatever it found. Exit code 1 means the command failed, for example a bad flag or an unreadable target. Exit code 2 means results were written but the vulnerability scan did not run. To fail a local scan on a critical finding, you read vuln.json yourself, as the Safeguard CI docs show with jq.
Policies and gates are evaluated in the platform, not by the CLI. When you want the organisation's policy to decide whether a build passes, use safeguard ci scan, or connect the repository as an integration so the platform scans it with nothing running in the pipeline.
Safeguard does not publish a GitHub Action, Bitbucket Pipe or CI plugin. Every system runs the CLI as a plain shell step, and the docs include examples for GitHub Actions, GitLab CI/CD, Bitbucket Pipelines, Jenkins and Azure Pipelines.
Do the work properly with the Safeguard CLI
The Safeguard CLI scans code, container images and SBOMs on your own machine, sends only the results to the platform, and includes an assistant that can fix what it finds. The same binary you run at your desk is the one you install in the pipeline. That matters: when a build fails, you can reproduce the exact scan locally, ask the assistant about it, and fix it before you push again.
Install
On macOS and Linux:
curl -fsSL https://cli.safeguard.sh/install | bash
On Windows, in PowerShell:
irm https://cli.safeguard.sh/install.ps1 | iex
It is a single static binary, so there is no Node, Python or JVM to install first. It runs on macOS, Linux and Windows, on x86_64 or arm64, and Windows is x86_64 only. The download page lists version 1.4.0 with builds for macOS Apple Silicon and Intel, Linux x86_64 and arm64, and Windows x86_64.
First session on your own machine
Run it once by hand before you wire anything into CI:
cd ~/projects/payments-api
safeguard
With no arguments, safeguard opens the interactive terminal. Type /login to sign in through the browser, then /scan . to scan the current folder. You can also ask in plain language, for example "how many critical vulns do we have". This first run also downloads the scanner toolchain, which tells you early whether your network or proxy will get in the way later on a build agent.
Writing and fixing code
The Safeguard CLI writes code, not only scans it. The assistant has tools for your local files and for your Safeguard data, and it picks whichever the question needs. Ask "fix the lodash CVE in this repo" and it can edit the manifest, update the lockfile and run your tests, with your approval. That is the fastest way to clear a red build: reproduce it locally, let the assistant propose the change, read the diff, approve it, and push.
This walkthrough shows that loop from the platform side: attach a computer running the Safeguard CLI and a folder, ask for a fix, review the diff and approve the command before it runs. The demo itself notes that computer work from Search is available and expanding.
Click through it at your own pace, or open it full screen.
Keep your Claude, ChatGPT or Codex plan
Bring the Claude, ChatGPT or Codex plan you already pay for and use it inside the Safeguard CLI. Safeguard adds no extra charge for that usage; your plan's own usage limits and terms still apply. /model shows or switches the model, and the CLI docs cover connecting one. You can also connect Claude or ChatGPT to the Safeguard MCP server and use Safeguard from the chat you already use.
Model choice is a tenant decision as well as a personal one. This walkthrough shows choosing the model for each conversation: Zero z20 answers with no language model at all by turning the question into a platform query, Griffin, Eagle and Lion are Safeguard's models, a tenant admin chooses which model families people may use, and you can bring your own model on your own provider account and route jobs such as chat or summaries to it.
Click through it at your own pace, or open it full screen.
Approvals
In the interactive terminal, tools that write a file, edit code or run a command ask before they act. /permissions sets the mode: Ask prompts before every action, Auto approves known actions and asks for anything new (the default), and Yolo approves everything. The current mode is shown in the bar under the input.
CI with safeguard exec
safeguard exec runs one non-interactive assistant turn and exits:
safeguard exec "summarise the critical findings in this repository"
There is nobody at a CI runner to answer a prompt, so exec uses a different default. Its permission mode is strict: nothing is asked, and anything the policy file has not pre-approved is denied. That makes it a good fit for read and report work in a pipeline, and a poor fit for unattended code changes, which is the right way round. Code changes belong in the interactive session, where you approve them.
One platform beyond the pipeline
The download page lists the other surfaces: a desktop app (3.2.2) for Windows, macOS and Linux, iOS and Android apps, IDE and browser extensions, and cloud options such as the Cloud CLI and a cloud sandbox, free for seven days with no card. The build flags a finding, you fix it in the terminal, review it on the desktop, and confirm it cleared from your phone.
Step by step: add a local scan to any pipeline
This is the pattern that works everywhere, because it is only shell. Adapt the paths to your CI system.
1. Install a pinned version
Pin the CLI so a new release does not change your builds without warning. The installer reads SAFEGUARD_VERSION, and SAFEGUARD_INSTALL_DIR sets where the binary goes. Root is only needed if you install into /usr/local/bin.
export SAFEGUARD_INSTALL_DIR="$PWD/.safeguard-bin"
curl -fsSL https://cli.safeguard.sh/install | SAFEGUARD_VERSION=<version> bash
"$SAFEGUARD_INSTALL_DIR/safeguard" --version
Two environment variables keep CI quiet and repeatable. SAFEGUARD_NO_UPDATE_CHECK=1 skips the background check for new releases, and SAFEGUARD_DISABLE_SELF_UPDATE=1 turns off binary updates entirely.
2. Put the scanners where the CLI can find them
safeguard scan runs syft (SBOM generation) and grype (vulnerability matching), both Apache-2.0 projects from Anchore. On a laptop the interactive terminal downloads them on first run; a fresh CI container has never done that, so install both. Their READMEs give these commands:
curl -sSfL https://get.anchore.io/syft | sudo sh -s -- -b /usr/local/bin
curl -sSfL https://get.anchore.io/grype | sudo sh -s -- -b /usr/local/bin
The CLI looks in TOOLS_DIR first, then next to its own binary, then on PATH. If you keep scanners in a cached folder, point TOOLS_DIR at it. safeguard update refreshes the vulnerability databases if you want that as an explicit step.
3. Check out, scan and handle all three exit codes
A local scan reads files, so check out the repository first. (safeguard ci scan clones it on Safeguard and needs no checkout.)
#!/usr/bin/env bash
set -u
"$SAFEGUARD_INSTALL_DIR/safeguard" scan --dir . --output out/
status=$?
case "$status" in
0) echo "Scan ran. Checking results." ;;
2) echo "Results written, but the vulnerability scan did not run." >&2
exit 2 ;;
*) echo "Scan command failed with exit code $status." >&2
exit "$status" ;;
esac
Treat exit code 2 as a failure in CI. A green build that never matched anything against the vulnerability databases is worse than a red one.
4. Gate on vuln.json
vuln.json is grype compatible, with the matches under .data.matches. This is the gate from the Safeguard CI docs. It fails the step if any match has critical severity:
jq -e '[.data.matches[].vulnerability.severity | ascii_downcase] | index("critical") | not' out/*/vuln.json
The -e flag sets jq's exit status from the last value it prints, so the whole expression returns false, and the step fails, as soon as one critical match exists. Make sure jq is installed on the runner.
5. Keep the results
Upload out/ as a build artifact even when the gate fails, so a developer can fix the problem without rerunning anything.
GitHub Actions
The pattern above as a workflow. A local scan of your own repository needs no secret, and the upload step runs on failure too.
name: safeguard-local-scan
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
env:
SAFEGUARD_INSTALL_DIR: ${{ runner.temp }}/safeguard-bin
SAFEGUARD_NO_UPDATE_CHECK: "1"
steps:
- uses: actions/checkout@v7
- name: Install scanners
run: |
curl -sSfL https://get.anchore.io/syft | sudo sh -s -- -b /usr/local/bin
curl -sSfL https://get.anchore.io/grype | sudo sh -s -- -b /usr/local/bin
- name: Install Safeguard CLI
run: curl -fsSL https://cli.safeguard.sh/install | SAFEGUARD_VERSION=<version> bash
- name: Scan
run: '"$SAFEGUARD_INSTALL_DIR/safeguard" scan --dir . --output out/'
- name: Fail on critical
run: |
jq -e '[.data.matches[].vulnerability.severity | ascii_downcase] | index("critical") | not' out/*/vuln.json
- name: Keep results
if: always()
uses: actions/upload-artifact@v7
with:
name: safeguard-results
path: out/
if-no-files-found: error
retention-days: 14
if-no-files-found: error turns a silent empty upload into a visible failure. Artifact names must be unique, so give each job its own.
GitLab CI
The same steps as a GitLab job. when: always keeps the results even when the gate fails, and expire_in controls how long GitLab stores them.
safeguard-scan:
image: ubuntu:24.04
variables:
SAFEGUARD_INSTALL_DIR: "$CI_PROJECT_DIR/.safeguard-bin"
SAFEGUARD_NO_UPDATE_CHECK: "1"
before_script:
- apt-get update && apt-get install -y curl jq
- curl -sSfL https://get.anchore.io/syft | sh -s -- -b /usr/local/bin
- curl -sSfL https://get.anchore.io/grype | sh -s -- -b /usr/local/bin
- curl -fsSL https://cli.safeguard.sh/install | SAFEGUARD_VERSION=<version> bash
script:
- '"$SAFEGUARD_INSTALL_DIR/safeguard" scan --dir . --output out/'
- jq -e '[.data.matches[].vulnerability.severity | ascii_downcase] | index("critical") | not' out/*/vuln.json
artifacts:
when: always
paths:
- out/
expire_in: 1 week
Safeguard also publishes a GitLab job template you can extend as .safeguard-scan; take its current URL from the CI docs.
Let the platform decide instead
When you want your organisation's policies to make the call, swap the local scan for a platform scan. Create a secret key with the scans:trigger and scans:read scopes under Settings, Developer, store it in your CI's secret store as SAFEGUARD_API_KEY, and run:
"$SAFEGUARD_INSTALL_DIR/safeguard" ci scan --wait --fail-on critical
On CI systems the CLI does not recognise, also pass --repository-url, --branch and --commit. Pull requests from forks receive no secrets (GitHub documents this for Actions), so this step fails there. Decide up front whether fork PRs skip the platform scan or fall back to a local one.
Reading the ./output JSON
The scan writes a folder per scan under the output directory. Inside you will find the SBOM plus vuln.json, license.json and collection.json, among others. One practical tip from the troubleshooting guide: JSON goes to stdout and messages go to stderr, so when you capture JSON from a command, redirect stdout alone.
The matches in vuln.json follow grype's shape: each match pairs a vulnerability (with id, severity and a fix object holding versions and state) with the artifact it was found in. Field names can shift between scanner versions, so open one real file before you script against it:
jq '.data.matches[0]' out/*/vuln.json
Count findings by severity
jq '[.data.matches[].vulnerability.severity | ascii_downcase]
| group_by(.)
| map({severity: .[0], count: length})' out/*/vuln.json
This is the one-line summary most people want at the top of a build log.
List critical findings with their fix versions
jq -r '.data.matches[]
| select(.vulnerability.severity | ascii_downcase == "critical")
| [.vulnerability.id, .artifact.name, .artifact.version,
((.vulnerability.fix.versions // []) | join(","))]
| @tsv' out/*/vuln.json
The // [] guard stops jq from failing on a match with no fix versions.
Only what you can fix today
Grype records a fix state of fixed, not-fixed, wont-fix or unknown. Filtering to fixed gives a developer a list where every line has an action:
jq -r '.data.matches[]
| select(.vulnerability.fix.state == "fixed")
| "\(.artifact.name) \(.artifact.version) -> \(.vulnerability.fix.versions | join(", "))"' out/*/vuln.json
One scan, one folder
If one job scans several targets, give each its own --output folder and run one jq check per folder. With several files, -e only looks at the last value printed, so a critical in the first file could slip past.
Worked examples with safeguard exec
safeguard exec brings the assistant into the pipeline for one turn. Because the default mode is strict, it cannot write files or run commands unless your policy file pre-approves them, so these examples stick to reading and summarising.
A finding summary for the build log
"$SAFEGUARD_INSTALL_DIR/safeguard" exec "summarise the critical findings in this repository"
Put it after the scan step so a failed gate comes with a readable explanation.
Machine-readable output for the next step
"$SAFEGUARD_INSTALL_DIR/safeguard" exec --output-format json \
"summarise the critical findings in this repository" > exec-result.json
Redirect stdout alone, since messages go to stderr. Then inspect exec-result.json once by hand before another step depends on its fields. Post it as a pull request comment or a chat message with whatever tooling your CI already uses for that.
Prompts that work well in CI
Keep prompts narrow and read only, such as "which of the critical findings have a fixed version available". Avoid prompts that ask for changes, like "upgrade every vulnerable package". Under strict those actions are denied, and an unattended change is one nobody reviewed. Run that request in the interactive terminal, approve each step, and let CI scan the result.
A note on sign-in
The CLI signs in with an OAuth 2.0 device code flow; safeguard login --no-browser prints a URL and code for machines without a browser. The token stays on the machine that signed in, by default in ~/.scanner/token.json. The docs state that the CLI does not read an API key from the environment; SAFEGUARD_API_KEY is documented for safeguard ci scan. A self-hosted runner you signed in once fits that model. For short-lived hosted runners, check the CLI docs before you depend on exec there.
Output falls back to plain text when the CI variable is set or output is piped, so CI logs stay diffable.
Common mistakes
Treating exit code 0 as "no vulnerabilities". It means the scan ran. Findings live in vuln.json; gate on that, or use safeguard ci scan --fail-on.
Ignoring exit code 2. Results were written, but the vulnerability scan did not run. Usually the scanners or their database are missing. Run safeguard doctor, which checks environment, sign-in, tools and policy and prints the most important problem first.
Forgetting the scanners on a fresh container. The "syft binary not found" error means the toolchain never got installed. Install syft and grype on PATH, or set TOOLS_DIR.
Skipping the checkout. safeguard ci scan does not need one. safeguard scan --dir . does, and safeguard exec refuses to run outside a git repository unless you pass --skip-git-repo-check.
Using an unpinned installer. A new release can change output or behaviour mid sprint. Pin SAFEGUARD_VERSION and bump it on purpose.
Expecting fork PRs to see secrets. They do not, so a platform scan step fails there. Plan for it.
Asking exec to change code. In strict mode that is denied unless pre-approved. Make changes interactively, with approvals, and let the pipeline verify them.
FAQ
Can I keep my Claude, ChatGPT or Codex plan if I switch to Safeguard?
Yes. Bring the Claude, ChatGPT or Codex plan you already pay for and use it inside the Safeguard CLI. Safeguard adds no extra charge for that usage; your plan's own usage limits and terms still apply. You can also connect Claude or ChatGPT to the Safeguard MCP server and use Safeguard from the chat you already use. See the CLI docs or /model for connecting a model.
Does my source code leave the build machine?
With safeguard scan --dir ., the scan runs on the build machine and writes files locally. When the local runner runs platform scan jobs, your source code never leaves the machine; only SBOMs and findings are uploaded. safeguard ci scan is different: the platform clones the repository and scans it on Safeguard.
Should I use safeguard scan or safeguard ci scan?
Use safeguard ci scan when you want platform policies to decide and results to appear in the platform. Use safeguard scan when you want files on the build machine, for artifacts or your own checks. Many teams run both.
What does safeguard exec do in CI?
It runs one non-interactive assistant turn and exits. The default permission mode is strict, so nothing is asked and anything the policy file has not pre-approved is denied. --output-format json gives machine-readable output.
Can I validate the SBOMs my build produces?
Yes. safeguard validate checks CycloneDX, SPDX and VEX documents. For example, safeguard validate ./sboms --recursive --strict -o report.json treats warnings as errors and writes a report you can keep as an artifact.
Which runners are supported?
The CI docs require a Linux or macOS runner with bash and curl. For the assistant side, see the security aware coding assistant post, and for checking what an AI agent changed before merge, see checking coding agent changes with the Safeguard CLI.
Try it yourself
Start on your own machine before you touch the pipeline. This walkthrough sets up the runner, picks the platform, starts it, approves it with its device code, then uses the same CLI to scan from the terminal.
Click through it at your own pace, or open it full screen.
Then download the Safeguard CLI, run safeguard scan --dir . --output out/ in a repository, and look at vuln.json with the jq recipes above. When the output looks right on your laptop, copy the same steps into your pipeline. The full reference is in the CLI docs.
Quick check
What does exit code 2 from safeguard scan mean?
Results were written, but the vulnerability scan did not run. Treat it as a failure in CI and run safeguard doctor to find out why.
What is the default permission mode for safeguard exec?
strict. Nothing is asked, and anything the policy file has not pre-approved is denied.
Sources
- Safeguard CLI overview
- Safeguard CLI usage guide
- Safeguard CI/CD integration
- Safeguard CLI configuration
- Safeguard CLI interactive terminal
- Safeguard CLI troubleshooting
- Safeguard downloads
- Grype repository
- Syft repository
- Grype JSON output guide
- jq manual
- Using secrets in GitHub Actions
- actions/upload-artifact
- actions/checkout
- GitLab job artifacts
Last reviewed: 21 October 2026.