The command line is where developers do their most focused work. It is also where they form their most durable opinions about the tools their company asks them to use. A security CLI that respects the conventions of the terminal — fast, scriptable, predictable — earns a permanent place in ~/.zshrc. A CLI that ignores those conventions gets aliased to true within a week.
This post is a design guide for security CLIs that developers run by choice. It draws on lessons from building Safeguard's CLI and from watching teams adopt or reject CLI-based security tooling at scale.
The CLI is not a thin wrapper
The first mistake security teams make when shipping a CLI is treating it as a thin wrapper around their web API. The implementation looks clean — a Go or Rust binary that authenticates, posts a payload, and renders the response — but the developer experience is poor. Every invocation makes a network round trip. Output is paginated for the web. Errors echo HTTP status codes. The tool feels like a wrapper because it is a wrapper, and developers can tell.
A good CLI is a first-class product with its own design constraints. It runs offline when it can. It produces machine-readable output by default. It has opinions about exit codes. It composes with grep, awk, jq, and the rest of the Unix toolbox. The fact that it talks to a backend API somewhere is an implementation detail, not the user experience.
Safeguard's CLI is built around this principle. The single binary, safeguard, holds an embedded policy bundle, a local cache of recent scan results, and an offline-capable scanner for the most common ecosystems. Network calls are reserved for situations where the local data is genuinely insufficient — a brand-new package, a custom policy update, or an explicit --remote flag.
Latency budgets and what they buy
Developers will run a CLI dozens of times a day if it is fast enough to fit between thoughts. They will run it once and never again if it is not.
The Safeguard CLI's latency targets are simple. Help and version commands return in under 50 milliseconds. Scan commands against a cached project return in under 500 milliseconds. Scan commands against a fresh project — first run, no cache — return in under 5 seconds for 95 percent of repositories. Anything slower and we treat it as a bug.
Hitting these numbers requires structural choices. The CLI does not parse JSON from disk on every run; it uses a memory-mapped binary cache. It does not call the policy service unless the local bundle is stale; bundle updates run as a background process behind a safeguard sync command. It does not redownload package metadata; the cache is populated incrementally as the developer's projects evolve.
The payoff is that developers run safeguard scan casually. They run it before opening a PR. They run it after merging from main. They run it when a teammate asks "is this dependency okay." A CLI that takes 8 seconds gets used once a week. A CLI that takes 500 milliseconds gets used 50 times a week, and it catches problems that would otherwise slip through.
Output formats: machine-first
Most security CLIs default to a colorful, table-formatted human output. This looks good in screenshots and demos. It is the wrong default.
Developers pipe CLI output into other tools. They grep for specific findings, awk specific columns, pipe through jq to filter, and feed the results into shell scripts that automate remediation across many repos. A CLI that emits decorative output by default is a CLI that breaks every script the moment its formatting changes.
The Safeguard CLI defaults to JSON when stdout is not a terminal and to a compact human format when stdout is a terminal. The detection is automatic via isatty, but it can be overridden with --output json or --output text. The JSON output is documented, versioned, and stable across CLI releases — breaking changes require a major version bump.
A second format, --output sarif, emits SARIF JSON for integration with code scanning platforms. A third, --output github-comment, emits a Markdown PR comment ready to paste into a GitHub comment field. Each format is a different consumer, and the CLI treats each one as a first-class output target.
Exit codes that mean something
Exit codes are the primary signal that CI systems and shell scripts use to decide what happens next. Most security CLIs use a binary scheme — zero on success, one on any failure — and miss an opportunity.
The Safeguard CLI uses a structured exit code scheme. Zero means no findings above the configured threshold. One means findings exist but they are advisory. Two means findings exist that violate the policy. Three means the scan failed for an environmental reason — network, authentication, malformed input — and the developer should retry. Codes four through nine are reserved for specific operational states.
The benefit of structured exit codes is that CI pipelines can branch on them. A pipeline can choose to fail on exit code two, warn on exit code one, and retry on exit code three, all without parsing CLI output. The convention is documented in the CLI's man page and in the project's CI integration guides.
Configuration: convention over flags
A CLI that requires 12 flags to do useful work is a CLI that no developer will memorize. Configuration belongs in a file the developer can commit, version, and review.
Safeguard's CLI looks for safeguard.toml in the project root, then walks up the directory tree until it finds one. The file holds policy thresholds, ecosystem-specific settings, and any overrides the developer needs. CLI flags are reserved for invocation-specific decisions — the path to scan, the output format, whether to use the remote service. Everything else belongs in the file.
The file format is opinionated. We chose TOML over YAML because TOML's syntax is harder to get wrong and the error messages are clearer. We chose a small surface area over a flexible one because every option is a place developers can misconfigure their security checks.
A useful side effect of project-level configuration is that the security team can ship a default safeguard.toml with sensible thresholds and let teams override locally. The override is visible in version control, which makes drift easy to spot during audits.
Errors that fail forward
A CLI's error messages are its dialogue with the developer when something goes wrong. Most security CLIs treat errors as an afterthought and emit cryptic messages like Error: invalid configuration or 403 Forbidden. The developer is left to guess what to do next.
Safeguard's error messages follow a fixed shape. Each error has a stable code (like SG-AUTH-001), a one-line explanation in plain language, and a suggested next step. Authentication failures suggest running safeguard login. Network failures suggest checking the proxy configuration. Policy parse failures point at the line in safeguard.toml that failed to parse and explain what was expected.
The error codes are stable across releases, which means search engines and internal documentation can link directly to remediation guides. A developer who encounters SG-POLICY-014 once and finds the fix can navigate the same error in the future without re-reading the CLI output.
Telemetry without surveillance
Security CLIs are a sensitive case for telemetry. The CLI necessarily sees source code, dependency lists, and project structure. Sending any of that to a backend without consent is a violation of trust, and developers will find out.
Safeguard's CLI sends only aggregate, anonymized metrics by default — invocation count, exit code distribution, CLI version. No project names, no package lists, no source paths. Detailed scan data is sent only when the developer invokes --remote, and the data sent is documented in the privacy notice.
Organizations that need richer telemetry can configure a telemetry block in safeguard.toml. The default is minimal. The maximum is documented. There is no hidden middle ground.
The CLI as a teaching tool
A well-designed security CLI does more than enforce policy. It teaches developers about the supply chain. The Safeguard CLI exposes subcommands like safeguard explain for a one-page summary of a package's risk profile — maintainer history, advisories, license, and download trends. Developers run this when evaluating a new dependency, with enough context to decide without leaving the terminal.
safeguard graph shows the dependency graph as ASCII with risk annotations on each node. The output is shareable, greppable, and concrete.
These commands turn the CLI from a gate into a workbench. Developers reach for it not because they have to but because it is the fastest way to answer a question about their dependencies. That is the relationship a security tool should aim for.