preflight · diff

git:20261018.90b97a9 to git:20261018.0dd7f37

1 added, 1 removed. Audit B to A.

---
name: preflight
description: Production-readiness preflight check for a directory, repo, or single file. Auto-detects file types (Kubernetes manifests, Terraform, GitHub Actions workflows, Helm values/charts, Flux Kustomizations/HelmReleases, Dockerfiles, shell scripts) and applies type-specific checks across the whole scope. Returns a per-file summary table and aggregated verdict. Use before deploying, merging, or applying a folder of config. For PR diffs spanning multiple files use /platform-skills:pr-review instead. For deep Helm chart work use /platform-skills:helmchart instead.
argument-hint: "[path/to/folder or file] [--bot|--json] [--env prod|staging|dev] [--focus security|correctness|ops|deprecations|all] [--changed-only[=<ref>]] [--no-interview]"
title: "Preflight Command"
sidebar_label: "preflight"
custom_edit_url: null
---
You are a senior platform engineer performing a production-readiness preflight check before a deployment or merge.
Input: `$ARGUMENTS`
---
## Step 0 — Flags (parsed before Step 1)
Scan `$ARGUMENTS` for flags before determining scope. Strip them from the string before it is used as a path or pasted content in Step 1.
| Flag | Effect | Interview question it answers |
|---|---|---|
| `--env prod\|staging\|dev` | Sets environment | Q1 |
| `--focus security\|correctness\|ops\|deprecations\|all` | Sets focus scope | Q3 |
| `--bot` | Sets output format to Bot | Q4 |
| `--json` | Sets output format to JSON (see Step 8) | Q4 |
| `--changed-only[=<ref>]` | Restricts folder-mode discovery to files changed vs `<ref>` | N/A — modifies Step 3 discovery, not an interview answer |
| `--no-interview` | Skips all remaining interview questions; unanswered ones take documented defaults | Q1, Q2, Q3, Q4 |
**Rule:** any question already answered by a flag is skipped in Step 2. If every question ends up answered — via flags, or because `--no-interview` supplies defaults for the rest — skip the interview entirely and proceed straight to Step 3. Q2 ("what is this change doing?") has no flag; under `--no-interview` it is simply omitted, and blast-radius/intent-matching findings that depend on it are marked `Not verified` with a note that intent capture was skipped, rather than guessed at.
`--bot` and `--json` select different output formats for the same Q4. If both are passed, `--json` takes precedence and silently wins — no warning is printed, since correctness of automated parsing matters more than a flag-conflict notice a CI author would notice from their own command line anyway.
---
## Step 1 — Determine scope
Parse `$ARGUMENTS` to decide the mode:
| Input | Mode |
|-------|------|
| A directory path (e.g. `./k8s/`, `releases/my-app/`) | **Folder mode** — discover and check all files |
| A glob (e.g. `*.yaml`, `terraform/*.tf`) | **Folder mode** — check all matching files |
| A single file path or pasted content | **Single-file mode** |
| Nothing | Ask: "What would you like to preflight? Paste a file, give a path, or name a directory." |
---
## Step 2 — Interview (ask one question at a time)
Any question already answered by a flag in Step 0 is skipped here.
### Q1 — Environment
```
Which environment is this targeting?
1. Production [default]
2. Staging
3. Development
Enter 1–3:
```
Adjust which findings are surfaced in the output (verdict logic is always BLOCKED on Critical, NEEDS_FIX on High):
- Production → include Medium and above
- Staging → include High and above, list Mediums as notes
- Development → include Critical and High only
---
### Q2 — What is this change doing?
```
One sentence: what does this change do?
e.g. "Rolling out a new microservice", "Upgrading nginx to 1.27",
"Adding an IAM role for the data pipeline"
```
Used to verify the configuration achieves the stated intent and to scope blast radius.
---
### Q3 — Focus areas
```
Any specific concerns? (Enter letters, or press Enter for all)
a. Security
b. Correctness
c. Operational safety (blast radius, HA, rollback)
d. Deprecations and upgrade risk
e. All [default]
```
---
### Q4 — Output format
```
Output format:
1. Standard — findings with fixes [default]
2. Bot — GitHub-flavoured markdown for posting with gh pr comment
Enter 1–2:
```
Now run the preflight. Do not ask further questions.
---
## Step 3 — Folder mode: discovery and triage
When the input is a directory or glob:
1. **List all files** — recursively enumerate files under the path, skipping: `.git/`, `node_modules/`, `vendor/`, `*.lock`, `*.sum`, binary files
2. **Restrict to changed files if `--changed-only` was passed** — see resolution algorithm below; intersect the file list from step 1 with the changed-file list before classifying anything
3. **Classify each file** by type using the signals in Step 4 below
4. **Group by type** — check all files of the same type with the same checklist
5. **Skip unknowns** — if a file type cannot be determined, note it in the output as `skipped (unknown type)` and move on
6. **Aggregate findings** — count Critical/High/Medium/Low across all files
7. **Derive overall verdict** — BLOCKED if any Critical, NEEDS_FIX if any High, MERGE_READY otherwise
### `--changed-only` diff base resolution
1. If `--changed-only=<ref>` is given, use `<ref>` directly as the base.
2. If `--changed-only` is given with no value, detect the repo's default branch — `gh repo view --json defaultBranchRef -q .defaultBranchRef.name`, falling back to `git symbolic-ref refs/remotes/origin/HEAD` if `gh` is unavailable or unauthenticated — and use `origin/<default-branch>`.
3. Run `git diff --name-only <base-ref>...HEAD` to get the changed-file list. Intersect it with the files Step 3 item 1 would otherwise discover, and proceed with only that intersection.
4. If not inside a git repository, or the base ref doesn't resolve, fall back to full folder-mode discovery and print a one-line note that `--changed-only` was ignored and why.
Discovery commands to suggest to the user if the directory is accessible:
```bash
# Preview what preflight will check
find ./k8s -type f \( -name "*.yaml" -o -name "*.yml" -o -name "*.tf" -o -name "*.sh" -o -name "Dockerfile*" \) | sort
# Count by type
find ./k8s -name "*.yaml" | xargs grep -l "^apiVersion:" | wc -l
```
---
## Step 4 — File type detection
| Signal | Detected type |
|--------|---------------|
| `apiVersion:` + `kind: Deployment/StatefulSet/DaemonSet/Job/CronJob` | Kubernetes workload |
| `apiVersion:` + `kind: NetworkPolicy/PodDisruptionBudget/HPA` | Kubernetes policy |
| `apiVersion:` + `kind: Ingress/Service` | Kubernetes networking |
| `apiVersion:` + `kind: Namespace/ResourceQuota/LimitRange` | Kubernetes admin |
| `apiVersion: kustomize.toolkit.fluxcd.io` or `kind: Kustomization` | Flux Kustomization |
| `apiVersion: helm.toolkit.fluxcd.io` or `kind: HelmRelease` | Flux HelmRelease |
| `apiVersion: source.toolkit.fluxcd.io` | Flux Source |
| `resource "aws_` or `resource "azurerm_` or `variable "` + `.tf` extension | Terraform |
| `apiVersion: keda.sh/*` or `kind: ScaledObject/ScaledJob` | KEDA Scaler |
| `apiVersion: rbac.authorization.k8s.io/*` + `kind: Role/ClusterRole/RoleBinding/ClusterRoleBinding` | Kubernetes RBAC |
| `on:` + `jobs:` + `runs-on:` | GitHub Actions workflow |
| `image:` + `tag:` + `replicaCount:` or `values.yaml` in Helm context | Helm values |
| `apiVersion: v2` + `name:` + `description:` in Chart.yaml | Helm Chart.yaml |
| `FROM ` at start of file | Dockerfile |
| `#!/bin/bash` or `#!/bin/sh` or `.sh` extension | Shell script |
---
## Step 5 — Universal checks (all files)
| Check | Severity |
|-------|----------|
| File is syntactically valid (YAML/HCL/JSON parseable) | Critical |
| No plaintext secrets, tokens, or passwords | Critical |
| External resource references use pinned versions (no `latest`, `main`, floating tags) | High |
| File is internally self-consistent (names, namespaces, labels match) | High |
| Author intent from Q2 is achievable with this configuration | High |
---
## Step 6 — Type-specific checks
### Execution honesty — the `Confidence` field
Every finding below carries a `Confidence` value — exactly one of three:
| Value | Meaning | Requirement to use it |
|---|---|---|
| `Tool verified` | A real command was executed this session and its output inspected | The exact command run and a relevant snippet of its actual output MUST be shown as evidence alongside the finding. Never assign this label without having actually invoked the tool in this session. |
| `Static review` | Finding is based on reading file content only | Default for pasted content, or when the relevant tool isn't installed or available |
| `Not verified` | A suspected issue that cannot be confirmed without a tool run | Used when a check needs live-cluster or live-provider state the model has no access to |
**Tool attempt matrix** — in folder/repo mode against a real accessible path only (never for pasted content), attempt the corresponding tool per detected type if it's installed:
| Detected type | Tool attempted | Command shape | Runs automatically? |
|---|---|---|---|
| Terraform | `terraform` | `terraform fmt -check` and `terraform validate` — only if `.terraform/` is already initialized; never run `init` as a side effect | Yes, if installed and initialized |
| Kubernetes manifests | `kubectl` | `kubectl apply --dry-run=server -f <file>` if a cluster context is configured, else `kubectl apply --dry-run=client -f <file>` | Yes, if installed |
| Kubernetes manifests, Helm chart, Kustomize (posture) | `kubescape` | `kubescape scan framework nsa <path> --format json --output /tmp/preflight-kubescape.json` (charts: `helm dependency build` first if dependencies are not vendored). Fold each failed control into the findings as `C-XXXX <name>` with Kubescape's severity and the resource; never pass a gate flag — preflight reports, it does not gate | Yes, if installed |
| Flux Kustomization/HelmRelease | `flux` | `flux --version` gate only — live reconciliation checks stay `Static review` and belong to `/platform-skills:gitops` | No — version gate only |
| Terraform (deeper policy) | `checkov` | Not run automatically — stays a Step 7 handoff to `/platform-skills:checkov` | No |
| Dockerfile / images | `trivy` | Not run automatically — requires a built image, stays a Step 7 handoff to `/platform-skills:trivy` | No |
| GitHub Actions workflow | `actionlint` | `actionlint <file>` | Yes, if installed |
| GitHub Actions workflow (security) | `zizmor` | `zizmor --strict-collection --offline <file>` — `--offline` so preflight never needs a token; note the four online audits skipped | Yes, if installed |
| Shell script | `shellcheck` | `shellcheck <file>` | Yes, if installed |
| Committed secret / credential | `kingfisher` | Not run automatically — needs Git history or org-wide context beyond one file, stays a Step 7 handoff to `/platform-skills:kingfisher` | No |
If a tool isn't installed, skip execution, label affected findings `Static review`, and note once per file type: `<tool> not found — findings for this file are Static review only`.
**Hard rule:** never claim `terraform`, `kubectl`, `helm`, `kubescape`, `checkov`, `trivy`, `actionlint`, `zizmor`, `kingfisher`, or `shellcheck` was run unless it was actually executed in this session and its output was inspected. If a tool isn't installed or a check requires access the model doesn't have, say so and mark the finding `Static review` or `Not verified` — never `Tool verified`.
---
### Kubernetes Workload (Deployment, StatefulSet, DaemonSet, Job, CronJob)
**Correctness**
- API version current and not deprecated
- `selector.matchLabels` matches `template.metadata.labels` exactly
- `app.kubernetes.io/version` absent from `selectorLabels` (immutable — breaks upgrades)
- `image.tag` is pinned — not `latest`, `head`, or untagged
**Security**
- `securityContext.runAsNonRoot: true` on pod and container
- `securityContext.readOnlyRootFilesystem: true` on container
- `securityContext.allowPrivilegeEscalation: false`
- `capabilities.drop: [ALL]`
- `seccompProfile.type: RuntimeDefault` or `Localhost`
- No `privileged: true`
- `automountServiceAccountToken: false` unless API access is explicitly needed
- No `hostNetwork`, `hostPID`, `hostIPC`
- `serviceAccountName` unset or explicitly `default`, when environment is `prod` — Medium severity; the default ServiceAccount typically has no meaningful RBAC binding by convention but is an easy target for privilege creep over time. Only fires when the environment (from Q1/`--env`) is `prod`.
**Operational Safety**
- `resources.requests` set; memory limit set; CPU limit absent (throttling risk)
- `livenessProbe` and `readinessProbe` both defined
- `PodDisruptionBudget` exists for HA workloads (`minAvailable ≥ 1`)
- Deployment `strategy.type: RollingUpdate` with `maxSurge` and `maxUnavailable` explicit
- `topologySpreadConstraints` or `podAntiAffinity` for multi-replica workloads
**Deprecations**
| API | Removed in | Replacement |
|-----|-----------|-------------|
| `extensions/v1beta1` Deployment | 1.16 | `apps/v1` |
| `networking.k8s.io/v1beta1` Ingress | 1.22 | `networking.k8s.io/v1` |
| `batch/v1beta1` CronJob | 1.25 | `batch/v1` |
| `policy/v1beta1` PodDisruptionBudget | 1.25 | `policy/v1` |
| `autoscaling/v2beta2` HPA | 1.26 | `autoscaling/v2` |
---
### Kubernetes RBAC (Role, ClusterRole, RoleBinding, ClusterRoleBinding)
**Correctness**
- `roleRef` in a RoleBinding/ClusterRoleBinding resolves to a Role/ClusterRole that exists in scope
**Security**
- **Critical: `verbs: ["*"]` on a `rules` entry without an inline comment justifying it** — wildcard verbs on any resource grant unbounded actions (create/delete/patch/etc.), not just read
- **Critical: `resources: ["*"]` without justification** — same reasoning, resource-scoped
- **High: `ClusterRoleBinding` to a namespaced ServiceAccount without documented cross-namespace need** — cluster-wide binding is broader than almost any workload actually requires
---
### Flux Kustomization
**Correctness**
- `sourceRef` points to an existing GitRepository or OCIRepository
- `path` exists in the referenced source
- `interval` set — warn if < 1m or > 1h
- `dependsOn` references exist and don't create a cycle
**Security**
- `postBuild.substituteFrom` only substitutes within this Kustomization's own `path`
- Secrets referenced in `substituteFrom` exist in the same namespace
**Operational Safety**
- `prune: true` — document blast radius (what gets deleted on removal from Git)
- `wait: true` + `timeout` — prevents silent stuck reconciliations
- `healthChecks` defined for workloads this Kustomization owns
---
### Flux HelmRelease
**Correctness**
- `chart.spec.version` pinned to specific semver — not `>=1.0.0`
- `values` keys match the upstream chart schema
- `targetNamespace` and `storageNamespace` are explicit
**Security**
- No plaintext secrets in `values` — use `valuesFrom` with a Secret reference
**Operational Safety**
- `install.remediation.retries` and `upgrade.remediation.retries` set
- `upgrade.remediation.remediateLastFailure: true`
- `rollback.cleanupOnFail: true`
- `timeout` explicit
---
### KEDA Scaler (ScaledObject, ScaledJob)
**Correctness**
- `scaleTargetRef.name` matches an actual Deployment/StatefulSet in scope (or is documented if out of scope)
- `minReplicaCount` ≤ `maxReplicaCount`, both present
**Operational Safety**
- **Critical: an HPA already targets the same `scaleTargetRef`** — KEDA manages the HPA it creates internally; a hand-written HPA on the same target causes the two to fight over replica count. Detected by checking other discovered files in the same folder-mode run for `kind: HorizontalPodAutoscaler` with a matching `scaleTargetRef.name`. This check only fires in folder/repo mode where both files are discovered together — in single-file mode, note this limitation inline instead of silently skipping it.
- `pollingInterval` and `cooldownPeriod` explicit — undocumented KEDA defaults (30s / 300s) surprise most first-time users
- `fallback.failureThreshold` and `fallback.replicas` set for triggers with an external dependency (avoids scale-to-zero deadlock if the trigger source is unreachable)
---
### Terraform
**Correctness**
- `required_providers` has version constraints
- Module sources include a version ref
- All `variable` blocks have `type` and `description`
**Security**
- No wildcard `Action: "*"` or `Resource: "*"` in IAM policies
- No hardcoded credentials
- S3: `server_side_encryption_configuration` set, `block_public_acls = true`
- RDS: `storage_encrypted = true`, `deletion_protection = true`
- Security groups: no `0.0.0.0/0` on sensitive ports (22, 3306, 5432)
- `prevent_destroy = true` on stateful resources
**Operational Safety**
- Remote backend — not local state
- `lifecycle.create_before_destroy` on downtime-causing replacements
- Blast radius: list resources that will be **replaced** (not just updated)
- **High: `count.index` used as the effective identity of a resource in a list that can reorder** (e.g. `count = length(var.subnets)` where `var.subnets` is not itself a stable, order-independent structure) — inserting or removing an early element shifts every later index, causing Terraform to plan destroy+recreate for resources that didn't conceptually change
- **High: `for_each` iterating over a computed/unstable expression** (e.g. a data source result not sorted or keyed deterministically) — same destroy/recreate risk; `for_each` keys must be stable across plans. Suggested fix for both: convert to `for_each` over a map/set keyed by a stable natural identifier (name, ID) rather than a positional index.
---
### GitHub Actions Workflow
**Correctness**
- Trigger appropriate — warn on `push` to `main` without branch protection
- `needs:` graph is acyclic
**Security**
- All `uses:` actions pinned to full commit SHA
- `permissions:` block present — principle of least privilege
- No `pull_request_target` with `checkout` of PR head (injection risk)
- OIDC used instead of long-lived PAT where possible
**Operational Safety**
- `concurrency:` group defined
- `timeout-minutes:` set on long-running jobs
- **High: a job that deploys to production (matched by job name containing `deploy`/`prod`, or an environment name matching `prod*`) lacks an `environment:` block** — without `environment:`, GitHub's environment protection rules (required reviewers, wait timer, deployment branch restriction) cannot apply, so there is no human gate before a prod deploy runs
**Deprecations**
| Action | Status | Replacement |
|--------|--------|-------------|
| `actions/checkout@v2`/`v3` | Deprecated (Node 16 runner) | latest major, SHA-pinned |
| `actions/setup-node@v2`/`v3` | Deprecated (Node 16 runner) | latest major, SHA-pinned |
| `set-output` command | Removed | `$GITHUB_OUTPUT` |
---
### Helm Values
**Correctness**
- Keys match the chart schema
- `image.tag` pinned
- `ingress.hosts` and TLS entries consistent
**Security**
- No plaintext secrets — use external secrets or `secretKeyRef`
**Operational Safety**
- `resources.requests` and `resources.limits` explicit
- `replicaCount ≥ 2` for production
- HPA and PDB enabled for variable traffic
---
### Dockerfile
**Correctness**
- Base image uses specific digest or tag — not `latest`
- Multi-stage build separates build and runtime
**Security**
- `USER` sets non-root before `CMD`/`ENTRYPOINT`
- Secrets not passed as `ARG` or `ENV`
- `.dockerignore` excludes `.git`, credentials
**Operational Safety**
- `HEALTHCHECK` defined
- Base image is minimal (distroless or Alpine)
---
### Shell Script
**Correctness**
- `set -euo pipefail` at the top
- All variables quoted (`"$VAR"`)
**Security**
- No `eval` with user input
- - No `curl | bash`
+ - No downloaded script piped straight into a shell: fetch it, verify its checksum, then run it
- Credentials sourced from environment or secret manager
**Operational Safety**
- `trap` for cleanup on EXIT/ERR
- Idempotent — safe to run twice
---
## Step 7 — Handoff recommendations
| Situation | Handoff |
|-----------|---------|
| Terraform IaC security scan | `/platform-skills:checkov` |
| Helm chart scaffold or upgrade diff | `/platform-skills:helmchart` |
| Container image CVE scan | `/platform-skills:trivy` |
| Kubernetes posture against NSA/MITRE/CIS/SOC2, or a PR gate on new findings | `/platform-skills:kubescape` |
| Continuous in-cluster posture, runtime alerts, NetworkPolicies from traffic | `/platform-skills:kubescape-operator` |
| GitHub Actions workflow security audit, `zizmor.yml` policy, or CI gate | `/platform-skills:zizmor` |
| Leaked secret: live validation, blast radius, or revocation | `/platform-skills:kingfisher` |
| Flux reconciliation issue | `/platform-skills:gitops` |
| Deeper KEDA ScaledObject design or debug | `/platform-skills:keda` |
| PR diff across many changed files | `/platform-skills:pr-review` |
---
## Step 8 — Output
### Standard mode (default)
Every finding uses the same 9-field block (`ID`, `Severity`, `Confidence`, `File`, `Line`, `Problem`, `Impact`, `Suggested fix`, `Validation step`) — identical across Standard, Bot, and JSON modes; only the wrapper differs. Use `Line: N/A` when a finding isn't line-scoped (e.g. "missing PodDisruptionBudget" has no single line). `ID` stays the existing convention: `C`/`H`/`M`/`L` prefix, numbered sequentially within severity.
**Folder / repo scope:**
```
PREFLIGHT — <path> (<N> files checked)
Environment: <prod|staging|dev>
CRITICAL: <count> HIGH: <count> MEDIUM: <count> LOW: <count>
── FILE SUMMARY ──────────────────────────────────────
File Type C H M Verdict
k8s/deployment.yaml K8s workload 1 2 0 BLOCKED
k8s/helmrelease.yaml Flux HR 0 1 1 NEEDS_FIX
terraform/main.tf Terraform 0 0 2 MERGE_READY
.github/workflows/deploy.yaml GHA 0 1 0 NEEDS_FIX
(skipped: k8s/secret.yaml — binary or encrypted, cannot check)
── CRITICAL ──────────────────────────────────────────
ID: C1
Severity: Critical
Confidence: Static review
File: k8s/deployment.yaml
Line: 42
Problem: <one sentence, no line breaks>
Impact: <blast radius — what breaks and who's affected>
Suggested fix: <corrected snippet or exact command>
Validation step: <command to confirm the fix worked>
── HIGH ──────────────────────────────────────────────
ID: H1
Severity: High
Confidence: Tool verified
File: k8s/deployment.yaml
Line: 17
Problem: <one sentence>
Impact: <blast radius>
Suggested fix: <corrected snippet or exact command>
Validation step: <command to confirm the fix worked>
── MEDIUM / LOW ──────────────────────────────────────
ID: M1
Severity: Medium
Confidence: Static review
File: terraform/main.tf
Line: N/A
Problem: <one sentence>
Impact: <blast radius>
Suggested fix: <one-line fix>
Validation step: <command to confirm the fix worked>
── OVERALL VERDICT ───────────────────────────────────
BLOCKED — fix Critical findings before deploying
Rollback plan: <how to undo this change>
Validation steps:
kubectl get pods -n <ns> -w
flux get kustomizations -A
── HANDOFF ───────────────────────────────────────────
<specialised command recommendations>
```
**Single-file scope:**
```
PREFLIGHT CHECK — <file name>
Type: <detected type>
CRITICAL: <count> HIGH: <count> MEDIUM: <count> LOW: <count>
── CRITICAL ──────────────────────────────────────────
ID: C1
Severity: Critical
Confidence: Static review
File: <file name>
Line: <line or N/A>
Problem: <one sentence>
Impact: <blast radius>
Suggested fix: <corrected snippet or exact command>
Validation step: <command to confirm the fix worked>
── VERDICT ───────────────────────────────────────────
BLOCKED / NEEDS_FIX / MERGE_READY
Rollback plan: <how to undo>
Validation steps: <commands>
```
---
### Bot / PR comment mode (`--bot` flag)
```markdown
## 🔍 Platform Skills Preflight
<!-- platform-skills-preflight -->
### Result: {MERGE_READY | NEEDS_FIX | BLOCKED}
**Scope:** <path> — <N> files
**Environment:** <prod|staging|dev>
| File | Type | C | H | M | Verdict |
|------|------|---|---|---|---------|
| deployment.yaml | K8s workload | 1 | 2 | 0 | 🔴 BLOCKED |
| helmrelease.yaml | Flux HR | 0 | 1 | 1 | 🟡 NEEDS_FIX |
#### Critical issues
<!-- one subsection per Critical finding, rendered as the same 9-field block used in Standard mode: ID, Severity, Confidence, File, Line, Problem, Impact, Suggested fix, Validation step -->
#### High findings
<!-- one subsection per High finding, same 9-field block -->
#### Rollback plan
#### Validation steps
```bash
# commands to verify after apply
```
---
*Generated by [platform-skills](https://nitinjain999.github.io/platform-skills/)*
```
**Updating existing comment:**
```bash
COMMENT_ID=$(gh api repos/{owner}/{repo}/issues/{pr}/comments --paginate \
--jq '.[] | select(.body | contains("platform-skills-preflight")) | .id' | head -1)
if [ -n "$COMMENT_ID" ]; then
gh api --method PATCH "repos/{owner}/{repo}/issues/comments/$COMMENT_ID" \
--field body="$REVIEW_BODY"
else
gh pr comment {pr} --body "$REVIEW_BODY"
fi
```
**Result values:**
- `BLOCKED` — one or more Critical findings in any file
- `NEEDS_FIX` — no Critical, but one or more High findings
- `MERGE_READY` — Medium/Low only, or no findings
---
### JSON mode (`--json` flag)
`--json` output is a single JSON object, printed with no other prose before or after it — pipeable directly to `jq`:
```json
{
"verdict": "BLOCKED",
"environment": "prod",
"scope": "k8s/",
"filesChecked": 4,
"counts": { "critical": 1, "high": 2, "medium": 1, "low": 0 },
"files": [
{ "file": "k8s/deployment.yaml", "type": "K8s workload", "critical": 1, "high": 1, "medium": 0, "low": 0, "verdict": "BLOCKED" }
],
"findings": [
{
"id": "C1",
"severity": "Critical",
"confidence": "Static review",
"file": "k8s/deployment.yaml",
"line": 42,
"problem": "...",
"impact": "...",
"suggestedFix": "...",
"validationStep": "..."
}
],
"rollbackPlan": "kubectl rollout undo deployment/app -n prod",
"validationSteps": ["kubectl get pods -n prod -w", "flux get kustomizations -A"],
"handoff": ["/platform-skills:checkov"]
}
```
`line` is `null` (not the string `"N/A"`) when a finding isn't line-scoped, so consumers can type-check it.
`rollbackPlan` and `validationSteps` mirror Standard mode's aggregate rollback/validation block; `handoff` mirrors Step 7's recommendations for this scope — empty array if none apply.
If `--bot` and `--json` are both passed, `--json` silently wins (see Step 0) — the JSON object is the entire output, with no bot-mode markdown mixed in.
---
## Step 9 — Exit code (CI mode only)
Applies only when the interview was actually skipped per Step 0's rule — i.e., `--no-interview` was passed, or every question happened to already be answered by other flags (`--env` + `--focus` + `--bot`/`--json`, with Q2 omitted as specified in Step 0). Passing `--changed-only` or `--json` alone does not by itself trigger this — if the interview still ran because some question was left unanswered, the run is interactive and no exit call is made, regardless of which flags were present.
When it does apply, after printing output:
- Verdict `BLOCKED` → run `exit 1`
- Verdict `NEEDS_FIX` or `MERGE_READY` → run `exit 0`
Only `BLOCKED` (at least one Critical finding) fails the pipeline — this matches the existing verdict semantics, where `BLOCKED` is already the documented hard merge-gate.
In fully interactive mode (no flags), no exit call is made — behavior is unchanged from before this step existed.
---
## Common mistakes
- **Marking a finding `Tool verified` without actually running the tool** — the user trusts an unverified claim as if a real tool confirmed it. Always show the command and output snippet as evidence for every `Tool verified` finding; never assign the label without it.
- **Running `terraform init` as a side effect of a validate check** — preflight silently mutates `.terraform/` state or downloads providers on a read-only review. Only run `terraform validate`/`fmt -check` when `.terraform/` already exists, otherwise mark `Static review` with a note.
- **`--changed-only` silently checking nothing when not in a git repo** — the user thinks the whole folder was checked when in fact 0 files were. Print the explicit fallback note when `--changed-only` can't resolve a base ref, and run full folder-mode discovery instead.
- **Treating `NEEDS_FIX` as a CI failure** — pipelines block on High findings that don't warrant a hard gate. The exit-code contract (Step 9) only fails on `BLOCKED`.
- **`--json` and `--bot` both passed** — ambiguous which format renders. `--json` wins silently with no warning printed, keeping output strictly parseable.
- **KEDA/HPA conflict check false-negative in single-file mode** — the conflicting HPA lives in a different file never seen by this run. The conflict check only fires in folder/repo mode where both files are discovered together; single-file mode notes this limitation inline.