Safeguard
Developer Tools•

Scan Docker images before you push, step by step with the Safeguard CLI

Build the image locally, scan it with safeguard container-scan, fix findings with the assistant, rescan, then push. Results land on your machine first.

Safeguard Security Team
19 min read

To scan a Docker image before you push it, build it under a local candidate tag, run safeguard container-scan --image on that tag, read the findings, fix them with the Safeguard CLI assistant, rebuild and scan again. Push only when the second scan is clean enough for you. Results land as JSON files on your machine first.

You do not have to give up the assistant you already use to do this. 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. If you would rather stay in the chat app you use today, connect Claude or ChatGPT to the Safeguard MCP server and ask about your images from there.

This guide walks through the whole loop on a sample service called payments-api, then repeats it in GitHub Actions.

What you will have at the end:

  • A repeatable local routine: scan the source, check the Dockerfile, build, scan the image, fix, rescan, push.
  • A clear split between findings in your own code and findings in the base image, and a way to fix each.
  • An SBOM for the exact image you shipped, saved next to the digest you pushed.
  • A CI job that builds, scans and only then publishes.
  • The habit of approving each change the assistant makes, from the terminal, the desktop app or your phone.

Why scan before the push and not after

A push is a publication. Once docker push finishes, anyone with pull access to that repository can run the image, and automated deploys may pick it up within minutes. Scanning after the push still tells you what is wrong, but it tells you after the image is out.

Docker's own build guidance gives two more reasons. First, images are snapshots. The best practices page says to "rebuild your images regularly with updated dependencies", because packages inside a base image keep getting fixes after you build. Second, tags are mutable. The same page notes that pinning a base image to a digest is how you "fully secure your supply chain integrity", and that pinning also means you opt out of automatic fixes, so you need some way to notice when an update is due. A scan before each push is that way.

Before you start

You need four things.

  1. Docker with a working docker build. Docker Desktop or the Docker engine both work. Keep it running while you scan local images.
  2. The Safeguard CLI. It is one static binary with no Node, Python or JVM to install, for macOS, Linux and Windows on x86_64 or arm64 (Windows is x86_64 only).
  3. A Safeguard sign in. Run safeguard, type /login, and finish in the browser.
  4. A project with a Dockerfile. The examples use payments-api, a small Node service, but nothing here is Node specific.

Install on macOS or Linux:

curl -fsSL https://cli.safeguard.sh/install | bash
safeguard --version

On Windows, in PowerShell:

irm https://cli.safeguard.sh/install.ps1 | iex
safeguard --version

If you want a platform specific walkthrough, see installing the Safeguard CLI on macOS. Then check the install before your first scan:

safeguard doctor

safeguard doctor checks the environment, your sign in and the tools the CLI depends on, and changes nothing. It exits 0 whenever the checks ran, so read the lines, not the status.

Here are the commands this guide uses, all taken from the CLI's own --help output:

CommandWhat it doesLeaves your machine
safeguard findings sca --dir .Runs the dependency scanner locally and prints findingsNothing is uploaded
safeguard iac-scan --target .Checks Infrastructure as Code, including Dockerfiles, for misconfigurationsResults, when signed in
safeguard container-scan --image NAME:TAGBuilds an SBOM for the image and matches it against vulnerability databasesSBOM and findings, never source
safeguard findings container --image NAME:TAG --jsonSame scanner, printed as JSON linesNothing is uploaded
safeguard sbom --image NAME:TAG --output FILEWrites an SPDX JSON SBOM for the image and stopsNothing beyond the file
safeguard scan --sbom FILEScans an SBOM you already have (SPDX or CycloneDX)SBOM and findings

container-scan is a thin wrapper over safeguard scan --image, and its output is identical, so use whichever name you remember.

The walkthrough: build, scan, fix, rescan, push

Work in the project folder. Commit or stash first, so every change the assistant makes shows up in git diff.

Step 1. Scan the source before you build

Problems in your own dependencies end up in the image, so catch them first.

cd ~/code/payments-api
safeguard findings sca --dir .

findings runs one scanner on your machine and prints a normalised list: severity, title, the CVE or rule, package@version and the fixed version. Nothing is registered and nothing is uploaded, even when you are signed in. That makes it a good first pass on a branch you are not ready to share.

When you want the results in the platform, use the full scan instead:

safeguard scan --dir .

It writes JSON files under ./output, in a folder per scan. vuln.json holds the vulnerabilities and license.json the licences.

Step 2. Check the Dockerfile itself

A clean package list does not help if the Dockerfile runs everything as root or copies more than it should.

safeguard iac-scan --target .

iac-scan runs the trivy config engine over Terraform, CloudFormation, Helm, Kubernetes manifests, Dockerfiles and ARM or Bicep files in the target folder. Read its findings next to Docker's best practices: a minimal base image, separate build and runtime stages, and a pinned base where you can manage updates.

Step 3. Build under a candidate tag

Use a tag that makes it obvious the image is not released yet, and pull a fresh base while you are at it:

docker build --pull -t payments-api:candidate .

Docker's docs explain the flags. --pull checks for a newer base image even if one is cached. --no-cache rebuilds every layer from scratch, which refreshes packages from package managers but does not fetch a new base by itself. Use both together when you want a completely fresh build.

Step 4. Scan the image

safeguard container-scan --image payments-api:candidate

The CLI generates an SBOM for the image, runs the vulnerability match and writes the results under ./output. The SBOM covers both layers that matter: operating system packages from the base image and the application dependencies you copied in. When you are signed in, the SBOM and findings are uploaded to your Safeguard project. Your source code is not.

If you want the findings in your terminal only, with nothing uploaded:

safeguard findings container --image payments-api:candidate --json

Add --artifacts ./scan-artifacts to keep the scanner's raw output and SBOM in a folder for later.

For an image in a private registry, pass the credentials from environment variables rather than typing them into your shell history:

safeguard container-scan --image registry.example.com/team/payments-api:1.8.0 \
  --registry-username "$REGISTRY_USER" \
  --registry-password "$REGISTRY_TOKEN"

Step 5. If the local tag cannot be read, scan the SBOM instead

Some setups keep images somewhere the scanner cannot reach, such as a remote builder. In that case let Docker write the SBOM during the build and hand that file to Safeguard. Docker's attestation docs show the pattern: build with --sbom=true and the local exporter, and BuildKit writes an SPDX document called sbom.spdx.json to the output folder.

docker buildx build --sbom=true --output type=local,dest=out .
safeguard scan --sbom out/sbom.spdx.json

safeguard scan --sbom detects SPDX or CycloneDX automatically. Docker's docs also suggest validating a generated SBOM before you push, which the Safeguard CLI can do too:

safeguard validate out/sbom.spdx.json --strict

Step 6. Read the findings in the right order

Open the results with the assistant rather than scrolling JSON. Start the interactive terminal in the project folder:

safeguard

Then ask in plain language:

Summarise the findings from the payments-api:candidate image scan.
Split them into base image packages and application dependencies,
and show the fixed version for each high or critical one.

The assistant has tools for your local files and for your Safeguard data, so it can read ./output and your project's findings in one answer. Sort the work into two piles:

  • Application dependencies. These live in your lockfile. Fix them in the repository and rebuild.
  • Base image packages. These come from the FROM line. Fix them by moving to a newer base tag or digest, or a smaller base, and rebuilding.

If the platform shows reachability for your project, look at reachable findings first. A vulnerable function that your code never calls is still worth fixing, but not before one it calls on every request.

Step 7. Fix with the assistant, approving each step

Ask for the fix you want:

Fix the high and critical findings in the application dependencies.
Keep changes to patch and minor versions, then run the tests.

The recorded session below is the same pattern on this sample project: the assistant reads the finding, the files and the advisory, asks before each command, edits package.json, runs npm install and npm test with your approval, prints a summary with a suggested commit message and pushes nothing.

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

For the base image, be specific:

Update the FROM line in the Dockerfile to a newer patch release of the
same base image, keep the multi-stage layout, then rebuild as
payments-api:candidate and rescan it.

Each file edit and each command waits for you. Read the diff of the Dockerfile before you approve the rebuild. If you prefer a non conversational fix for dependencies, safeguard fix --dry-run previews dependency updates without changing files, and --severity high limits it to high and critical issues.

Step 8. Rebuild and rescan

docker build --pull -t payments-api:candidate .
safeguard container-scan --image payments-api:candidate

Compare the two scans, or ask the assistant "what changed between the last two image scans". Some findings will not have a fix yet. Note them, decide whether to accept them for this release, and move on.

Step 9. Tag, record the digest and push

docker tag payments-api:candidate registry.example.com/team/payments-api:1.8.0
docker push registry.example.com/team/payments-api:1.8.0
docker images --digests registry.example.com/team/payments-api

docker push prints a digest: sha256:... line for the pushed tag, and docker images --digests shows it again in the DIGEST column. Write that digest down with the release. Tags can move; a digest names exact content.

Step 10. Keep an SBOM for the image you shipped

safeguard sbom --image registry.example.com/team/payments-api:1.8.0 \
  --output payments-api-1.8.0-sbom.json

safeguard sbom writes an SPDX JSON SBOM using the same Syft based path the scanner uses, and stops before vulnerability analysis. Store it with the release notes. When a new advisory lands next month, you can rescan this file with safeguard scan --sbom without rebuilding anything.

Do the work properly with the Safeguard CLI

The CLI's larger job is to let you do the whole task in one place: find the problem, change the code, run the build and tests, and check the result, with an assistant that already knows your vulnerabilities.

Install and first session

Install it as shown above. Run safeguard with no arguments to open the interactive terminal, type /login, then /scan . to scan the folder you started in. Use @ to attach a file from that folder, such as @Dockerfile, so the assistant reads exactly what you mean. /resume picks up the last session in the folder.

Writing and fixing code

The assistant can write files, edit code and run commands, so it is not limited to reports. For container work that means editing the Dockerfile, bumping a lockfile, running docker build, running your tests and rescanning. It works on your machine, and when the local runner runs platform scan jobs, only SBOMs and findings are uploaded. Your source stays where it is.

Use 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. Use /model to see or switch the model, and see the CLI docs for connecting one.

Approvals

/permissions sets how much the assistant may do without asking:

  • 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. This is the default.
  • Strict. Pre-approved actions only, intended for CI.

There is also a --yolo flag that approves everything. Keep it for throwaway sandboxes. For image work, Ask or Auto is a good fit, because docker build and docker push are exactly the commands you want to see before they run.

CI with safeguard exec

safeguard exec "summarise the critical findings in this repository" runs one non interactive assistant turn and exits. Its default permission mode is strict, so nothing is asked and anything the policy has not pre-approved is denied, and it refuses to run outside a git repository unless you pass --skip-git-repo-check. Our guide to running the CLI in CI with safeguard exec covers it in depth.

The same check in GitHub Actions

Docker's GitHub Actions docs describe a test before push pattern: build with load: true so the image lands in the runner's local Docker, test it, and only then build again with push: true. A Safeguard scan fits between those two steps.

name: build-scan-publish
on: push

env:
  IMAGE: registry.example.com/team/payments-api

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7

      - uses: docker/login-action@v4
        with:
          registry: registry.example.com
          username: ${{ vars.REGISTRY_USER }}
          password: ${{ secrets.REGISTRY_TOKEN }}

      - uses: docker/setup-buildx-action@v4

      - name: Build locally
        uses: docker/build-push-action@v7
        with:
          context: .
          load: true
          tags: ${{ env.IMAGE }}:test

      - name: Install Safeguard CLI
        run: curl -fsSL https://cli.safeguard.sh/install | bash

      - name: Scan dependencies and fail on high
        env:
          SAFEGUARD_API_KEY: ${{ secrets.SAFEGUARD_API_KEY }}
        run: safeguard ci scan --local --wait --fail-on high

      - name: Scan the image
        run: safeguard findings container --image ${{ env.IMAGE }}:test --json

      - name: Publish
        uses: docker/build-push-action@v7
        with:
          context: .
          push: true
          tags: ${{ env.IMAGE }}:${{ github.sha }}

A few notes on this job:

  • safeguard ci scan --local scans the checkout on the runner and uploads only the results, never the source. It needs no SCM integration and works for private and self hosted repositories. With --wait --fail-on high, the step fails when the scan has a high or critical vulnerability, so the publish step never runs.
  • Authenticate with a secret key from Settings, Secret Keys, with the scans:trigger scope (and scans:read for --wait), stored as the CI secret SAFEGUARD_API_KEY.
  • To enforce your organisation's rules rather than a single severity, use --fail-on-gate with a gate name. Policies and gates are evaluated in the platform, not by the CLI; the CLI waits for the result and fails the step when the gate fails.
  • The image scan step prints findings into the job log for reviewers.

Approve the fix from your desk or your phone

A CLI session is live. It shows up in the web app, the desktop app and the phone app, and those devices can send work to a computer running the CLI, so you do not need to watch a terminal through every rebuild.

From the desktop or phone app, choose "Work on my computer", pick the computer running the CLI and the project folder, and ask for the rebuild. Each command and file edit waits for your approval on the device you are holding. The recorded Android session below shows the flow: the same conversations appear on the phone, the command waits for Allow once, Allow for turn or Deny, and the answer shows the tests passed.

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

The download page lists the CLI, a desktop app for Windows, macOS and Linux, apps for iOS and Android, IDE extensions and a browser extension. The cloud sandbox gives you an isolated tenant against a seeded demo repository, free for seven days with no card. Our guide to Safeguard on Android phones and tablets covers the phone side.

Troubleshooting

The scan says it cannot find the image. Check the exact name and tag with docker images. A scan of payments-api without a tag is a different reference from payments-api:candidate. Make sure Docker is running. If the image only exists on a remote builder, use the SBOM route in step 5.

A private registry returns an authentication error. Pass --registry-username and --registry-password, and check that the token can pull, not just push. For a CI job, confirm the secret is available to that workflow.

Results look stale. Rebuild with --pull so Docker fetches the newest base image, and --no-cache if you need package managers to run again. A scan only sees what is in the image you built.

The assistant will not run a command. Check /permissions. In Plan mode it reads and plans only. In Strict mode it runs only pre-approved actions.

safeguard exec refuses to start in CI. It runs only inside a git repository, so check out the code first, or pass --skip-git-repo-check if you have a real reason not to.

Common mistakes

  • Scanning latest. You end up with findings for an image you cannot name later. Scan the candidate tag, push a versioned tag, record the digest.
  • Fixing only the app dependencies. Many findings in a container come from the base image. If the FROM line has not changed in months, that is usually where the work is.
  • Pinning a digest and forgetting it. Docker's docs are clear that a pinned digest stops automatic fixes. Pin, but rescan on a schedule and bump on purpose.
  • Skipping the rescan. A fix can pull in a new version with its own issues. Always scan the rebuilt image, not the one you started with.
  • Approving without reading. Approvals only help if you read the diff and the command. A changed base image deserves a second look.
  • Using --yolo on your laptop. It approves everything. Keep it for throwaway sandboxes.
  • Pushing before the publish step is gated. In CI, the publish step must come after the scan step, and the scan step must be able to fail the job.

FAQ

Can the Safeguard CLI scan an image that has not been pushed?

Yes, that is the point of this workflow. Build with a local candidate tag and run safeguard container-scan --image on it. If your image lives on a remote builder the scanner cannot reach, build with Docker's --sbom=true local exporter and run safeguard scan --sbom on the SPDX file it writes.

Does my code or image leave my machine?

safeguard findings uploads nothing. A signed in scan or container-scan uploads the SBOM and the findings so they appear in your project. When the local runner runs platform scan jobs, your source code never leaves the machine; only SBOMs and findings are uploaded.

Can I keep using my Claude, ChatGPT or Codex subscription?

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 ask about your image findings from the chat you already use.

What is the difference between container-scan and scan --image?

None in output. container-scan is a thin wrapper over scan --image and produces identical results. It exists because it is easier to remember.

Will the assistant push my image for me?

Only if you ask and approve it. Commands such as docker push ask before they run in Ask and Auto modes. In the recorded session the assistant finishes with a summary and a suggested commit message and pushes nothing.

How do I block a release on findings in CI?

Use safeguard ci scan --local --wait --fail-on high to fail on a severity, or --fail-on-gate with a gate name to fail on your organisation's policies, which are evaluated in the platform. Put the publish step after it.

Which machines does the CLI run on?

macOS (Apple Silicon and Intel), Linux (x86_64 and arm64) and Windows (x86_64). It is a single static binary with no runtime to install.

Try it yourself

Set 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:

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

When you are ready, download the Safeguard CLI, build your next image under a candidate tag, and scan it before it goes anywhere. The CLI docs have every command, and our post on reviewing AI generated code before merge covers the step that comes right before the build.

Quick check

Which command scans an image and uploads nothing at all?

safeguard findings container --image NAME:TAG. It runs the scanner on your machine and prints the findings; no project is registered and nothing is uploaded, even when you are signed in.

Why record the digest after you push?

Tags are mutable, so the same tag can point to different content later. The digest names the exact image you scanned and shipped, which is what you need when a new advisory arrives.

Where are policies and gates evaluated when you use --fail-on-gate?

In the Safeguard platform. The CLI starts the scan, waits for the gate's result and fails the CI step when the gate fails.

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.