Reference
What decolint lints
decolint [directory ...]
Each directory is detected as one of the following based on its layout, and the configuration files it contains are linted:
- Dev container definition — the
devcontainer.jsonfiles at the locations defined by the devcontainer specification:.devcontainer/devcontainer.json,.devcontainer.json, and.devcontainer/<folder>/devcontainer.json - Feature (contains
devcontainer-feature.json) — that file - Template (contains
devcontainer-template.json) — that file, plus the dev container configuration the template ships
With no arguments, the current directory is linted.
Configuration
Every linting setting can be declared in the config file. The four scalar settings additionally have a command-line flag, which overrides the config file when given:
| Setting | Config member | Flag |
|---|---|---|
| Target platforms | platforms | -platform |
| Merge features | merge | -merge |
| Deny warnings | denyWarnings | -deny-warnings |
| Output format | format | -format |
| Category severities | categories | — |
| Rule severities | rules | — |
-merge and -deny-warnings override in either direction when given
explicitly — e.g. -merge=false disables merging even if the config file sets
"merge": true. Category and rule severities are config-file only, and
color is set by the -color flag only.
The remaining flags perform a one-off action and exit; run decolint -help
for the full list:
| Flag | Action |
|---|---|
-config <path> | use the config file at <path> instead of auto-discovery |
-init | write a new .decolint.jsonc listing every rule at its default severity |
-rules | print the built-in rules as a Markdown table |
-version | print version information |
-help | print usage |
Config file
decolint looks for .decolint.jsonc, then .decolint.json, in the current
directory; the first one found is used. Pass -config <path> to use a file at
a different location instead. It is an error (exit code 2) if -config points
at a file that doesn’t exist or fails to parse, or if the config references an
unknown rule ID or category name.
categories sets the severity (error, warn, or off) of every rule in a
category at once; rules sets an individual rule’s
severity and takes precedence over its category:
// .decolint.jsonc
{
"platforms": ["vscode"],
"categories": {
"security": "error"
},
"rules": {
"no-image-latest": "error",
"pin-image-digest": "warn",
"require-non-root": "off"
}
}
localEnv maps names to the values ${localEnv:NAME} resolves to when
merging; it is also the environment Compose-file ${NAME}
interpolation reads (note the differing syntax):
{
"localEnv": { "USERPROFILE": "/home/user" }
}
The remaining members mirror their flags: platforms, merge,
denyWarnings, and format (see the Configuration table).
Target platforms
Each rule optionally targets specific platforms (vscode, codespaces); a
rule with no target platform applies to every platform and always runs. By
default, only those platform-agnostic rules run; pass -platform with a
comma-separated list to also run rules scoped to specific platforms:
decolint -platform=vscode,codespaces
Merging
The Features a
devcontainer.json references, and the base image it names, both contribute
configuration of their own, which the Dev Container tooling merges into the
effective configuration following the specification’s
merge logic. By
default decolint lints only the raw file, so an issue introduced by a Feature
or inherited from the base image (say, one that sets privileged: true or
bind-mounts the Docker socket) goes unnoticed.
Enable merging to lint the merged configuration instead:
decolint -merge
This fetches every referenced Feature and resolves the base image, including
any metadata in the base image’s
devcontainer.metadata
label. A Feature or image that cannot be fetched is an error (exit code 2).
Merging also resolves the ${...}
variables
in the devcontainer.json first, so the reference a Feature or image is
fetched by, and the values the rules see, match what the real tooling would
use:
${localEnv:NAME}(and${env:NAME}) resolves from the config file’slocalEnvmap only — decolint never reads environment variables. A name missing from the map resolves to the default in${localEnv:NAME:default}, or to the empty string.${localWorkspaceFolder}resolves to the linted directory’s absolute path,${containerWorkspaceFolder}to the configuration’sworkspaceFolder(defaulting to/workspaces/<folder name>, or/for Docker Compose); each has a...Basenamevariant.${devcontainerId}resolves to a fixed placeholder with the format of a real id, since the real value exists only once a container is created.- Anything else, including
${containerEnv:NAME}, resolves to the empty string.
A few limits apply:
- For Docker Compose,
extendsandincludeare resolved asdocker compose configwould, and later files override earlier ones. Compose interpolation uses its own bare${NAME}syntax — not devcontainer.json’s${localEnv:NAME}— but reads the same values from the config file’slocalEnvmap: an unset variable resolves to its default (${VAR:-default}) or the empty string, and a${VAR:?}requirement on an unset variable is an error. Compose profiles, theCOMPOSE_FILEenvironment variable, and.envfiles are not applied. - Registries are accessed anonymously, so a private image that cannot be pulled that way counts as a fetch failure.
Output formats
Every format reports the configuration files that were linted, including those
with no finding, alongside the findings themselves. The default text format
additionally names the config file the settings came from:
Config: .decolint.jsonc
Linted 1 file:
.devcontainer/devcontainer.json (devcontainer)
.devcontainer/devcontainer.json:4:12: warn: image "ubuntu:latest" uses the "latest" tag; pin a specific version (no-image-latest)
Found 0 errors and 1 warning.
With no config file, that first line says so and how to create one. It is
specific to text — the machine-readable formats carry the linted files but
not the config path. Written to a terminal, the report is colored by severity;
see Color.
Select a different output format to change this:
text(default) — the format shown above.json— a JSON object of the linted files and the findings, for scripting:{"files":[{"path":".devcontainer/devcontainer.json","type":"devcontainer"}],"issues":[{"path":".devcontainer/devcontainer.json","line":4,"col":12,"ruleId":"no-image-latest","message":"image \"ubuntu:latest\" uses the \"latest\" tag; pin a specific version","severity":"warn"}]}github— GitHub Actions workflow commands, so findings show up as inline annotations on pull request diffs. The linted files go into a collapsed group in the run log, so a file with no finding gets no annotation:::group::decolint Linted 1 file: .devcontainer/devcontainer.json (devcontainer) ::endgroup:: ::warning file=.devcontainer/devcontainer.json,line=4,col=12,title=no-image-latest::image "ubuntu:latest" uses the "latest" tag; pin a specific versionsarif— a SARIF 2.1.0 log, for upload to GitHub Code Scanning so findings appear as alerts in the repository’s Security tab. The log declares every rule the run had enabled, so Code Scanning resolves the alerts of a rule that ran and found nothing, while leaving those of a rule you have turned off untouched.
Every format reports paths relative to the directory decolint runs in,
whichever way the linted directory was named on the command line. A file
outside that directory is reported with its absolute path.
Color
The text format colors each finding by its severity when it is written to a
terminal, and writes plain text otherwise — so a report piped into another
command, or redirected to a file, stays free of escape sequences. The other
formats are never colored.
Pass -color to decide instead of leaving it to the terminal:
decolint -color=always | less -R # always color
decolint -color=never # never color
Those two decide on their own. Under the default -color=auto, the NO_COLOR
and FORCE_COLOR environment variables apply instead: set NO_COLOR to any
non-empty value to turn color off, or FORCE_COLOR to color output that does
not go to a terminal, such as a CI log — FORCE_COLOR=0 turns color off
instead. NO_COLOR wins over FORCE_COLOR.
Exit codes
0— noerror-severity findings (there may still bewarnfindings)1— at least oneerror-severity finding was reported2— an error occurred (e.g. a file could not be parsed)
Enable deny-warnings (the -deny-warnings flag or "denyWarnings": true) to
also fail (exit code 1) on warn-severity findings. Exit codes are unaffected
by the output format.
Rules
Every rule belongs to one category, which sets its severity unless overridden by a config file. A rule can also optionally target specific platforms (see Target platforms); a rule with no target platform applies to all platforms.
The rule reference lists
every rule by category, and each rule’s page covers why it exists, the
configuration it accepts and rejects, and the specification it is based on. Run
decolint -rules to print the same list with the severity your configuration
gives each rule.
The SARIF output links every rule it reports to its page, so the reasoning is one click away from the alert in GitHub Code Scanning.
Rule categories
Only correctness runs by default; the rest are off until enabled:
correctness(defaulterror) — the configuration is invalid or does not behave as written.security(defaultoff) — container runtime privileges and hardening.reproducibility(defaultoff) — unpinned versions or digests that make the environment change over time.style(defaultoff) — discouraged or legacy configuration that still works.
Suppressing findings
Findings can be suppressed with comments in the configuration files:
decolint-ignore-line— suppress findings on the same line, typically as a trailing commentdecolint-ignore-next-line— suppress findings on the next linedecolint-ignore-file— suppress findings in the whole file
Each directive optionally takes rule IDs, separated by commas or spaces;
omitting them suppresses all rules. Block comments (/* ... */) work the same
way.
// decolint-ignore-file no-app-port
{
// decolint-ignore-next-line no-image-latest
"image": "ubuntu:latest",
"privileged": true // decolint-ignore-line
}
Verifying release artifacts
Each release
includes a decolint-<version>-checksums.txt file listing the SHA-256
checksum of every binary, plus a
decolint-<version>-checksums.txt.sigstore.json file: a Sigstore
bundle
containing the cosign keyless signature
(signed via GitHub Actions OIDC) and its Rekor transparency log entry.
To verify a downloaded binary:
cosign verify-blob \
--bundle decolint-<version>-checksums.txt.sigstore.json \
--certificate-identity-regexp '^https://github\.com/bare-devcontainer/decolint/\.github/workflows/release\.yml@.*$' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
decolint-<version>-checksums.txt
sha256sum --ignore-missing -c decolint-<version>-checksums.txt
The first command confirms the checksums file was signed by this repository’s release workflow; the second confirms the downloaded binary matches a checksum in that file.
Each binary’s provenance can also be verified with
gh attestation verify,
using the build
provenance
attested during the release:
gh attestation verify decolint_<version>_<os>_<arch>.tar.gz \
--repo bare-devcontainer/decolint
The container image carries the same kind of attestation:
gh attestation verify oci://ghcr.io/bare-devcontainer/decolint:<version> \
--repo bare-devcontainer/decolint