Skip to content
Back to skills

Gitops Audit

ASecurity

Git-to-cluster reconciliation: kubectl/helm/argocd/flux bypassing a GitOps-managed namespace, live drift, unseen drift.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 20, 2026
ai-agentsgobashreactnodeazuregitapidevopssecuritydocumentation

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 20, 2026

npx -y skills add adnanmokhtar/refract --skill gitops-audit --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Gitops Audit?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Gitops Audit
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/adnanmokhtar-gitops-audit/badge)](https://www.skillsdirectory.com/skills/adnanmokhtar-gitops-audit)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: gitops-audit
description: "Git-to-cluster reconciliation: kubectl/helm/argocd/flux bypassing a GitOps-managed namespace, live drift, unseen drift."
kind: skill
pack: devops
allowed-tools: [Read, Grep, Glob, Bash]
---

# Skill: gitops-audit

## Premise

Under Argo CD or Flux, git is meant to be the only way a change reaches the cluster. The ways that stops being true are small and look harmless:
- a hotfix script runs `kubectl scale … -n <prod>` in a namespace an Application owns: with `selfHeal` off, git no longer describes prod; with it on, the controller quietly reverts the hotfix minutes later;
- a CI step runs `kubectl set image` or `argocd app set --parameter` beside the controller, so two writers fight over one object;
- `flux suspend` in a script turns reconciliation off and nothing turns it back on;
- an Application with `selfHeal` off and no OutOfSync alert drifts for weeks, and nobody is told.

None of this is visible in the manifests the controller syncs; it lives in scripts, CI files and the controller's live state.

This skill owns `ownership.json` row `gitops-reconciliation` (§1–§3). `@deployment-engineer` (this pack) judges its rows. It does **not** own the controller's spec (prune, sync waves, `selfHeal` × an autoscaler, `ignoreDifferences`): that is infrastructure `k8s-render-scan` §4. It does not own committed Secrets: that is security.

**Every finding cites the line `<file:line>` + the CD target that owns the namespace (Application or Flux Kustomization, with its file:line) + the object the verb touches + the sync policy that decides the effect + the oracle and its tier + the fix.** A pre-flight row is a candidate. "Git is the source of truth" is never printed; the output is the class-coverage block (§ Output). Verification follows [`refute-before-report.md`](../../../../snippets/refute-before-report.md) with [`precedents/devops.md`](../../../../snippets/precedents/devops.md) (deploy § 22–28; § 23 is this skill's: a GitOps CI that only commits manifests is gated by the controller). The oracles are tier S (a two-ended static trace: the verb and the CD target that owns its namespace) and tier L (the controller's own diff); [`proof-oracles.md`](../../../../snippets/proof-oracles.md) § Infrastructure V266 routes live reconciliation drift here, and § DevOps V290 is §1's oracle (out-of-band apply).

## The rule

A candidate is a finding when all four hold (the seven gates as `precedents/devops.md` § Reading the seven gates applies them):

1. **A controller owns the target** (gates 1, 7). A CD target in this repo (or the platform repo the user names) deploys the namespace, and the object the verb touches is in that target's render. A namespace no controller manages is a `deploy-safety-scan` row, not this skill's.
2. **The verb runs where it matters** (gate 2). A CI job on the default branch, a script an operator runs against prod, a Makefile target in a runbook — not a kind cluster, not a dry run.
3. **No control already handles it** (gate 4). The script commits to git and lets the controller sync; the field is one the controller is told to ignore (`ignoreDifferences` with `RespectIgnoreDifferences=true`, which infrastructure §4a reads); the object is the controller's own install (precedent below).
4. **The oracle fired, or both ends are cited** (gate 6). The controller's diff (L) shows the delta, or the static trace names the verb line and the CD target line; without the diff the row is `[report-flagged]`.

## Boundaries

- **Controller spec** — `selfHeal` over a workload an HPA or KEDA scales (K-GITOPS-REPLICAS), `prune` over stateful kinds (K-GITOPS-PRUNE), sync waves (K-GITOPS-WAVES), `ignoreDifferences` over all of `/spec` (K-GITOPS-IGNORE-WIDE) → infrastructure `k8s-render-scan` §4 when the infrastructure pack is installed. Without it, the pre-flight's `spec ->` rows are listed `infra-class (owner pack infrastructure not installed)` and never graded here. **The `allowEmpty` polarity**, for whoever judges those rows: `automated.allowEmpty` defaults to `false`, and that default *is* the guard — an Application whose render is empty is refused instead of pruning everything it owns. The finding is an explicit `allowEmpty: true` on a pruning prod Application, never "allowEmpty is missing". `PruneLast=true` prunes after the rest is healthy; `FailOnSharedResource` and the propagation policy do not constrain pruning.
- **A committed `kind: Secret`** (base64 is not encryption) → security `iac-scan` §3 and `secret-scan-method` when the security pack is installed; an `ExternalSecret`, a `SealedSecret` or a SOPS-encrypted file is the correct pattern. Without the pack, the row is tagged `security-class (owner pack not installed)`.
- **A deploy that is not GitOps** (a script or CI job that applies to a namespace no controller manages): concurrency, environment, rollout wait → `deploy-safety-scan` §2–§4. **The CI pipeline up to publish** → `pipeline-integrity-scan`, `release-integrity-scan`.
- **Unpinned controller or chart versions** (Argo `targetRevision: HEAD`, PIN-HELM) → infrastructure `iac-state-scan` §3. **Admission policy** that would reject what the controller syncs → infrastructure `admission-policy`.

## Adapt to the codebase — the lanes

Discover every CD target by **apiVersion** (pre-flight 0); never assume `apps/`, `clusters/` or `k8s/`. A `kustomize.config.k8s.io` `Kustomization` is a build file, not a Flux object (measured: the real repo has two, and they must print nothing).

| Lane | CD target (pre-flight 0) | Namespace it owns | Live read (§2–§3, L) |
|---|---|---|---|
| **Argo CD** | `Application`, `ApplicationSet` (`argoproj.io/`) | `spec.destination.namespace` (an object's own `namespace:` wins) | `argocd app list -o wide`, `argocd app diff <app>`, `argocd app get <app> -o json` |
| **Flux** | `Kustomization` (`kustomize.toolkit.fluxcd.io/`), `HelmRelease` (`helm.toolkit.fluxcd.io/`) | `targetNamespace`, else each object's namespace; a HelmRelease its own namespace | `flux get kustomizations -A`, `flux diff kustomization <n> --path <dir>` |
| **Rancher Fleet** | `GitRepo` (`fleet.cattle.io/`) | `fleet.yaml` `namespace` / `defaultNamespace` (read) | `kubectl get bundles -A` on the named context |
| **None** | — | — | every § prints `N/A (no GitOps controller)`; imperative verbs are `deploy-safety-scan`'s |

Every live command runs **read-only**, and only against a controller context the user names (`argocd --server <named>`, `flux --kubeconfig=<named file>`). Never the default kube context: without a named context the row is `NO-DATA(controller)` with the command written out.

## Pre-flight

Run as **one** script from the repo root (bash 3.2 and zsh print the same rows). Every `rg` names its path; an rg exit 2 is a failed grep, never zero hits. Nothing is written in the repo; no cluster binary runs.

```bash
X=(--hidden -g '!.git/**' -g '!**/node_modules/**' -g '!**/{dist,build,out,target,vendor,.venv,venv,__pycache__,coverage,.next,.turbo,generated}/**'
   -g '!**/{test,tests,__tests__,spec,e2e,fixtures,__fixtures__,mocks,__mocks__,testdata,examples}/**' -g '!**/{docs,ai,.claude}/**' -g '!*.{md,mdx,snap,lock,svg,map}')
CI=(--hidden -g '.github/workflows/*.{yml,yaml}' -g '**/.gitlab-ci*.yml' -g '.gitlab/**/*.{yml,yaml}' -g '.circleci/*.{yml,yaml}' -g '**/bitbucket-pipelines.yml'
    -g '**/azure-pipelines*.yml' -g '**/Jenkinsfile*' -g '!**/{node_modules,vendor,test,tests,fixtures,__fixtures__,testdata}/**')
SH=(-g '*.{sh,bash}' -g '**/Makefile' -g '**/Justfile' -g '**/Taskfile.{yml,yaml}' -g '!**/{node_modules,vendor,test,tests,fixtures,__fixtures__,testdata,docs,.claude,.git}/**')
NC=(rg -v --pcre2 '^[^:]+:\d+:\s*(#|//)')
SCAN="${SCAN:-$(mktemp -d "${TMPDIR:-/tmp}/gitops.XXXXXX")}"; echo "SCAN=$SCAN"
echo '# 0 CD targets, by apiVersion (a kustomize.config.k8s.io Kustomization is a build file, not Flux): name, namespace it deploys, sync policy'
rg -l --pcre2 -g '*.{yml,yaml}' "${X[@]}" '^\s*apiVersion:\s*["\x27]?(argoproj\.io/|kustomize\.toolkit\.fluxcd\.io/|helm\.toolkit\.fluxcd\.io/|fleet\.cattle\.io/)' . | sort | while IFS= read -r f; do awk -f - "$f" <<'AWK'
function v(t) { sub(/^[^:]*:[ \t]*/, "", t); sub(/[ \t]+#.*$/, "", t); gsub(/^["']|["']$/, "", t); return t }
function out() { if (api ~ /fluxcd/) au = (sp == "true") ? "suspended" : "interval"
  if (k ~ /^(Application|ApplicationSet|Kustomization|HelmRelease|GitRepo)$/ && (api !~ /^argoproj/ || k ~ /^Application/))
    printf "%s:%d: CD %s %s ns=%s automated=%s selfHeal=%s prune=%s allowEmpty=%s suspend=%s path=%s\n", FILENAME, dl, k, n, (ns == "" ? (k == "HelmRelease" ? mns : "?") : ns), au, sh, pr, ae, sp, (pa == "" ? "-" : pa)
  k = ""; n = ""; ns = ""; mns = ""; au = "no"; sh = "-"; pr = "-"; ae = "-"; sp = "-"; pa = ""; dst = 0 }
BEGIN { au = "no"; sh = "-"; pr = "-"; ae = "-"; sp = "-" }
/^---/ { out(); next }
/^apiVersion:/ { api = v($0) } /^kind:/ { k = v($0); dl = FNR }
/^  name:/ && n == "" { n = v($0) } /^  namespace:/ && mns == "" { mns = v($0) }
/^[ \t]+destination:/ { dst = 1 } dst && /^[ \t]+namespace:/ { ns = v($0); dst = 0 }
/^[ \t]+targetNamespace:/ { ns = v($0) } /^[ \t]+path:/ && pa == "" { pa = v($0) }
/^[ \t]+automated:/ { au = "yes" } /^[ \t]+selfHeal:/ { sh = v($0) } /^[ \t]+prune:/ { pr = v($0) } /^[ \t]+allowEmpty:/ { ae = v($0) } /^  suspend:/ { sp = v($0) }
END { out() }
AWK
done | tee "$SCAN/cd-targets.txt"
awk '{ for (i = 1; i <= NF; i++) if ($i ~ /^ns=/) { s = substr($i, 4); if (s != "?") print s } }' "$SCAN/cd-targets.txt" | sort -u > "$SCAN/managed-ns.txt"
echo "managed namespaces: $(tr '\n' ' ' < "$SCAN/managed-ns.txt")"; [ -s "$SCAN/cd-targets.txt" ] || echo 'N/A (no GitOps controller declared in this repo): §1–§3 print N/A; an out-of-band verb is a deploy-safety-scan row'
echo '# 1 imperative verbs in CI, scripts, Makefiles: the namespace each one targets, and whether a controller manages it'
OOB='\b(?:kubectl|oc)\b[^\n|;&]*?\s(?:apply|create|edit|scale|set|patch|replace|delete|annotate|label|autoscale|rollout\s+(?:restart|undo|pause|resume))\b|\bhelm\s+(?:-\S+\s+)*(?:install|upgrade|rollback|uninstall)\b|\bargocd\s+app\s+(?:set|unset|patch|sync\b[^\n]*--(?:prune|force|local|revision))\b|\bflux\s+(?:suspend|resume)\b'
if [ ! -s "$SCAN/cd-targets.txt" ]; then echo "N/A (no GitOps controller): $(rg -c --pcre2 "${CI[@]}" "${SH[@]}" "$OOB" . | awk -F: '{ s += $NF } END { print s + 0 }') imperative lines left to deploy-safety-scan"
else rg -n --pcre2 "${CI[@]}" "${SH[@]}" "$OOB" . | "${NC[@]}" | rg -v --pcre2 '^[^:]+:\d+:\s*(?:\|\|\s*)?(?:die|echo|printf|warn|say|log|info|fail)\b' | awk -v M="$SCAN/managed-ns.txt" -v C="$SCAN/cd-targets.txt" '
  BEGIN { while ((getline l < M) > 0) m[l] = 1
          while ((getline l < C) > 0) { n = split(l, a, " "); for (i = 1; i <= n; i++) if (a[i] ~ /^ns=/) app[a[4]] = substr(a[i], 4) } }
  { l = $0; ns = "?"
    if (match(l, /create[ \t]+(namespace|ns)[ \t]+[A-Za-z0-9_.-]+/)) { ns = substr(l, RSTART, RLENGTH); sub(/.*[ \t]/, "", ns) }
    else if (match(l, /(-n|--namespace)[ =]+[A-Za-z0-9_.-]+/)) { ns = substr(l, RSTART, RLENGTH); sub(/^(-n|--namespace)[ =]+/, "", ns) }
    else if (match(l, /argocd[ \t]+app[ \t]+[a-z]+[ \t]+[A-Za-z0-9_.-]+/)) { t = substr(l, RSTART, RLENGTH); sub(/.*[ \t]/, "", t); ns = (t in app) ? app[t] : "?" }
    else if (match(l, /flux[ \t]+(suspend|resume)[ \t]+[a-z]+[ \t]+[A-Za-z0-9_.-]+/)) { t = substr(l, RSTART, RLENGTH); sub(/.*[ \t]/, "", t); ns = (t in app) ? app[t] : "?" }
    tag = (ns ~ /^(argocd|argo-cd|flux-system|kube-system)$/) ? "controller-ns" : (ns in m) ? "MANAGED" : (ns == "?") ? "unknown" : "unmanaged"
    if (l ~ /--dry-run(=client|=server)?([ \t]|$)/ && l !~ /\|[ \t]*kubectl[ \t]+apply/) tag = "dry-run"
    print tag "\tns=" ns "\t" l }' | sort; fi
echo '# 3 drift that nobody sees: selfHeal off or manual sync, joined with any OutOfSync alert (Argo metric/notification, Flux Alert)'
awk '$3 == "Application" || $3 == "ApplicationSet" { if ($7 != "selfHeal=true") print $0 "  -> NO-SELF-HEAL" }' "$SCAN/cd-targets.txt"
rg -n --pcre2 -g '*.{yml,yaml,json}' "${X[@]}" '\bargocd_app_info\b|\bsync_status\b|\bOutOfSync\b|\btrigger\.on-(sync-status-unknown|sync-failed|out-of-sync)|\bgotk_(reconcile_condition|resource_info)\b|^\s*apiVersion:\s*notification\.toolkit\.fluxcd\.io/' . | "${NC[@]}" | rg -v --pcre2 'kind:\s*Application\b' || echo 'OutOfSync alerting: none found in the repo (read the alerting repo or the controller install before a NO-ALERT row)'
echo '# routed: controller spec (infrastructure k8s-render-scan §4) and committed Secrets (security)'
awk '$0 ~ /allowEmpty=true/ || $0 ~ /prune=true/ { print "spec -> infrastructure k8s-render-scan §4: " $0 }' "$SCAN/cd-targets.txt"
rg -l --pcre2 -g '*.{yml,yaml}' "${X[@]}" '^\s*kind:\s*Secret\s*$' . | while IFS= read -r f; do rg -q --pcre2 '^\s*sops:|kind:\s*(SealedSecret|ExternalSecret)\b' "$f" || echo "secret -> security iac-scan §3 / secret-scan-method: $f"; done
echo '# 2 live drift (L tier, read-only, only on a controller context the user names; never the default kube context):'
echo '#   argocd app list -o wide · argocd app diff <app> · flux get kustomizations -A · flux diff kustomization <name> --path <dir>'
```

How to read it:
- **0** prints one `CD` row per Argo or Flux object: the namespace it deploys, `automated` (Flux: `interval`, or `suspended`), `selfHeal`, `prune`, `allowEmpty`, `path`. `ns=?` (an ApplicationSet template, a Flux Kustomization with no `targetNamespace`) means the objects' own namespaces decide: read the render of `path`.
- **1** tags each imperative line `MANAGED` (a CD target deploys that namespace), `controller-ns` (`argocd`, `flux-system`: the controller's own install), `unmanaged`, `dry-run` or `unknown` (a variable namespace: resolve it by reading the script). Message lines (`echo`, `printf`, `die "… helm upgrade …"`) are dropped.
- **3** lists Applications without `selfHeal: true`, then any OutOfSync alert source in the repo; the alert usually lives in a platform repo, so "none found" is a question, not a finding.
- **routed** rows are listed, never graded.

### Measured (2026-10-10)

| Tree | Result |
|---|---|
| Seeded sample (2 Argo Applications, 1 Flux Kustomization, 2 kustomize build files, a CI workflow, 4 scripts, a runbook in `docs/`, a committed Secret; 7 positives, 6 decoys, 3 routed) | 0.1 s, 0 rg errors; bash 3.2 and zsh identical. **7/7 positives surface**: `kubectl scale` and `kubectl annotate` into the Argo prod namespace (`selfHeal: false`: drift persists), CI `kubectl set image` into a `selfHeal: true` namespace (the controller reverts it), CI `argocd app set --parameter`, `flux suspend` with no resume, `kubectl set env` into the Flux namespace, and NO-SELF-HEAL with no OutOfSync alert. Decoys: the controller bootstrap (3 lines, `controller-ns`), a kind dev cluster (`unmanaged`), the kustomize build files (no CD row) and the `docs/` runbook (excluded). Routed: `allowEmpty: true` + `prune: true`, Flux `prune: true` → infrastructure; the base64 Secret → security |
| `benchmarks/fixtures/*` (16) | 0 CD targets, 0 imperative lines, 0 rg errors: every § `N/A (no GitOps controller)`. obs-defects' kustomize `Kustomization` prints no row |
| Real NestJS + React monorepo (read-only, `git status` clean before and after) | 0.1 s. No controller: two kustomize `Kustomization` build files and a KEDA ScaledObject print no CD row. 11 imperative lines in deploy scripts are counted and left to `deploy-safety-scan` (its UNIT rows read them). Before the N/A gate existed, 3 of the 11 were message lines (`printf` of the command, `die "helm upgrade … failed"`) — the message filter drops them on a controller-managed repo |

## The 3,000-hit path

1. **Intersect** with `MANAGED` rows: everything else is `controller-ns`, `unmanaged` or `dry-run` and is dismissed with one read each, named in the coverage block.
2. **Group** by script or CI job and by CD target (precedents § 5): one hotfix script with five verbs is one finding.
3. **Rank:** a prod CD target with `selfHeal` off first (the drift persists), then `selfHeal` on (the change is silently reverted), then the rest.
4. **Read the top 30 groups.**
5. **Run the bulk oracle once** on a named controller context: `argocd app list -o wide` settles §2 for every Application, `flux get kustomizations -A` for every Flux object.
6. **Ledger the rest** as `Live, unreviewed (k groups / n rows — <continuing command>)`. Never mark it `checked`.

## Scans for

| § | Classes | Default severity |
|---|---|---|
| 1 | Out-of-band mutation: an imperative `kubectl`/`oc`/`helm` verb, `argocd app set/patch/sync --local`, or `flux suspend` against a namespace a controller owns | H on a prod CD target, M otherwise |
| 2 | Live drift: a CD target OutOfSync or Degraded, the diff naming fields the spec does not ignore | M (H on prod when the field changes behaviour) |
| 3 | Drift nobody sees: `selfHeal` off or manual sync, and no OutOfSync alert | M |

### 1. Out-of-band mutation

```
BAD:   scripts/scale-up.sh kubectl scale deploy/api -n shop-prod --replicas=6     (Application api-prod owns shop-prod, selfHeal: false)
       cd.yml              kubectl set image deployment/web … -n shop-web          (selfHeal: true — reverted on the next reconcile)
       cd.yml              argocd app set api-prod --parameter image.tag=…          (an override that lives in the Application, not git)
       pause-workers.sh    flux suspend kustomization workers                       (no resume anywhere)
GOOD:  commit the change (an image bump, a replica patch) and let the controller sync it · `argocd app sync` with no --local/--revision
```
**Read the code:**
- Does the verb touch an object the CD target renders (same kind and name in its render)? A one-off `kubectl create job --from=cronjob/…` or a `kubectl debug` creates an untracked object: Hardening.
- Is it the controller's own install or upgrade (`controller-ns`)? That is how Argo or Flux gets there: not a finding.
- What does the sync policy make of it: `selfHeal: false` (git stops describing prod) or `true` (the change disappears)? Both are findings; the fix differs.
- For a variable namespace (`-n "$NS"`), which value does the prod invocation pass? For `flux suspend`, does the same script resume, on every exit path?
- Is this a GitOps CI that only commits manifests (precedents § 23)? Then it is the intended path.

**Finding when** a verb that writes runs against an object a controller owns, outside git. **Proof:** S — the verb's line and the CD target's line (namespace, sync policy); L — on a scratch cluster with the controller installed (isolated `--kubeconfig=<scratch>`), run the verb and read `argocd app diff` / `flux diff kustomization`: the object is OutOfSync (selfHeal off) or reverted within the reconcile interval (on). Without a scratch controller, `NO-DATA(controller)` and the row is `[report-flagged]`.

### 2. Live drift

**Read the code:** Which fields does the diff name? A field an autoscaler, VPA or cert-manager owns is the controller spec's question (infrastructure `k8s-render-scan` §4a, §4d), not drift. Did a §1 line cause it? **Finding when** a prod CD target is OutOfSync or Degraded on a field that changes behaviour, with the controller's own diff as evidence. "Probably drifted" is never a finding. **Proof:** L — `argocd app diff <app>` / `flux diff kustomization <n> --path <dir>` on the controller context the user names, read-only; without it `NO-DATA(controller)`.

### 3. Drift nobody sees

**Read the code:** Is the Application deliberately manual-sync (a prod gate a human clicks), with an alert on OutOfSync? Is the alert in a platform repo or the controller's Helm values (`argocd-notifications` triggers, a PrometheusRule on `argocd_app_info{sync_status="OutOfSync"}`, a Flux `Alert` [K?])? Do not recommend turning `selfHeal` on for an Application that is OutOfSync now: reconcile first, or self-heal enforces the wrong state. **Finding when** drift on a prod target can persist with no one told. **Proof:** S — the Application line and the alert search; L — `argocd app get <app>` sync status and the notifications config on the named context.

## Proof

**Scope: scratch kind clusters with the controller installed, through an isolated `--kubeconfig=<scratch>`, and read-only commands on a controller context the user names — never the default kube context, never a write to the user's cluster.**

| § | Oracle | Tier | V |
|---|---|---|---|
| 1 | Static trace (verb line + CD target line); controller diff on a scratch cluster after the verb | S + L | V290 |
| 2 | The controller's own diff on the named context | L | — |
| 3 | Application sync policy + alert search; sync status on the named context | S + L | — |

## Output

```
gitops-audit — lanes: argo-cd (2 Applications), flux (1 Kustomization) · controller context: none named (§2 NO-DATA) · infrastructure: installed

Findings: 2 · Unconfirmed: 4

1. scripts/scale-up.sh:4                                                  [report-with-fix]   high
   §1 — `kubectl scale deploy/api -n shop-prod --replicas=6`; Application api-prod (deploy/argocd/app-api.yaml:2)
   owns shop-prod with selfHeal: false, so git stops describing prod. Proof: static trace (S). Fix: a replica
   patch in deploy/k8s/overlays/prod committed and synced; delete the script line.

2. .github/workflows/cd.yml:11                                            [report-flagged]    high
   §1 — `kubectl set image … -n shop-web` while web-prod has selfHeal: true: the controller reverts every CI
   deploy. Unproven link: no scratch controller. Settling test: verb + `argocd app diff web-prod` on kind.

Verification: 11 candidates → 2 findings · 4 unconfirmed · 1 hardening · 0 pre-existing · 4 refuted (self-verified)

Class coverage:
  §1 out-of-band mutation   checked (10 imperative lines → 6 MANAGED; 3 controller-ns + 1 unmanaged refuted)
  §2 live drift             NO-DATA(controller) — argocd app list -o wide --server <named>
  §3 unseen drift           checked (2 Applications → 1 NO-SELF-HEAL; alerting: none in repo → Unconfirmed)
  routed                    allowEmpty + prune ×2 → infrastructure k8s-render-scan §4 · kind: Secret ×1 → security iac-scan §3
```

`[report-with-fix]`: the verb, the CD target, the sync policy and the static trace are in hand, and the fix is a commit. `[report-flagged]`: one link is unproven — the controller diff, a variable namespace, the alert in another repo. The coverage block lists all three §s on every run, with `N/A (no GitOps controller)`, `NO-DATA(controller)`, `blocked (<reason>)` or `Live, unreviewed (k groups / n rows)` as they apply.

## False positives / precedents

The verifier reads [`precedents/devops.md`](../../../../snippets/precedents/devops.md) and [`refute-before-report.md`](../../../../snippets/refute-before-report.md) § 4:
- **§ 23** a GitOps CI that only commits manifests is the intended path. **§ 24** preview and ephemeral environments. **§ 5** one finding per script, job or CD target. **§ 4** pre-existing lines on a diff run never block. **§ 41** a tool that errored is NOT-RUN.

Read rules (sample and real repo, 2026-10-10): the controller's own install into `argocd` / `flux-system` is not out-of-band; a kustomize `Kustomization` is not a Flux object; an `ExternalSecret` or `SealedSecret` holds no secret; drift on an autoscaler-owned field is the spec's question, not this skill's.

Rationalizations to reject: "it was an emergency" (commit the hotfix, or it vanishes or lingers); "selfHeal will fix it" (it also erases the hotfix); "we only suspend for a minute" (show the resume on every exit path).

## When to run

- On any diff touching an Argo or Flux object, or a `kubectl`/`helm`/`argocd`/`flux` line in a script, Makefile or CI file.
- After an incident where prod changed and no commit explains it (with a named controller context for §2).
- On every whole-repo `/devops-audit`, reporting all three §s.

## Halt conditions

- **Do not report a critical or high row as confirmed until a separate verifier has refuted it.** That verifier is not the run that found it; it applies [`refute-before-report.md`](../../../../snippets/refute-before-report.md) with `precedents/devops.md`. Under `/devops-audit` that is its verification phase; standalone, run the protocol as a second pass and tag those rows `self-verified`. List the rest as Unconfirmed, Hardening, Pre-existing or Refuted; never drop them.
- **No drift claim without the controller's own diff** (§2). No §1 row without the CD target that owns the namespace.
- **Never run a cluster command without an isolated or user-named context**; never a write. Without one, `NO-DATA(controller)` with the command written out.
- **No proposed fix may weaken reconciliation to clear a row:** no `selfHeal: false`, no `ignoreDifferences` widened, no suspended controller.
- **Do not grade the controller spec or a Secret** (§ Boundaries); list them as routed.

## Related

- `/devops-audit` dispatches this skill on an Argo or Flux object or an imperative line in a script; `/devops-fix` closes §1 rows with a commit and the controller diff.
- `@deployment-engineer` judges these rows; `deploy-safety-scan` owns imperative deploys outside GitOps; `@ci-reviewer` and `/add-ci` own the pipeline up to publish.
- Infrastructure `k8s-render-scan` §4 (controller spec), `iac-state-scan` §3 (unpinned controllers), `admission-policy`; security `iac-scan` §3 and `secret-scan-method` — each when its pack is installed.

## Sources

Scope from the batch-4 devops eval (kept-skill scope: §1–§3; spec classes to infrastructure, Secrets to security) and the infrastructure render contract (`k8s-render-scan` §4). Argo CD and Flux field semantics are from the controllers' documentation as the infrastructure research read it (2026-10-10); `allowEmpty` polarity from Argo CD's automated-sync documentation. Rows tagged `[K?]` are settled on a scratch controller before a fix relies on them. Paraphrased; no text reproduced beyond identifiers.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.