Lint Rules
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
Section titled “Lint rules”Lint rules read your TypeScript source before the build.
WK8001 — Hardcoded namespace
Section titled “WK8001 — Hardcoded namespace”Severity: warning | Category: correctness
Flags hardcoded namespace strings in resource constructors. Namespaces should be parameterized or derived from configuration.
// Bad — hardcoded namespacenew Deployment({ metadata: { namespace: "production" } });
// Good — parameterizednew Deployment({ metadata: { namespace: config.namespace } });Post-synth checks
Section titled “Post-synth checks”Post-synth checks run against the serialized YAML after build.
Security
Section titled “Security”| Rule | Description |
|---|---|
| WK8005 | Hardcoded secrets in environment variables |
| WK8041 | API keys detected in env values |
| WK8042 | Private keys in ConfigMaps or Secrets |
| WK8202 | Privileged container (privileged: true) |
| WK8203 | Writable root filesystem (readOnlyRootFilesystem not set) |
| WK8204 | Container running as root (runAsNonRoot not set) |
| WK8205 | Capabilities not dropped (drop: ["ALL"] missing) |
| WK8207 | Host network access (hostNetwork: true) |
| WK8208 | Host PID namespace (hostPID: true) |
| WK8209 | Host IPC namespace (hostIPC: true) |
Best practices
Section titled “Best practices”| Rule | Description |
|---|---|
| WK8006 | Latest image tag or untagged image |
| WK8101 | Deployment selector doesn’t match template labels |
| WK8102 | Resource missing metadata.labels |
| WK8103 | Container missing name |
| WK8104 | Unnamed container ports |
| WK8105 | Missing imagePullPolicy |
Reliability
Section titled “Reliability”| Rule | Description |
|---|---|
| WK8201 | Container missing resource limits |
| WK8301 | Port-serving container missing health probes (skips Jobs/CronJobs and port-less workers) |
| WK8302 | Single replica Deployment |
| WK8303 | HA Deployment without PodDisruptionBudget |
Custom resources
Section titled “Custom resources”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.
| Rule | Description |
|---|---|
| WK8501 | Custom resource spec has a field the CRD schema does not declare (with a “did you mean” suggestion) |
| WK8502 | Custom resource spec field has the wrong scalar type, or a value outside its enum |
Secret provenance
Section titled “Secret provenance”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.
| Rule | Description |
|---|---|
| WK8503 | Workload consumes a Secret nothing in the output produces (error; waived by a SecretProvenance declaration) |
| WK8504 | committed-encrypted declaration does not resolve — missing file, wrong metadata.name, no sops block, or an unencrypted data/stringData value (error) |
OpenTelemetry Collector placement
Section titled “OpenTelemetry Collector placement”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.
| Rule | Description |
|---|---|
| WK8601 | A DaemonSet collector runs tail_sampling, so it samples partial traces |
| WK8602 | A multi-replica gateway runs tail_sampling, and a collector in the build sends it traces without a loadbalancing exporter routed by traceID |
| WK8603 | A DaemonSet or multi-replica collector runs the k8s_cluster receiver without a k8s_leader_elector |
| WK8604 | A collector config in a ConfigMap or an OpenTelemetryCollector’s spec.config fails the otel config checks (reported as OTEL101 to OTEL106, OTEL112 to OTEL127) |
| WK8605 | A 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 |
Prometheus Operator selection
Section titled “Prometheus Operator selection”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.
| Rule | Description |
|---|---|
| WK8606 | A PrometheusRule’s groups fail the prometheus rule-file checks (reported under their PROM ids, naming the PrometheusRule) |
| WK8701 | A PrometheusRule that no Prometheus or ThanosRuler selects through ruleSelector and ruleNamespaceSelector |
| WK8702 | A ServiceMonitor, PodMonitor, Probe or ScrapeConfig that no Prometheus or PrometheusAgent selects |
| WK8703 | An AlertmanagerConfig that no Alertmanager selects |
| WK8704 | A 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 |
| WK8705 | A Prometheus whose serviceMonitorSelector, podMonitorSelector, probeSelector and scrapeConfigSelector are all null |
Argo CD
Section titled “Argo CD”Quality checks for the Argo CD composites. ARGO001/ARGO004 are declarative (source AST); ARGO002/003/005 are post-synth (cross-resource / filesystem).
| Rule | Description |
|---|---|
| ARGO001 | Production Application enables automated prune without the argocd.chant.dev/allow-prune override |
| ARGO002 | Application.spec.project references an undeclared AppProject |
| ARGO003 | Application.spec.destination references an unregistered cluster |
| ARGO004 | ApplicationSet template doesn’t scope to a single static AppProject |
| ARGO005 | Application 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).
| Rule | Description |
|---|---|
| FLUX001 | GitRepository has no spec.ref pin — Flux falls back to the master branch |
| FLUX002 | Kustomization.spec.sourceRef references an undeclared source (the bootstrap flux-system repo is exempt) |
| FLUX003 | Kustomization.spec.dependsOn names a Kustomization nothing in the build declares, or itself (warn) |
Running lint
Section titled “Running lint”# Lint your chant projectchant lint
# Build (also runs post-synth checks)chant buildTo suppress a rule on a specific line:
// chant-disable-next-line WK8001export const deploy = new Deployment({ metadata: { namespace: "prod" } });To suppress globally in chant.config.ts:
export default { lint: { rules: { WK8001: "off", }, },};