decolint

decolint is a linter for Dev Container configuration files. Following file types are supported:
- Dev Container definition (
devcontainer.json)
- Feature (
devcontainer-feature.json)
- Template (
devcontainer-template.json)
It checks for common mistakes, security issues, and best practices in these files. See Rules for the list of checks decolint performs.
Installation
decolint can be installed as a prebuilt binary, as a container image, or
from source with Go.
Prebuilt binary (Recommended)
Download a prebuilt binary from the
releases page.
Release artifacts are signed and carry build provenance; see
Verifying release artifacts.
Docker
docker run --rm -v "$PWD:/workspace" ghcr.io/bare-devcontainer/decolint [directory ...]
Images are published for linux/amd64 and linux/arm64 and tagged
latest, <major>, <major>.<minor>, and <major>.<minor>.<patch>.
They carry the same build provenance attestation as the binaries; see
Verifying release artifacts.
Install with Go
GOEXPERIMENT=jsonv2 go install github.com/bare-devcontainer/decolint/cmd/decolint@latest
GOEXPERIMENT=jsonv2 is required because decolint uses the still
experimental encoding/json/v2 standard library package.
Usage
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.json files 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.
decolint supports the following flags; run decolint -help for the full
list.
| Flag |
Description |
-platform |
comma-separated list of platforms to also lint against (see Target platforms) |
-format |
output format: text (default), json, or github (see Output formats) |
-deny-warnings |
also exit non-zero on warn-severity findings (see Exit codes) |
-config |
path to a config file overriding rule severities (see Config file) |
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
By default, findings are printed one per line, with the rule's
severity (error or warn):
.devcontainer/devcontainer.json:4:12: warn: image "ubuntu:latest" uses the "latest" tag; pin a specific version (no-image-latest)
Pass -format to select a different output format:
text (default) — the one-line-per-finding format shown above.
json — a JSON array of finding objects, for scripting:
[{"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 when
decolint is run from a GitHub Actions workflow:
::warning file=.devcontainer/devcontainer.json,line=4,col=12,title=no-image-latest::image "ubuntu:latest" uses the "latest" tag; pin a specific version
Exit codes
0 — no error-severity findings (there may still be warn
findings)
1 — at least one error-severity finding was reported
2 — an error occurred (e.g. a file could not be parsed)
Pass -deny-warnings to also fail (exit code 1) on warn-severity
findings. Exit codes are unaffected by -format.
Config file
Rule severities can be overridden per project with a JSON/JSONC config
file:
// .decolint.jsonc
{
"rules": {
"no-image-latest": "error",
"pin-image-digest": "warn",
"require-non-root": "off"
}
}
Each entry under rules overrides that rule's default severity to
error, warn, or off (see Rules for the list of rule
IDs and their defaults); rules with no entry keep their default.
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. If no -config flag is
given and neither default file exists, decolint proceeds with every
rule at its default severity.
Rules
Each rule has a default severity, either error or warn. A rule can
also be set to off to disable it entirely. Severities can be
overridden per project with a config file.
Each rule also optionally targets specific platforms (see
Target platforms); a rule with no target platform
applies to all platforms.
| ID |
Platform |
Default severity |
Description |
id-dir-mismatch |
(all) |
error |
disallow a Feature's or Template's id that does not match the name of its containing directory |
invalid-semver |
(all) |
error |
disallow a Feature's or Template's version that is not a valid semantic version |
missing-build-dockerfile |
(all) |
error |
disallow a devcontainer.json build object that is missing dockerfile |
missing-compose-service |
(all) |
error |
disallow a devcontainer.json that sets dockerComposeFile without service |
missing-container-def |
(all) |
error |
disallow a devcontainer.json that defines none of image, build, or dockerComposeFile |
missing-required-props |
(all) |
error |
disallow a Feature's or Template's metadata that is missing a required property (id, version, or name) |
missing-workspace-mount-folder |
(all) |
error |
disallow a devcontainer.json using image or build that sets only one of workspaceMount or workspaceFolder |
no-app-port |
(all) |
warn |
disallow the legacy appPort property in favor of forwardPorts |
no-cap-add-all |
(all) |
warn |
disallow granting all Linux capabilities via an ALL entry in a devcontainer.json's or Feature's capAdd property, or a --cap-add=ALL entry in a devcontainer.json's runArgs |
no-docker-socket-mount |
(all) |
warn |
disallow bind-mounting the host's Docker socket via a devcontainer.json's mounts or runArgs, which grants the container root-equivalent control over the host |
no-image-latest |
(all) |
warn |
disallow container images without an explicit tag or with the latest tag |
no-privileged-container |
(all) |
warn |
disallow running the container in privileged mode via a devcontainer.json's or Feature's privileged property, or a --privileged entry in a devcontainer.json's runArgs |
no-seccomp-unconfined |
(all) |
warn |
disallow disabling seccomp confinement via a devcontainer.json's or Feature's securityOpt property, or a --security-opt seccomp=unconfined entry in a devcontainer.json's runArgs |
pin-feature-version |
(all) |
warn |
disallow a Feature reference without an explicit version or with the latest version |
no-seccomp-override |
(all) |
off |
disallow overriding the container runtime's default seccomp profile via a devcontainer.json's or Feature's securityOpt property, or a --security-opt seccomp=... entry in a devcontainer.json's runArgs |
pin-image-digest |
(all) |
off |
disallow an image property that does not pin the image by content digest (e.g. image@sha256:...) |
require-cap-drop-all |
(all) |
off |
require an ALL entry in a devcontainer.json's capDrop property, or a --cap-drop=ALL entry in runArgs, dropping every Linux capability |
require-no-new-privileges |
(all) |
off |
require no-new-privileges to be set via a devcontainer.json's securityOpt property, or a --security-opt no-new-privileges... entry in runArgs |
require-non-root |
(all) |
off |
require remoteUser or, if unset, containerUser to be set to a non-root user |
codespaces-no-host-port-format |
codespaces |
error |
disallow host:port entries in forwardPorts and portsAttributes, which GitHub Codespaces does not support |
codespaces-no-bind-mount |
codespaces |
warn |
disallow bind type entries in mounts, which GitHub Codespaces silently ignores except for the Docker socket |
pin-extension-version |
vscode, codespaces |
warn |
disallow a customizations.vscode.extensions entry without an explicit pinned version |
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 comment
decolint-ignore-next-line — suppress findings on the next line
decolint-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
Contributing
Rules are plain Go code and new ones are easy to add; see
CONTRIBUTING.md for the development workflow and a
walkthrough of adding a rule.
License
MIT