CLI flags and commands#
These are passed on the command line and are not part of the config file. For TOML keys see Configuration file; for ways to silence a single rule see Suppression mechanisms.
Top-level commands and flags#
SafeLint's top-level surface mirrors ruff's: every command can be invoked positionally (safelint check ...), and every flag has both a short and a long form where conventional. Help and version are intercepted before any command parsing so they always work, even when no subcommand is given.
$ safelint --help
SafeLint: Holzmann-inspired safety lint rules and pre-commit integration for Python, JavaScript, and TypeScript.
Usage: safelint [OPTIONS] <COMMAND>
Commands:
check Scan a file or directory for safety violations
skill Manage the bundled AI-client skill / project rule (Claude, Cursor, Copilot, Gemini, Windsurf, codex, Continue.dev, Cline, aider, Trae, Antigravity, Zed)
help Print this message or the help of the given subcommand
version Display SafeLint's version
Options:
-h, --help Print help (see a summary with -h)
-V, --version Print version
Global options:
--fail-on <LEVEL> Minimum severity that blocks the run: error | warning
--mode <MODE> Execution mode: local (only errors block) | ci (warnings block too)
--ignore <CODE> Repeatable; suppress a rule for this run
--format <FORMAT> Output format: pretty (default) | json | sarif
--statistics Print a per-rule violation count summary
--no-cache Disable the per-file lint-result cache
--stdin Read source from stdin (editor mode)
--stdin-filename <PATH> Pseudo-filename for stdin input
Equivalent invocations:
| Goal | Forms |
|---|---|
| Top-level help | safelint help, safelint --help, safelint -h |
| Subcommand help | safelint help check, safelint check --help |
| Version | safelint version, safelint --version, safelint -V |
ANSI colour is auto-disabled when stdout is not a TTY (piping to a file produces clean text), matching the rest of safelint's output conventions.
safelint check flags#
| Flag | Default | What it does |
|---|---|---|
--all-files |
off | Scan every supported source file under the target (today: .py, .pyw, .js, .mjs, .cjs, .ts, .tsx, .as). Default (without this flag) is to check only git-modified files. |
--fail-on |
from config | Override the minimum severity that blocks the run: error or warning. |
--mode |
from config | local (only errors block) or ci (warnings block too). |
--config |
auto-discovered | Path to a config file (pyproject.toml or safelint.toml) or a directory to use as the config search root. |
--ignore |
none | Repeatable flag to suppress a rule for this run only, e.g. --ignore SAFE101 --ignore function_length. Stacks on top of the ignore list in the config file. |
--format |
pretty |
Output format: pretty (ruff/ty-style coloured), json (stable schema documented in JSON schema), or sarif (SARIF 2.1.0 for GitHub code scanning). |
--statistics |
off | After the run, print a per-rule violation-count table (active + suppressed). Pretty mode only. Useful for CI snapshots and finding the most-fired rules. |
--no-cache |
off | Disable the per-file lint-result cache. By default safelint memoises rule output keyed on sha256(source + engine config + filepath) in .safelint_cache/ next to your config. |
--check-skill-freshness |
off | Added in 1.9.0. Before linting, verify each installed AI-client skill (Claude Code at ~/.claude/skills/safelint/SKILL.md, Cursor at ~/.cursor/rules/safelint.mdc, project-scoped equivalents) matches the bundled version in the active wheel. Stale installs surface as safelint: warning: … lines on stderr. Informational only, doesn't fail the lint. Use safelint skill status (also added in 1.9.0; exits 1 if stale) for the dedicated CI-friendly check. |
--stdin |
off | Read source from stdin instead of from disk. Designed for editor extensions linting un-saved buffers. Pair with --stdin-filename. |
--stdin-filename |
(none) | Pseudo-filename for --stdin input, drives language detection by extension and is shown as the violation file path. Required when --stdin is set. |
When to use --all-files:
- CI pipelines (clean checkout, no modified files in git terms)
- Running a one-off full audit
pre-commit run --all-filesalready passes all files directly; the hook mode handles this automatically.
Exit codes#
SafeLint's exit code tells CI / pre-commit how the run finished:
| Code | Meaning |
|---|---|
0 |
Clean run, no blocking violations (suppressed violations don't count). |
1 |
One or more blocking violations found (severity ≥ --fail-on). |
2 |
Silent-failure guard. Fires in every output mode (pretty / JSON / SARIF) so a CI pipeline can't silently report clean. Three distinct triggers, see below for the exact stderr message each emits. |
Exit code 2: silent-failure triggers#
All three triggers print a safelint: error: line to stderr before exiting; pre-commit treats exit 2 as a hook Failed (red), not Passed. Fix by installing the matching grammar extra (pip install 'safelint[python]' etc.) or, for pre-commit users, adding additional_dependencies: ['safelint[<lang>]'] to your .pre-commit-config.yaml.
| Trigger | Path | Stderr message |
|---|---|---|
Directory mode (safelint check src/ --all-files) discovers files but none get linted because every grammar is missing |
_check_exit_code after _run_check's lint pass |
safelint: error: no files linted, every supported file was skipped because its grammar package isn't installed, install with: pip install 'safelint[<lang>]' |
Git-modified mode (default safelint check src/), user modified files but every one was dropped by the supported-extensions filter |
_handle_no_targets in the no-targets short-circuit |
safelint: error: no files linted, every git-modified source file has a grammar that isn't installed, install with: pip install 'safelint[<lang>]' |
Hook mode (pre-commit invokes safelint <files>), every passed file has an extension whose grammar isn't installed |
_guard_hook_silent_failure before the engine runs |
safelint: error: no files linted, every file pre-commit passed had a grammar that isn't installed, add 'safelint[<lang>]' to additional_dependencies in your .pre-commit-config.yaml |
Each error embeds the install hint for the missing extras so the failure is self-explanatory even when no prior warnings were printed (e.g. the no-targets short-circuit, or any machine-output run). The hint phrasing flips between install with: pip install 'safelint[<lang>]' (direct CLI) and add 'safelint[<lang>]' to additional_dependencies in your .pre-commit-config.yaml (pre-commit, detected via PRE_COMMIT=1).
Per-extension stderr warnings (one line per missing grammar, listing the affected extensions) are emitted only in pretty (human) CLI output and in the pre-commit hook flow. Machine-readable modes, --format json and --format sarif, deliberately suppress those warnings so stderr stays parseable for CI / editor pipelines; the embedded install hint inside the safelint: error: line above is the only signal you get there.
safelint hook mode flags (pre-commit)#
Pre-commit passes the staged files as positional arguments automatically. --fail-on, --mode, and --ignore are all supported here.
# .pre-commit-config.yaml
- id: safelint
args: [--fail-on=error] # or --fail-on=warning for strict CI
# Ignore specific rules in the hook:
- id: safelint
args: [--fail-on=error, --ignore=SAFE203, --ignore=side_effects]
Skill freshness commands (1.9.0)#
After pip install --upgrade safelint, the bundled skill files in the wheel update but copy-mode installs at ~/.claude/skills/safelint/SKILL.md and ~/.cursor/rules/safelint.mdc stay frozen at whatever version was last installed. Two commands answer "is my installed skill up to date?":
# Dedicated subcommand: pipe-friendly, exits 1 if any install differs from bundled
safelint skill status
# Or fold the same check into a normal lint run (opt-in stderr warning, doesn't fail the run)
safelint check --check-skill-freshness --all-files .
safelint skill status walks every registered AI client × both scopes (user / project) and prints one line per detected install showing whether it's fresh (matches the bundled version) or differs from bundled. Exit code is 0 when everything matches, 1 when anything differs.
A few details worth knowing:
- Symlink installs (those created with
safelint skill install --symlink) always show as fresh, because the installed file is a symlink pointing back at the bundled location inside the wheel. Afterpip upgrade safelint, the upgrade is visible immediately; noskill updateneeded. - Copy installs are content-compared against the bundled file. If they match byte-for-byte, fresh; otherwise, differs.
- Edge case, a symlink install hand-replaced with an identical copy is reported as fresh (the freshness check compares content, not install mode). The user-visible behaviour is fine today because the content matches, but the next
pip upgrade safelintwon't propagate to that location anymore. If you want to re-establish the live link, runsafelint skill update --symlink --force.
The canonical CI / upgrade-script idiom is:
safelint check --check-skill-freshness is the same drift check folded into a normal lint run, emits one safelint: warning: line per stale install but doesn't change the lint exit code. Off by default so day-to-day safelint check invocations stay fast (no extra FS scan).
A locally-customised install will surface as differs from bundled; the diagnostic message explicitly mentions that case so customisers can ignore it. See Updating, removing, freshness checks for the full workflow.
Skill update + remove commands (1.10.0)#
Two follow-on commands to round out the install lifecycle:
safelint skill update # idempotent refresh, no-op when fresh
safelint skill update --force # re-install every detected install
safelint skill remove # delete every detected install
safelint skill remove --symlink # delete only symlink-shape installs (keep copies)
safelint skill remove --path /unusual/place # delete one specific location
safelint skill remove --dry-run # preview without deleting
Both commands inherit --client and --project from install, but the meaning of --client auto is different from install's:
install --client autoasks: "which AI client(s) does this user use?", and answers that by scanning the cwd for marker files (CLAUDE.md,.cursor/, etc.).update --client autoandremove --client autoask: "what's already installed?", and answer that by scanning the actual install paths (~/.claude/skills/safelint/SKILL.md, etc.).
This distinction matters when the user has marker files but no install yet (only install will fire) or has an install but the marker file has been deleted (only update/remove will fire).
update flag semantics, --force: without --force, update only re-installs the locations that have actually drifted from the bundle. Running it when everything is fresh is a no-op: it just prints "already fresh" lines and exits 0. That's what makes it safe to run from cron, CI, or pre-commit hooks, calling update repeatedly does nothing extra. With --force, it re-installs every detected location regardless of drift; useful for reverting a customised install back to the bundled content.
update flag semantics, --symlink and shape preservation: by default, update preserves the existing install's shape. A copy install stays copy after refresh; a symlink install stays symlink. To switch a copy install to symlink mode, pass --symlink explicitly. There's one wrinkle: if the copy install is already fresh, update is normally a no-op, so converting copy → symlink in that case requires update --force --symlink to force the re-install. Switching the other way (symlink → copy) isn't supported via update; do remove followed by install (without --symlink) instead, so the intent is unambiguous.
remove flag semantics: flags compose orthogonally, the absence of a flag means "no filter", not "only the opposite":
| Invocation | What gets removed |
|---|---|
remove (no flags) |
Every detected install, copy + symlink, every client, both scopes |
remove --symlink |
Only symlink-shape installs (copies survive) |
remove --client cursor |
All detected Cursor installs (both shapes, both scopes) |
remove --project |
All detected project-scope installs (user-scope survives) |
remove --path PATH |
Exactly one location, regardless of every other flag |
In particular, safelint skill remove without --symlink removes both copy and symlink installs; it's not a "remove copies only" command. --symlink is a filter you can opt into when you want to be selective, not a creation-mode toggle like in install.
remove only deletes the install location. Bundled files inside site-packages/ are never touched (shutil.rmtree doesn't follow symlinks for deletion, and unlink removes the symlink itself, not its bundled target). The worst case for a misfired remove is "re-run install to get the skill back". See Updating, removing, freshness checks for the full filesystem-level breakdown.