Secure an internal CLI by giving it the least privilege that works, distributing it from a verified source with checksums or signatures, refusing to trust config files from the repository it is run in, logging what it does, and scanning its dependencies on every release. It runs as the developer, so treat it like a privileged service.
Definition
An internal CLI is a command line tool your team builds for its own workflows: deploy helpers, environment setup, migration runners, access request tools. It typically calls internal APIs, holds or fetches credentials, and shells out to other programs. Because it runs on developer machines, its compromise gives an attacker a foothold on the very machines that can ship code.
The checklist
| Area | What good looks like |
|---|---|
| Privilege | The CLI asks for the narrowest token that works; actions needing more require re-authentication |
| Distribution | One official download location; checksums or signatures published; build comes from CI |
| Provenance | Release artifacts carry provenance linking them to a source commit |
| Config | Reads user level config; treats project level config as untrusted input |
| Credentials | Uses the operating system keychain or short lived tokens, never plain text in the repo |
| Input handling | No shell interpolation of arguments; validates paths and URLs |
| Logging | Records who ran which action, without logging secrets |
| Dependencies | Inventory kept, scanned each release, pinned by lock file |
| Updates | Defined, verifiable path (see our auto update decision guide) |
Why each row matters
Privilege and credentials come first because they decide the damage. GitHub's guidance on tokens points the same way: fine-grained tokens give narrower access than classic ones, and every personal token is tied to its owner. A CLI that needs to read pull requests should not ask for rights to delete repositories.
Provenance is where you can raise assurance over time. SLSA describes build levels: level 1 means provenance exists, level 2 means signed provenance from a hosted build platform, and level 3 means a hardened build platform. Even level 1 is useful, because it gives people a way to ask where a binary came from.
Config handling is the one that people forget. If your CLI reads a config file from the current directory, then cloning an unfamiliar repository and running the tool in it means running someone else's settings. Editors learned this lesson with workspace trust. Your CLI can copy the idea: ignore project level settings that can change commands or endpoints unless the user explicitly trusts the folder.
Steps to improve an existing tool
- List what the tool can do and which credentials it holds. Remove capabilities nobody uses.
- Move credential storage to the keychain or to short lived tokens issued at login.
- Build releases in CI, publish checksums, and record the commit.
- Search the code for shell calls built from strings. Replace with argument arrays.
- Add an audit log line for each state changing action.
- Run a dependency scan and fix the findings that are reachable first.
- Write a one page owner note: who maintains it, how to report a problem, and how to revoke access.
Try it yourself
Click through it at your own pace, or open it full screen. Notice how findings tie back to specific lines, the way string built shell calls would.
Where Safeguard fits
Safeguard's SBOM and dependency scanning and its CLI can run in your pipeline for internal tools too. Try the free scan at gold.safeguard.sh/scan.
Quick check
Why treat project config as untrusted?
Because a cloned repository can contain a config file that changes what your CLI does. Trusting it silently means running someone else's settings.
Is signing required before release?
Not required to start. Publishing checksums and build provenance is a useful first step, and signing can follow.
Sources
- SLSA security levels
- Managing your personal access tokens, GitHub Docs
- Workspace Trust, Visual Studio Code documentation
Last reviewed: 19 October 2026.