Error codes
Every diagnostic the ks parser and checker emit carries exactly one code, stable forever: tools and agents match on the code, not on the message. Codes are grouped by block — KS00xx syntax, KS01xx types, KS02xx names and references, KS03xx workflow structure, KS04xx policy, KS05xx secrets, KS06xx environment, file format and CI warnings, KS07xx approval safety, KS08xx build reproducibility — with gaps left for growth.
This file is generated from the registry in crates/core/src/registry.rs; edit the registry, never this file. Regenerate with:
UPDATE_GOLDEN=1 cargo test -p ks-core --test registryContents
KS0001: unexpected syntax
Severity: error.
The parser could not make sense of a line or expression: a missing value, a misplaced token, an indentation matching no open block. The tree still round-trips the source; fix the line the span points at.
Not ok:
steps:
build: oci.image
from:Ok:
steps:
build: oci.image
from: ./DockerfileKS0002: unclosed string or interpolation
Severity: error.
A string was opened with " but never closed on its line, or a { interpolation was never closed with }. Everything up to the end of the line is treated as part of the broken piece.
Not ok:
steps:
risk: decide
ask: "How riskyOk:
steps:
risk: decide
ask: "How risky?"KS0003: invalid interpolation
Severity: error.
Inside {...} there must be a dotted name to resolve — {git.sha}, {build.digest}. Empty braces or prose leave nothing to look up.
Not ok:
steps:
risk: decide
ask: "plan {} is ready"Ok:
steps:
risk: decide
ask: "plan {git.sha} is ready"KS0004: tab in indentation
Severity: error.
A tab sits in a line's indentation. Tabs advance to editor-dependent columns, so they break block alignment; indent with spaces instead. keepshipping fmt converts them.
Not ok:
steps:
build: oci.image
from: ./DockerfileOk:
steps:
build: oci.image
from: ./DockerfileKS0005: unsupported file format
Severity: error.
The file declares a keepshipping: format this keepshipping does not read, or the header is not a format version or is not the file's first entry. A file whose meaning moved on is refused rather than reinterpreted: raise the file to a format this build reads.
Not ok:
keepshipping: 0.9
steps:
build: oci.image
from: ./DockerfileOk:
keepshipping: 0.1
steps:
build: oci.image
from: ./DockerfileKS0101: type mismatch
Severity: error.
The value an input carries does not have the type the step kind declares. Reach for an output of the right type; when the workflow produces one, the hint names it.
Not ok:
steps:
build: oci.image
from: ./Dockerfile
deploy: k8s.rollout
image: build.tagOk:
steps:
build: oci.image
from: ./Dockerfile
deploy: k8s.rollout
image: build.digestKS0102: digest pinned by value
Severity: warning.
A well-formed sha256 digest is pasted straight into the input. It pins every run to the image that existed when the file was written; referencing the build step's digest output keeps prod on exactly the image each run built.
Not ok:
steps:
build: oci.image
from: ./Dockerfile
deploy: k8s.rollout
image: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"Ok:
steps:
build: oci.image
from: ./Dockerfile
deploy: k8s.rollout
image: build.digestKS0201: unknown reference
Severity: error.
The first segment of a dotted reference names no step declared in this file and no context namespace (git, pr, run, env, ci, secrets). Check the spelling; the hint suggests the closest step name.
Not ok:
steps:
build: oci.image
from: ./Dockerfile
deploy: k8s.rollout
image: buld.digestOk:
steps:
build: oci.image
from: ./Dockerfile
deploy: k8s.rollout
image: build.digestKS0202: unknown input
Severity: error.
The step kind declares no input with this name. Check the spelling against the kind's contract; the hint suggests the closest declared input.
Not ok:
steps:
build: oci.image
from: ./Dockerfile
deploy: k8s.rollout
imagee: build.digestOk:
steps:
build: oci.image
from: ./Dockerfile
deploy: k8s.rollout
image: build.digestKS0203: unknown output
Severity: error.
The step produces no output with this name. Check the spelling against the kind's contract; for kinds whose outputs are known only at run time, read {name}.out.<name> instead.
Not ok:
steps:
build: oci.image
from: ./Dockerfile
deploy: k8s.rollout
image: build.shaOk:
steps:
build: oci.image
from: ./Dockerfile
deploy: k8s.rollout
image: build.digestKS0204: unknown answer
Severity: error.
A decide step is compared against, or tested for, an answer it does not declare under returns:. Spell one of the declared answers, or add the new one to returns:.
Not ok:
steps:
plan: tofu.plan
dir: ./infra
risk: decide
model: typesafe/jev
ask: "How risky is the plan?"
returns: low | medium | high
gate: approval
auto: risk is huge
and plan.destroys == 0Ok:
steps:
plan: tofu.plan
dir: ./infra
risk: decide
model: typesafe/jev
ask: "How risky is the plan?"
returns: low | medium | high
gate: approval
auto: risk is low
and plan.destroys == 0KS0205: unknown action
Severity: error.
An else: action must be ask @handle or show reference — the two things a human needs to decide. Anything else is not an action the language knows.
Not ok:
steps:
plan: tofu.plan
dir: ./infra
risk: decide
model: typesafe/jev
ask: "How risky is the plan?"
returns: low | high
gate: approval
auto: risk is low
and plan.destroys == 0
else: notify opsOk:
steps:
plan: tofu.plan
dir: ./infra
risk: decide
model: typesafe/jev
ask: "How risky is the plan?"
returns: low | high
gate: approval
auto: risk is low
and plan.destroys == 0
else: show risk.reasonKS0206: missing block input
Severity: error.
A block use leaves out an input the block requires. Only an input with a default may be left out — pass the value, or give the input a default so leaving it out means something. The hint suggests the closest name when the missing one is a misspelling of another.
Not ok:
steps:
build: oci.image
from: ./Dockerfile
deploy: acme/web-service@v3
image: build.digestOk:
steps:
build: oci.image
from: ./Dockerfile
deploy: acme/web-service@v3
image: build.digest
domain: api.acme.devKS0301: duplicate step
Severity: error.
Two steps share a name, so references to it are ambiguous. Merge the declarations or rename one; the label points at the first definition.
Not ok:
steps:
build: oci.image
from: ./Dockerfile
build: oci.image
from: ./DockerfileOk:
steps:
build: oci.image
from: ./DockerfileKS0302: reserved step name
Severity: error.
The step is named like a context namespace (git, pr, run, env, ci, secrets), which would make name.output references ambiguous. Rename the step.
Not ok:
steps:
git: oci.image
from: ./DockerfileOk:
steps:
build: oci.image
from: ./DockerfileKS0303: unknown trigger
Severity: error.
A trigger must be push [glob], pr [glob], tag [glob] or manual; schedule is not supported yet. Check the spelling against those kinds.
Not ok:
on: pussh main
steps:
build: oci.image
from: ./DockerfileOk:
on: push main
steps:
build: oci.image
from: ./DockerfileKS0304: environment problem
Severity: error.
An environment is referenced but not declared (env is prod, a policy action naming it), or its declaration uses an unknown property or a bad value. Declare the environment under envs: or fix the spelling.
Not ok:
envs:
prod:
ci-only: true
steps:
build: oci.image
from: ./Dockerfile
ship: k8s.rollout
image: build.digest
when: env is staggingOk:
envs:
prod:
ci-only: true
steps:
build: oci.image
from: ./Dockerfile
ship: k8s.rollout
image: build.digest
when: env is prodKS0305: cycle
Severity: error.
The steps depend on each other in a circle, so no order can run. Break the circle: the message spells the path, the span points at the reference that closes it.
Not ok:
steps:
a: decide
model: typesafe/jev
ask: "Ship it?"
input: b.answer
returns: low | high
b: decide
model: typesafe/jev
ask: "Ready?"
input: a.answer
returns: low | highOk:
steps:
a: decide
model: typesafe/jev
ask: "Ship it?"
input: b.answer
returns: low | high
b: decide
model: typesafe/jev
ask: "Ready?"
returns: low | highKS0306: blocks nest too deep
Severity: error.
Blocks use other blocks, and a nest deeper than four cannot be resolved: there is no order to expand it in, and a file that nests that deep is not worth a reader's time either. Split the innermost block in two. The examples below are one block per file, separated by ---; only the last one is checked against the rest.
Not ok:
block: a
steps:
use: b
---
block: b
steps:
use: c
---
block: c
steps:
use: d
---
block: d
steps:
use: e
---
block: e
steps:
build: oci.image
from: ./DockerfileOk:
block: a
steps:
use: b
---
block: b
steps:
use: c
---
block: c
steps:
use: d
---
block: d
steps:
build: oci.image
from: ./DockerfileKS0401: unknown policy declaration
Severity: error.
A policy: block names a subject other than agents or humans, or declares a property other than ask, can, before or never.
Not ok:
policy:
robots:
never: read secrets
steps:
build: oci.image
from: ./DockerfileOk:
policy:
agents:
never: read secrets
steps:
build: oci.image
from: ./DockerfileKS0402: unknown policy action
Severity: error.
A policy rule must name a known action — check, build, test, plan, apply, deploy, destroy, publish, read — optionally bound to an environment (deploy prod). read takes exactly one object: secrets.
Not ok:
envs:
prod:
ci-only: true
policy:
humans:
never: delete prod
steps:
build: oci.image
from: ./DockerfileOk:
envs:
prod:
ci-only: true
policy:
humans:
never: deploy prod
steps:
build: oci.image
from: ./DockerfileKS0403: policy forbids the run
Severity: error.
The run starts as an agent, and the file's policy says agents: never: read secrets — but the workflow reads a secret. The run is refused before it starts: give the run a human actor, or have the step take the value without a secret.
Not ok:
envs:
prod:
secrets: env
policy:
agents:
never: read secrets
steps:
migrate: db.migrate
url: secrets.db_urlOk:
envs:
prod:
secrets: env
policy:
agents:
never: deploy prod
steps:
migrate: db.migrate
url: secrets.db_urlKS0404: policy rule for an action no step can do
Severity: warning.
A can: or before: rule names an action class no step in this file can produce, so the rule binds nothing. It is a warning rather than an error: the rule may well be for a step kind another environment, a plugin or a future change brings in. Nothing is warned about under never: — forbidding what the file does not do is defensible policy.
Not ok:
envs:
prod:
ci-only: true
policy:
agents:
can: deploy prod
steps:
build: oci.image
from: ./DockerfileOk:
envs:
prod:
ci-only: true
policy:
agents:
can: deploy prod
steps:
build: oci.image
from: ./Dockerfile
deploy: k8s.rollout
image: build.digestKS0405: a block may not carry policy
Severity: error.
A block definition declares a policy: block of its own. A block is someone else's pipeline code: letting it ship rules into your run would let the publisher decide what constrains it. Policy belongs to the repository and to the org baseline, so move these rules to the workflow file that uses the block.
Not ok:
block: web-service
inputs:
image: oci.Digest
policy:
agents:
never: destroy prodOk:
block: web-service
inputs:
image: oci.DigestKS0501: secret in a string
Severity: error.
A secrets.* reference is interpolated into a string. Anything a string can reach — a log, an URL, an error message — can leak the secret; pass it to an input typed Secret instead.
Not ok:
steps:
risk: decide
model: typesafe/jev
ask: "Is {secrets.db_url} reachable?"
returns: yes | noOk:
steps:
risk: decide
model: typesafe/jev
ask: "Is the database reachable?"
returns: yes | noKS0502: secret shown
Severity: error.
A secrets.* reference is passed to show:, which displays it to the approver and into the run log. Show a non-secret fact instead; the secret itself stays an input.
Not ok:
steps:
build: oci.image
from: ./Dockerfile
ok: approval
show: secrets.api_key
from: @platformOk:
steps:
build: oci.image
from: ./Dockerfile
ok: approval
show: build.digest
from: @platformKS0601: optional value may be absent
Severity: warning.
The context value used here is optional — it has no value on a laptop or outside a pull request. Handle the absent case instead of assuming a value.
Not ok:
steps:
risk: decide
model: typesafe/jev
ask: "Deploy tag {git.tag}?"
returns: yes | noOk:
steps:
risk: decide
model: typesafe/jev
ask: "Deploy commit {git.sha}?"
returns: yes | noKS0602: step needs more than this context has
Severity: warning.
The step asks for something the context it would run in cannot give: CI itself, an OIDC token, or network access. Checked with --context local a ci-only: true step warns here; checked with --context ci it is clean. Run it on CI, or bound the run before the step with --until or --skip.
Not ok:
steps:
build: oci.image
from: ./Dockerfile
deploy: k8s.rollout
image: build.digest
ci-only: trueOk:
steps:
build: oci.image
from: ./Dockerfile
deploy: k8s.rollout
image: build.digestKS0603: unknown step kind
Severity: warning.
The step's kind is not in the catalog — usually a misspelling, sometimes a kind another build, a plugin or a harness registers. The file still checks, but the step's inputs are not verified, so a typo in an input name or a value of the wrong type passes silently on this step; its outputs type any, read bare or through .out.. Check the spelling (the hint suggests the closest known kind) or register the kind's schema; where CI should refuse unknown kinds, run keepshipping check --warnings-as-errors.
Not ok:
steps:
plan: tofu.plna
dir: ./infra
apply: tofu.apply
plan: plan.changesOk:
steps:
plan: tofu.plan
dir: ./infra
apply: tofu.apply
plan: plan.fileKS0610: file format is one behind
Severity: warning.
The file declares the format before the one this keepshipping writes. It still checks — the previous format stays readable until the format after next drops it — but keepshipping fmt --upgrade rewrites it mechanically, comments and all.
Not ok:
keepshipping: 0.1
steps:
build: oci.image
from: ./DockerfileOk:
keepshipping: 0.2
steps:
build: oci.image
from: ./DockerfileKS0611: file format not declared
Severity: warning.
The file declares no keepshipping: header, so it is read as the latest format this keepshipping knows. Pin it: a declared format is what tells the next breaking change what it has to migrate.
Not ok:
steps:
build: oci.image
from: ./DockerfileOk:
keepshipping: 0.1
steps:
build: oci.image
from: ./DockerfileKS0701: auto: could destroy
Severity: warning.
The auto: rule of an approval step approves without asking anyone, and it does not say what happens to a plan that destroys. Destroy goes to a human, always — the engine conjoins and plan.destroys == 0 whatever the file says — so this is a warning about the file reading as more permissive than the run is. Write the clause: a guard inside an or does not count.
Not ok:
steps:
plan: tofu.plan
dir: ./infra
risk: decide
model: typesafe/jev
ask: "How risky is this plan?"
input: plan.changes
returns: low | medium | high
review: approval
auto: risk is low ≥ 0.95Ok:
steps:
plan: tofu.plan
dir: ./infra
risk: decide
model: typesafe/jev
ask: "How risky is this plan?"
input: plan.changes
returns: low | medium | high
review: approval
auto: risk is low ≥ 0.95
and plan.destroys == 0KS0702: artifact used after an approval that does not show it
Severity: error.
An approval is a promise about particular hashes: the approver is shown the plan file and the image digests, and says yes to those. A step the file orders after the approval consumes an artifact the approval never showed, so the thing being deployed is not the thing that was approved. Add the artifact to show:, or order the step before the approval.
Not ok:
steps:
plan: tofu.plan
dir: ./infra
build: oci.image
from: ./Dockerfile
review: approval
show: plan.changes
from: @platform
deploy: k8s.rollout
image: build.digest
after: reviewOk:
steps:
plan: tofu.plan
dir: ./infra
build: oci.image
from: ./Dockerfile
review: approval
show: plan.changes, build.digest
from: @platform
deploy: k8s.rollout
image: build.digest
after: reviewKS0801: image build is not reproducible
Severity: warning.
Two independent builds only produce the same image digest when every input is pinned: base images and downloaded files by digest or checksum, package indexes by snapshot date, timestamps fixed (SOURCE_DATE_EPOCH), and the same builder version. Otherwise the digest CI reports matches your local one only because CI promotes — it reuses the digest it built rather than building again — and the file is free to drift until it does. Reported by keepshipping check --repro, which reads the Dockerfiles an oci.image step names; the examples below are Dockerfiles, not ship.ks.
Not ok:
FROM node:20
RUN apt-get update && apt-get install -y curl
ADD https://example.com/tool.tar.gz /usr/local/bin/toolOk:
FROM node:20@sha256:1a79a4d60de6718e8e5b326e338ae533b95b6e5b4c7d0b6a9b0e3e6b6c5d4e3f2
RUN sed -i s/deb.debian.org/snapshot.debian.org/ /etc/apt/sources.list && apt-get update && apt-get install -y --no-install-recommends curl
ADD --checksum=sha256:5f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f https://example.com/tool.tar.gz /usr/local/bin/tool