Safeguard
Developer Tools•

Set Up the Safeguard CLI on a CI Runner, Step by Step

Set up a CI runner for the Safeguard CLI: pin the install, finish first-run setup, add SAFEGUARD_API_KEY, run safeguard ci scan --local, check with doctor.

Safeguard Security Team
19 min read

To run the Safeguard CLI on a CI runner, install a pinned version with the one-line installer, finish first-run setup so the Syft and Grype scanners are present, store a secret key as SAFEGUARD_API_KEY, then run safeguard ci scan --local --wait --fail-on high. Check the machine at any time with safeguard doctor.

Switching your pipeline to Safeguard does not mean giving up the AI you already pay for. 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 anyone on the team can connect Claude or ChatGPT to the Safeguard MCP server and ask about the same findings from the chat they already use.

This guide is about the machine, not just the YAML. It covers how to prepare a hosted, self-hosted or ephemeral runner, where the scanners and databases live, how the runner signs in, how to test it before a real build depends on it, and what to do when it breaks. If you want the wider tour of pipeline commands first, read running the Safeguard CLI in CI with safeguard exec, then come back here.

What you will have at the end

  • A runner (GitHub Actions, GitLab, Jenkins, Bitbucket, Azure Pipelines or anything that runs a shell) with a pinned Safeguard CLI on its PATH.
  • Scanner tools and vulnerability databases in place, refreshed on a schedule you control.
  • A secret key stored as SAFEGUARD_API_KEY with the scopes safeguard ci scan needs.
  • A pipeline step that scans each push on the runner, uploads only the results, and fails the build at the severity you choose or when a platform gate fails.
  • A health check you can run in ten seconds: safeguard doctor.
  • Optionally, the same machine taking platform scan jobs with safeguard runner scan-jobs, so private code is scanned where it already lives.

You need a Linux or macOS runner with bash and curl (the Safeguard CI docs list both as requirements), admin rights on your CI project to add a secret, and a Safeguard account. The sample project throughout is payments-api.

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. It writes code too, not only scans. That matters for CI: when a build fails on the runner, you run the same binary at your desk, reproduce the result, and fix it with the assistant 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, with no Node, Python or JVM needed. It runs on macOS, Linux and Windows, on x86_64 or arm64, and Windows is x86_64 only. For a desktop walkthrough on a Linux machine, see installing the Safeguard CLI on Linux.

First session on your own machine

Before you touch a runner, run it once at your desk:

cd ~/projects/payments-api
safeguard

With no arguments, safeguard opens the interactive terminal. Type /login and approve in your browser. /scan . scans the current folder. Ask in plain language, for example "how many critical vulns do we have", or "the CI job failed on a high severity finding, show me which package and fix it". @ attaches a file from the folder you started in, so @.github/workflows/security.yml lets the assistant read your workflow. /resume picks up the last session in that folder, and /model shows or switches the AI model; the plan you already pay for works here, as described above, and the CLI docs cover connecting a model.

Writing and fixing code

The assistant has tools for your local files and for your Safeguard data. It can write files, edit code and run commands, and each of those asks before it acts, per your permission mode. The recording below shows the loop you use when a CI scan fails.

It is a real CLI session on a Mac with a sample project. It installs with the curl command (checksum verified), runs safeguard --version, opens the interactive terminal and asks "fix the lodash CVE in this repo". The assistant reads the finding, the files and the advisory, then asks before each command (press y or n), edits package.json to lodash 4.17.21, runs npm install and npm test with approval, and prints a summary with a suggested commit message. It pushes nothing. Then it shows /permissions, /model and /scan ..

Click through it at your own pace, or open it full screen.

Approvals

/permissions in the interactive CLI (v11.11.0) offers Ask (reads run; edits and commands ask first), Accept edits (edits in this folder and read-only commands run; other commands ask), Plan (read and plan only), Auto (read-only commands and what the policy allows run; anything else asks; the default) and Strict (pre-approved actions only, for CI). A --yolo flag approves everything; keep it for throwaway sandboxes only, and never put it on a shared CI runner.

CI with safeguard exec

safeguard exec "summarise the critical findings in this repository" runs one non-interactive assistant turn, for CI. Its default permission mode is strict: nothing is asked, and anything not pre-approved by the policy file is denied. It refuses to run outside a git repository unless you pass --skip-git-repo-check. Useful flags from its help text: --output-format json for a machine readable result, --restricted for a read-only tool set with no writes, no shell and no network, and --max-budget-usd to end the turn once a spending limit is reached.

Step by step: prepare the runner

0. Decide what kind of runner you have

The setup differs mostly in where state lives between jobs.

Runner typeWhat persists between jobsInstall the CLIScanner tools and databasesSign-in for safeguard exec
Hosted (for example ubuntu-latest on GitHub)NothingEvery job, pinnedProvide them in the job, or use safeguard ci scan without --local so the platform does the scanNot covered by the docs; skip exec here
Self-hosted, persistentEverything on diskOnce, then on your upgrade scheduleOnce, then safeguard update on a timerSign in once as the runner's user
Self-hosted, ephemeralOnly what is baked into the imageIn the image, pinnedIn the imageNot covered by the docs; skip exec here

GitHub's docs describe self-hosted runners as machines you deploy and manage, where you are responsible for updating the operating system and other software, and where each job does not need a clean instance. That last point cuts both ways: a persistent runner keeps your scanner databases warm, and it also keeps whatever the previous job left behind. GitHub also supports ephemeral runners, registered with --ephemeral, which de-register after one job.

1. Choose an install location the runner user owns

The shell installer tries /usr/local/bin and falls back to ~/.local/bin. Set SAFEGUARD_INSTALL_DIR to put it somewhere the runner user owns, so the install needs no root:

curl -fsSL https://cli.safeguard.sh/install | SAFEGUARD_INSTALL_DIR="$HOME/bin" bash
export PATH="$HOME/bin:$PATH"

2. Pin the version

Installers resolve the latest release from https://cli.safeguard.sh/releases/latest.txt on every run. In CI, pin it so a new release never changes a build by surprise:

curl -fsSL https://cli.safeguard.sh/releases/latest.txt
curl -fsSL https://cli.safeguard.sh/install | SAFEGUARD_VERSION=11.11.0 SAFEGUARD_INSTALL_DIR="$HOME/bin" bash
safeguard --version
which -a safeguard

11.11.0 is the version we tested on 21 October 2026; use the string latest.txt gives you. The installer verifies the download against the release's checksums.txt and the shell installer aborts if that file is missing. Do not set SAFEGUARD_SKIP_CHECKSUM=1 unless you run an air-gapped mirror without checksums. which -a catches an older copy earlier on the PATH, which is a common cause of "the wrong version ran".

3. Stop the binary updating itself mid-pipeline

The self-update help text lists environment variables that turn updating off. On a pinned runner, set them in the runner's environment:

export SAFEGUARD_NO_UPDATE_CHECK=1
export SAFEGUARD_DISABLE_SELF_UPDATE=1

When you do want to move up, check first with safeguard self-update --check, which reports what a new release would change and installs nothing, then rerun the pinned installer with the new version.

4. Finish first-run setup so the scanners exist

safeguard scan and safeguard ci scan --local rely on Syft and Grype. The CLI downloads its scanner toolchain on first use of the interactive terminal. On a persistent runner, log in as the runner's user and run it once:

safeguard

On an ephemeral or hosted runner, put syft and grype on the PATH, or in a directory named by TOOLS_DIR, before the scan step. Bake them into the image if you build one.

5. Refresh the vulnerability databases on a schedule

safeguard update refreshes the Grype and Trivy databases. It does not update the binary, and it does not install scanners that are missing. Its help text notes that machines run it from cron, so on a persistent runner:

# crontab for the runner user: refresh databases at 05:15 every day
15 5 * * * $HOME/bin/safeguard update

On an air-gapped runner, download the database elsewhere and import it with safeguard update --scanner grype --import followed by the path to the archive. safeguard update --manual-instructions prints the steps.

6. Create the secret key

safeguard ci scan authenticates with a secret key from Settings in the Safeguard web app. Give it the scans:trigger scope, plus scans:read if you use --wait. Store it as a CI secret named SAFEGUARD_API_KEY: a repository secret on GitHub, a masked variable on GitLab, a secured repository variable on Bitbucket, a Secret text credential on Jenkins, or a secret pipeline variable on Azure Pipelines passed through the task's env block.

GitHub does not pass secrets, other than GITHUB_TOKEN, to workflows triggered from a fork, so the step will fail on pull requests from forks. Plan for that rather than working around it.

7. Check the network

The runner needs outbound HTTPS on port 443. Test it, and set a proxy or corporate CA if your network needs one:

curl -I https://api.safeguard.sh/auth
export HTTPS_PROXY=http://proxy.example.com:8080
export SSL_CERT_FILE=/path/to/corporate-ca.crt

The Safeguard troubleshooting docs advise against disabling certificate verification. Fix the trust store instead.

8. Run doctor and keep its output

safeguard doctor > doctor.txt
cat doctor.txt

safeguard doctor checks environment, sign-in, tools and policy, and changes nothing. Its exit code is 0 whenever the checks ran, so it is safe in a pipeline, but that also means you read the lines, not the status. Keep doctor.txt as a build artifact on the first few runs.

9. Run the first scan by hand

cd payments-api
SAFEGUARD_API_KEY=... safeguard ci scan --local --wait --fail-on high

With --local, the scan runs on the runner over --path (the current folder by default), and only the results are uploaded, never the source. It needs no repository integration and works for private and self-hosted repositories. It covers dependencies (SBOM and vulnerabilities); SAST runs only in the platform mode, without --local. Branch, commit and repository URL come from the CI environment, and --branch, --commit and --repository-url override them on systems that are not detected. The scan is recorded as a version named after the branch and the short commit SHA.

--wait polls every 15 seconds by default (--interval) and exits non-zero if the scan failed or did not finish within --timeout, which defaults to 30 minutes.

10. Let a platform gate decide

safeguard ci scan --local --wait --fail-on-gate Release

With --fail-on-gate, the command waits for the gate's policies to be evaluated against this scan. The step fails when the gate is Failed, Error or Not evaluated; Warning and Passed pass, and failed warn and notify policies are printed. This needs the policies:outcomes feature. Policies and gates are evaluated in the platform, not by the CLI, which is the point: the rules live in one place for every pipeline.

Wire it into your pipeline

GitHub Actions on a self-hosted runner

name: security
on: [push]
jobs:
  safeguard:
    runs-on: [self-hosted, linux, x64]
    steps:
      - uses: actions/checkout@v7
      - name: Scan on this runner
        env:
          SAFEGUARD_API_KEY: ${{ secrets.SAFEGUARD_API_KEY }}
        run: |
          export PATH="$HOME/bin:$PATH"
          safeguard --version
          safeguard ci scan --local --wait --fail-on high
      - name: Validate SBOMs the build produced
        run: |
          export PATH="$HOME/bin:$PATH"
          safeguard validate ./dist --recursive --strict -o validation-report.json
      - name: Keep reports
        if: always()
        uses: actions/upload-artifact@v7
        with:
          name: safeguard-reports
          path: validation-report.json

runs-on: [self-hosted, linux, x64] uses the default labels GitHub gives self-hosted runners; a runner must carry every listed label to pick up the job. The secret is passed through env, which is how GitHub recommends handing secrets to shell commands. Drop the validation step if your build does not write SBOMs to dist.

GitLab CI/CD

Safeguard publishes a GitLab template. Include it and extend .safeguard-scan:

include:
  - remote: https://registry.safeguard.sh/ci/safeguard.gitlab-ci.yml

safeguard:
  extends: .safeguard-scan
  variables:
    SAFEGUARD_LOCAL: "true"
    SAFEGUARD_FAIL_ON: "high"
    SAFEGUARD_VERSION: "11.11.0"

SAFEGUARD_LOCAL set to true scans on the runner and uploads only results. SAFEGUARD_TIMEOUT (default 30m), SAFEGUARD_PROJECT and SAFEGUARD_INSTALL_DIR are also available. If you included the older cli.safeguard.sh/ci/ template URL, update it: the docs say it stopped serving the file in October 2026. Store SAFEGUARD_API_KEY as a masked variable.

Jenkins, Bitbucket, Azure Pipelines and the rest

Safeguard does not publish a GitHub Action, Bitbucket Pipe or CI plugin. Every system runs the CLI as a plain shell step, so the same three lines work anywhere: install pinned, export the key from your CI's secret store, run safeguard ci scan. On Jenkins, bind a Secret text credential with withCredentials. On Azure Pipelines, map the secret into the bash task's env block, because secrets reach scripts only that way.

Optional: an assistant summary on a persistent runner

On a self-hosted runner where you signed in once, add a summary to the build log:

safeguard exec --restricted --output-format json "summarise the critical findings in this repository" > summary.json

The docs say the CLI does not read API keys from the environment for sign-in, so exec uses the runner user's own login. Sign in once with safeguard login --no-browser, which prints a URL and a short code you approve from any device. safeguard whoami --json confirms the sign-in and never prints the token. To check a branch's changes before merge, safeguard review --base origin/main reviews the diff for correctness and security; reviewing AI generated code before merge shows that workflow.

Beyond pipeline jobs: scan jobs and remote work

A persistent runner can do more than wait for pushes.

Take platform scan jobs. safeguard runner scan-jobs takes repository, image, directory and SBOM scans the platform routes to this machine, runs them there, and uploads only the results, never the source. Sign in first with safeguard login. All traffic is outbound HTTPS to the platform and nothing listens on a network port. This suits private repositories you do not want to grant a cloud service access to, and networks that only allow outbound HTTPS.

Serve assistant turns in a workspace. safeguard runner start --workspace ./payments-api starts the runner headless and serves turns for that workspace. safeguard runner status shows what it is doing and safeguard runner stop drains in-flight work and stops it. On networks that forbid held connections, pass --poll-wait 0 to poll at a fixed interval set by SAFEGUARD_POLL_INTERVAL (10 seconds by default). Only one headless machine runner may run per OS user.

Approve work from your phone. On a build machine you also use for maintenance, safeguard runner install registers the runner as a login service (a systemd user unit on Linux, a LaunchAgent on macOS, a logon scheduled task on Windows) so your phone or browser can send work to it. Keep this to machines you control, not shared runners that execute code from untrusted pull requests.

The recording below is a real Android app session. The same conversations appear on the phone. "Work on my computer" picks a computer running the CLI and a folder, the command waits for Allow once, Allow for turn or Deny on the phone, and the answer shows the tests passed. The Android guide covers installing the app.

Click through it at your own pace, or open it full screen.

Troubleshooting and common mistakes

Troubleshooting

syft binary not found on a fresh runner. First-run setup has not happened. Run safeguard once interactively as the runner user, or put Syft and Grype on the PATH or in TOOLS_DIR. If a directory scan warns and returns nothing, the database is empty: run safeguard update after that first run.

safeguard scan exits with 2. Results were written but the vulnerability scan did not run. Treat it as a failure and run safeguard doctor. Exit 1 means the command itself failed, for example a bad flag; exit 0 means the scan ran, whatever it found.

HTTP 401 or "not authenticated" from exec or runner. Check with safeguard whoami, then safeguard login --force. On a headless machine, add --no-browser and approve the device code from another device.

Certificate or SSL handshake errors. Set SSL_CERT_FILE to your corporate CA bundle or add the CA to the system trust store.

No dependencies found. The runner is in the wrong folder or the lock files are missing. Run your package manager's install first, for example npm install, pip install -r requirements.txt or go mod download, and pass --path to ci scan --local.

Timeouts or memory pressure on a monorepo. Scan subfolders as separate steps instead of the whole tree.

Need help. safeguard support bug with what you ran, what you expected and what happened files a report from the CLI. Attach doctor.txt and ~/.sg/logs/safeguard.log.

Common mistakes

  • Not pinning the version. An unpinned installer picks up each new release on the next run. Pin SAFEGUARD_VERSION and turn off self-update on the runner.
  • Gating on safeguard scan exit codes. It exits 0 whatever it finds. Use ci scan --fail-on or --fail-on-gate, or read vuln.json yourself.
  • Expecting SAST from --local. Local mode covers dependencies. Drop --local when you want the platform's full scan.
  • Running the runner as root. Install into a directory the runner user owns; nothing here needs root unless you choose /usr/local/bin.
  • Forgetting scans:read. Without it, --wait cannot read the result.
  • Putting --yolo on a shared runner. It approves everything. Use Strict, the CI mode.

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, as in connecting 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 runner?

Not with --local. The scan runs on the runner and only the results are uploaded. Without --local, Safeguard scans the repository on the platform, which is how SAST runs.

Should I use ci scan with or without --local?

Use --local for private or self-hosted repositories, for runners that cannot grant repository access, or when you want the scan on your hardware. Leave it off when the repository is connected as an integration and you want the platform's full scan, including SAST.

Is there a Docker image or GitHub Action for the CLI?

The docs say there is no Safeguard GitHub Action, Bitbucket Pipe or CI plugin, and the local runner docs say there is no container image. Install with the one-line installer in a shell step, or bake the binary into your own runner image.

Which runner operating systems work?

The CI docs ask for Linux or macOS with bash and curl. The binary itself ships for macOS (Apple Silicon and Intel), Linux (x86_64 and arm64) and Windows x86_64.

How do I keep databases fresh without slowing every build?

On persistent runners, run safeguard update from cron as the runner user. On ephemeral runners, bake recent databases into the image and rebuild it on a schedule.

Does the CLI evaluate our policies?

No. Policies and gates are evaluated in the platform. safeguard ci scan --wait --fail-on-gate waits for that evaluation and fails the step when the gate fails.

What permission mode does safeguard exec use in CI?

Strict, by default: nothing is asked and anything the policy file has not pre-approved is denied. Add --restricted for a read-only tool set.

Try it yourself

This demo sets up the runner on your own machine: pick the platform, start it, approve it with its device code, then use the same CLI to scan from the terminal. It is the same flow you follow once as the runner user on a persistent build machine.

Click through it at your own pace, or open it full screen.

Then get the CLI from the Safeguard download page, keep the CLI docs open, and prepare one runner with steps 1 to 10 above before you roll it out to the rest.

Quick check

What does safeguard ci scan --local upload?

Only the results of the scan it runs on the runner. The source stays on the machine. Local mode covers dependencies; SAST runs only in the platform mode.

Why is safeguard doctor safe to run in a pipeline?

It changes nothing and exits 0 whenever the checks ran. Read its output lines to see what is wrong.

Which scopes does the SAFEGUARD_API_KEY secret need?

scans:trigger, plus scans:read when you use --wait.

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.