Skip to content

Lint Rules

llms.txt lists every page for an agent

The Kubernetes lexicon ships lint rules that run during chant lint and post-synth checks that validate the serialized YAML after chant build.

Lint rules read your TypeScript source before the build.

Severity: warning | Category: correctness

Flags hardcoded namespace strings in resource constructors. Namespaces should be parameterized or derived from configuration.

// Bad — hardcoded namespace
new Deployment({ metadata: { namespace: "production" } });
// Good — parameterized
new Deployment({ metadata: { namespace: config.namespace } });

Post-synth checks run against the serialized YAML after build.

RuleDescription
WK8005Hardcoded secrets in environment variables
WK8041API keys detected in env values
WK8042Private keys in ConfigMaps or Secrets
WK8202Privileged container (privileged: true)
WK8203Writable root filesystem (readOnlyRootFilesystem not set)
WK8204Container running as root (runAsNonRoot not set)
WK8205Capabilities not dropped (drop: ["ALL"] missing)
WK8207Host network access (hostNetwork: true)
WK8208Host PID namespace (hostPID: true)
WK8209Host IPC namespace (hostIPC: true)
RuleDescription
WK8006Latest image tag or untagged image
WK8101Deployment selector doesn’t match template labels
WK8102Resource missing metadata.labels
WK8103Container missing name
WK8104Unnamed container ports
WK8105Missing imagePullPolicy
RuleDescription
WK8201Container missing resource limits
WK8301Port-serving container missing health probes (skips Jobs/CronJobs and port-less workers)
WK8302Single replica Deployment
WK8303HA Deployment without PodDisruptionBudget

Every CRD-generated class ships its spec field schema in the lexicon JSON (specSchema), so a custom resource is checked against the CRD the way the API server’s structural schema would check it, before apply. Built-in kinds are typed by the generated .d.ts and are not covered here.

RuleDescription
WK8501Custom resource spec has a field the CRD schema does not declare (with a “did you mean” suggestion)
WK8502Custom resource spec field has the wrong scalar type, or a value outside its enum

WK8503 is the consumption side of the secret provenance vocabulary: every secret reference in a pod spec (envFrom.secretRef, env[].valueFrom.secretKeyRef, secret volumes, projected secret sources, imagePullSecrets) must resolve to a Secret the same output produces: a literal Secret, an ExternalSecret target, an InfisicalSecret / InfisicalDynamicSecret managed secret reference, or a cert-manager Certificate secretName. A secret that legitimately lives outside the build (minted by a human, an operator, a just target) is declared, not suppressed: a declareSecret({ name, provenance: "referenced" | "from-provider" | "generated-once" }) declaration covering the name waives the check. References marked optional: true are skipped. There is no suppression comment; demote via lint.rules if you must.

A committed-encrypted declaration is the exception to that waiver, on purpose. The other three kinds are promises about something outside the build; declareSecret({ name, provenance: "committed-encrypted", file }) is a claim about an artifact inside it: sops ciphertext committed to the repo, which the build emits as a sidecar file for Flux to decrypt. So it joins the producer set instead, with its namespace read from the ciphertext’s own (cleartext) metadata, and namespace matching applies to it exactly as it does to a literal Secret.

WK8504 makes that claim falsifiable. It reads the emitted sidecar (the primary output never carries ciphertext) and fires when the declared file produced no file at all, when the document is not a v1 Secret of the declared name, when it has no top-level sops block, or when any data/stringData value is not ENC[...]-shaped. The last case is the one that matters: someone edits the file by hand, forgets sops -e, and commits plaintext. Only the offending key name reaches the message. A declaration that fails WK8504 produces nothing, so WK8503 fires too instead of being waived by a broken claim.

RuleDescription
WK8503Workload consumes a Secret nothing in the output produces (error; waived by a SecretProvenance declaration)
WK8504committed-encrypted declaration does not resolve — missing file, wrong metadata.name, no sops block, or an unencrypted data/stringData value (error)

Checks that read each collector config together with the workload that runs it, WK8604, which runs the otel config checks over the config in each ConfigMap, and WK8605, which checks that a workload gives a node-reading config the node. See Collector Placement Checks for the failure modes, how a config is joined to its workload, and why they go silent across build roots.

RuleDescription
WK8601A DaemonSet collector runs tail_sampling, so it samples partial traces
WK8602A multi-replica gateway runs tail_sampling, and a collector in the build sends it traces without a loadbalancing exporter routed by traceID
WK8603A DaemonSet or multi-replica collector runs the k8s_cluster receiver without a k8s_leader_elector
WK8604A collector config in a ConfigMap or an OpenTelemetryCollector’s spec.config fails the otel config checks (reported as OTEL101 to OTEL106, OTEL112 to OTEL127)
WK8605A collector config reads the node (k8sattributes filtered to its node, kubeletstats, hostmetrics with root_path, filelog), but the container running it lacks the node name variable or a host mount it reads

An operator accepts a PrometheusRule, monitor or AlertmanagerConfig that no Prometheus or Alertmanager selects and then ignores it without an error. WK8701 to WK8703 report those from the manifests in the build, by the operator’s rules: a null object selector matches nothing, {} matches all; a null namespace selector matches only the selecting resource’s own namespace. They are silent when the build has no Prometheus, ThanosRuler, PrometheusAgent or Alertmanager, since the stack is often installed separately. WK8606 runs the prometheus rule-file checks over each PrometheusRule’s spec.groups.

RuleDescription
WK8606A PrometheusRule’s groups fail the prometheus rule-file checks (reported under their PROM ids, naming the PrometheusRule)
WK8701A PrometheusRule that no Prometheus or ThanosRuler selects through ruleSelector and ruleNamespaceSelector
WK8702A ServiceMonitor, PodMonitor, Probe or ScrapeConfig that no Prometheus or PrometheusAgent selects
WK8703An AlertmanagerConfig that no Alertmanager selects
WK8704A ServiceMonitor whose selector matches no Service, or whose endpoints[].port names no port on it; the same for a PodMonitor against container ports. Needs no Prometheus in the build
WK8705A Prometheus whose serviceMonitorSelector, podMonitorSelector, probeSelector and scrapeConfigSelector are all null

Quality checks for the Argo CD composites. ARGO001/ARGO004 are declarative (source AST); ARGO002/003/005 are post-synth (cross-resource / filesystem).

RuleDescription
ARGO001Production Application enables automated prune without the argocd.chant.dev/allow-prune override
ARGO002Application.spec.project references an undeclared AppProject
ARGO003Application.spec.destination references an unregistered cluster
ARGO004ApplicationSet template doesn’t scope to a single static AppProject
ARGO005Application source.path doesn’t resolve to a directory (warn)

Quality checks for the Flux composites (FluxGitSource / FluxAppFor). FLUX001 is declarative (source AST); FLUX002/003 are post-synth (cross-resource).

RuleDescription
FLUX001GitRepository has no spec.ref pin — Flux falls back to the master branch
FLUX002Kustomization.spec.sourceRef references an undeclared source (the bootstrap flux-system repo is exempt)
FLUX003Kustomization.spec.dependsOn names a Kustomization nothing in the build declares, or itself (warn)
Terminal window
# Lint your chant project
chant lint
# Build (also runs post-synth checks)
chant build

To suppress a rule on a specific line:

// chant-disable-next-line WK8001
export const deploy = new Deployment({ metadata: { namespace: "prod" } });

To suppress globally in chant.config.ts:

export default {
lint: {
rules: {
WK8001: "off",
},
},
};

chant