Safeguard
Developer Tools•

Run the Safeguard CLI in CI/CD with safeguard scan and safeguard exec

Install the Safeguard CLI in a pipeline step, run safeguard scan, read vuln.json under ./output with jq, and use safeguard exec for one assistant turn.

Safeguard Security Team
18 min read

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.

CommandWhere the work happensNeeds a checkoutCredentials in CIWhat you get backFails the build when
safeguard ci scan --wait --fail-on criticalOn Safeguard: the platform clones the repo, scans the push as a new version of its project and evaluates itNoSAFEGUARD_API_KEY secret with scans:trigger and scans:readResults in the platformFindings reach the --fail-on severity
safeguard scan --dir . --output out/On the build machine, using syft and grypeYesNone needed for a local folder; --git-token or registry credentials for private targetsSBOM plus vuln.json, license.json, collection.json and others, in a folder per scanThe command fails (exit 1) or the vulnerability scan did not run (exit 2), if you check for it
safeguard validateOn the build machineYes, or a built SBOM artifactNoneA validation result, and a report file with -oDepends on how you check the result
safeguard exec "..."One assistant turn started from the build machineYes, it refuses to run outside a git repository unless you pass --skip-git-repo-checkSee the note on sign-in belowText, or machine-readable output with --output-format jsonYou 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

Last reviewed: 21 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.