In a CI job running `checkov -d .` over Terraform, what decides the exit code, and what does `--soft-fail` change?
answer
- zero versus one
- passed or skipped counts as passing
- soft fail still prints
- skip means never evaluated
basics
~20 sCheckov exits 0 when every check passed or was skipped and 1 when any check failed. With -s or --soft-fail it always exits 0 but still reports every failure, which is different from skipping a check, which never runs.
solid answer
~50 sCheckov returns `0` when every check it ran passed or was skipped and `1` as soon as one failed, so a plain `checkov -d .` step blocks the job on any finding. `-s` / `--soft-fail` keeps the scan and the report exactly as they were and only forces the exit code to `0` — the failures are still printed, still in `-o junitxml` or `-o sarif`, just no longer blocking. That is not the same as `--skip-check`, which removes a check from the run so it produces no result at all. `--soft-fail-on` and `--hard-fail-on` override the blanket flag per check ID or severity. Two quieter cases: a file Checkov cannot parse is reported as a parsing error but does not fail the run unless `CKV_PARSE_ERROR_FAIL=true`, and exit `2` is the crash path, for example a failed platform integration.
go deeper
Recall the two everyday exit codes: 0 when everything passed or was skipped, 1 when anything failed. Know that soft fail keeps the report and only changes the exit code.
Explain how --soft-fail-on and --hard-fail-on refine the blanket flag, and why skipping removes evidence while soft failing keeps it.
Show you would sequence a rollout from observe-only to blocking, and that you would close the quiet gaps such as parsing errors that do not fail the run.
Weigh how long an organisation can stay in soft-fail mode before the gate stops meaning anything, and who owns the switch to blocking.
## What Checkov's exit code means Checkov is a static scanner for infrastructure-as-code: it reads Terraform, CloudFormation, Kubernetes manifests and other formats **before anything is applied** and evaluates built-in checks against them. In a pipeline, the only thing the CI system listens to is the **exit code** of the `checkov` process. At Checkov 3.3 the rule is simple: - **`0`** — every check that ran either **passed** or was **skipped**. - **`1`** — at least one check **failed** and nothing told Checkov to tolerate it. - **`2`** — the run itself broke. The CLI reference describes it as a failure in the integration with the commercial platform; the code takes the same exit when a framework runner reports an error. `--no-fail-on-crash` turns that case into `0`. So a bare `checkov -d .` step fails the build on the very first failed check, in any framework it scanned. Teams adopting the tool on an existing repository usually discover this on day one, when the first run reports dozens of failures and blocks every merge. ## What `--soft-fail` does — and what it does not `-s` / `--soft-fail` makes Checkov **always return `0`**, regardless of results. It does **not** change what is scanned or what is reported: 1. Every check still runs. 2. Every failure is still printed in the CLI output and written to any report format you asked for (`-o cli`, `-o json`, `-o junitxml`, `-o sarif`, and so on). 3. Only the final exit code is forced to `0`. This makes soft fail an **observe mode**: the job goes green, the findings remain visible in the job log or in whatever system consumes the report, and nobody is blocked. The risk is the obvious one — a soft-failed pipeline that nobody reads is a scanner that has been switched off. ## Soft fail versus skipping The distinction interviewers probe is between a **soft failure** and a **skip**: | Mechanism | Does the check run? | Does the finding appear? | Effect on exit code | |---|---|---|---| | `--soft-fail` | yes | yes, as FAILED | forced to `0` | | `--skip-check CKV_AWS_20` | no | no result at all | the check cannot fail | | `--check CKV_AWS_20` | only the listed checks run | only their results | others cannot fail | A skipped check is not a failure, so it cannot produce exit `1`; it also produces no evidence. A soft-failed check is a real failure that has merely been made non-blocking. Which exceptions deserve an inline skip and how they are reviewed is a separate governance subject; here the point is the mechanical difference. ## Narrowing the blanket flag Two more flags refine the decision per finding: - **`--soft-fail-on`** — a comma-separated list of check IDs (wildcards allowed) and/or severities. The run exits `0` only if **every** failed check matches it; any failure outside the list hard-fails. - **`--hard-fail-on`** — a list of check IDs and/or severities that always produce exit `1`. If no failure matches it, the run soft-fails. When the lists overlap, Checkov evaluates each failed check in order: an ID in the hard-fail list wins, then an ID in the soft-fail list, then a severity in the hard-fail list, then a severity in the soft-fail list. A failure that matches **neither** list falls back to the value of `--soft-fail` — except when only a hard-fail list was given, in which case unmatched failures are soft. Severities only work when the checks actually carry one, which for the built-in catalogue needs the platform integration — a separate trap worth knowing. ## The quiet zero: parsing errors A Terraform file Checkov cannot parse is listed under **parsing errors** in the summary, but by default it does **not** fail the run. If every file that did parse is clean, the exit code is `0` while part of the repository was never evaluated. Setting the environment variable `CKV_PARSE_ERROR_FAIL=true` makes a parsing error return `1`. In a gate that is meant to be trusted, that is worth turning on, or at least watching the `parsing_errors` count in the JSON summary. ## Putting it in a pipeline A common, honest rollout looks like this: 1. Start with `--soft-fail` and a machine-readable report (`-o cli -o junitxml`), so the team sees the findings without being blocked. 2. Fix or consciously accept the existing failures, or record them in a baseline file. 3. Remove `--soft-fail`, or replace it with `--hard-fail-on` for a list of check IDs the team agrees must block. 4. Decide whether a parsing error should block, and set `CKV_PARSE_ERROR_FAIL` accordingly. The interview signal is precise vocabulary: Checkov's exit code reflects **failed checks**, `--soft-fail` changes **only the exit code**, and skipping removes the check from the run entirely.
- With `--soft-fail-on CKV_AWS_18` and two failures, CKV_AWS_18 and CKV_AWS_21, what does Checkov return?Exit `1`. `--soft-fail-on` tolerates a run only when every failed check matches the list. CKV_AWS_21 matches neither the soft-fail nor a hard-fail list, so it falls back to `--soft-fail`, which is off, and one hard failure makes the whole run a hard failure. Adding `-s` would turn that fallback into a soft failure and the run would return `0`.
- Why might a Checkov job stay green while a broken Terraform file sits in the repository?A file Checkov cannot parse becomes a parsing error, not a failed check, and by default parsing errors do not change the exit code. If everything that did parse passes, the run returns `0` even though the broken file was never evaluated. Setting `CKV_PARSE_ERROR_FAIL=true` makes parsing errors fail the run; otherwise watch the parsing-errors count in the summary.
saying these in an interview costs you the question
- Soft fail hides the failed checks from the output and the reports.
- A skipped check and a soft-failed check are the same thing.
- Checkov only fails the build on HIGH or CRITICAL findings by default.
- A file Checkov cannot parse always fails the run.
- Exit code 2 means Checkov found critical misconfigurations.