decolint

module
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 2, 2026 License: MIT

README ¶

decolint

GitHub Release CI Attestation Checks

📖 Documentation — getting started, every rule, and the full reference.

decolint is a linter for Dev Container configuration files: devcontainer.json, devcontainer-feature.json, and devcontainer-template.json. It reports mistakes, container privileges, and unpinned versions that the Dev Container tooling itself accepts without a word.

Take a devcontainer.json that opens cleanly in VS Code and builds without complaint:

// .devcontainer/devcontainer.json
{
  "name": "api",
  "image": "mcr.microsoft.com/devcontainers/go:latest",
  "features": {
    "ghcr.io/devcontainers/features/docker-in-docker": {}
  },
  "runArgs": ["--privileged"],
  "mounts": [
    "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind"
  ],
  "forwardPorts": ["db:5432"]
}
$ decolint .
Config: .decolint.jsonc
Linted 1 file:
  .devcontainer/devcontainer.json (devcontainer)

.devcontainer/devcontainer.json:1:1: error: "ALL" is not set via "runArgs", leaving the container with its default Linux capabilities (require-cap-drop-all)
.devcontainer/devcontainer.json:1:1: error: "no-new-privileges" is not set via "securityOpt" or "runArgs", allowing container processes to gain additional privileges (require-no-new-privileges)
.devcontainer/devcontainer.json:1:1: error: neither "remoteUser" nor "containerUser" is set, so the container defaults to running as root (require-non-root)
.devcontainer/devcontainer.json:3:12: error: image "mcr.microsoft.com/devcontainers/go:latest" uses the "latest" tag; pin a specific version (no-image-latest)
.devcontainer/devcontainer.json:3:12: error: image "mcr.microsoft.com/devcontainers/go:latest" is not pinned by digest; add an "@sha256:..." digest (pin-image-digest)
.devcontainer/devcontainer.json:5:5: error: feature "ghcr.io/devcontainers/features/docker-in-docker" has no explicit version; pin a specific version (pin-feature-version)
.devcontainer/devcontainer.json:7:15: error: "runArgs" contains "--privileged", disabling the container's isolation from the host (no-privileged-container)
.devcontainer/devcontainer.json:9:5: error: "mounts" entry bind-mounts the Docker socket, which grants the container root-equivalent control over the host (no-docker-socket-mount)
.devcontainer/devcontainer.json:11:20: error: "forwardPorts" entry "db:5432" uses "host:port" format; Codespaces only supports a bare port number (no-host-port-format)
Found 9 errors and 0 warnings.

That run has every category and both target platforms enabled; the defaults are quieter. See Set up your project.

Why decolint

  • A devcontainer.json is container runtime configuration, and nobody reviews it that way. --privileged and a bind-mounted Docker socket read like ordinary setup lines, and they ship to every teammate and every CI run that rebuilds the container.
  • It lints what actually runs, not just what you wrote. Features and the base image contribute configuration of their own, and decolint resolves them the way the real tooling does.
  • A silently ignored property is a mistake decolint names. GitHub Codespaces drops bind mounts and rejects host:port entries without reporting anything; the container just comes up wrong.
  • It goes where your other linters already are. Findings carry a line and column, and come out as text, JSON, GitHub Actions annotations, or SARIF.
  • One static binary. No Node.js, no Docker daemon, no project dependencies.

Lint what actually runs

Your devcontainer.json is only part of the configuration. The Features it references and the base image it names contribute their own, and the tooling merges all of it before the container starts. Take a file that is careful about everything visible in it — pinned by digest, capabilities dropped, no new privileges:

// .devcontainer/devcontainer.json
{
  "name": "api",
  "image": "mcr.microsoft.com/devcontainers/go:1.24@sha256:8de3d5b3a3ce235671c7649f0b910414158a220d18cbd2714a4446cc0cc6acd3",
  "runArgs": ["--cap-drop=ALL"],
  "securityOpt": ["no-new-privileges"]
}

Nothing in it is wrong. The problems are in what it pulls in:

$ decolint --merge .
Downloading image metadata(mcr.microsoft.com/devcontainers/go:1.24@sha256:8de3d5b3a3ce235671c7649f0b910414158a220d18cbd2714a4446cc0cc6acd3)
Config: .decolint.jsonc
Linted 1 file:
  .devcontainer/devcontainer.json (devcontainer)

.devcontainer/devcontainer.json:3:3: error: "securityOpt" overrides the default seccomp profile (no-seccomp-override)
.devcontainer/devcontainer.json:3:3: error: "securityOpt" contains "seccomp=unconfined", disabling the container's syscall filtering (no-seccomp-unconfined)
.devcontainer/devcontainer.json:3:3: error: extension "golang.Go" has no explicit version; pin a specific version (pin-extension-version)
.devcontainer/devcontainer.json:3:3: error: extension "dbaeumer.vscode-eslint" has no explicit version; pin a specific version (pin-extension-version)
Found 4 errors and 0 warnings.

Not one of those is in the file above. The base image disables seccomp and installs two unpinned VS Code extensions, and that configuration reaches the container whether or not anyone reads it. decolint reports each finding at the property that pulled it in.

Turn it on with --merge, or "merge": true in your config; see Lint what actually runs for what gets resolved and what does not.

Try it

Run it against your own repository, without installing anything:

docker run --rm -v "$PWD:/workspace" ghcr.io/bare-devcontainer/decolint

Install

Download a signed binary from the releases page, or build from source:

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. The container image is published for linux/amd64 and linux/arm64. See Getting started for the image tags available and how to verify a release artifact.

What it checks

Every rule belongs to one category, which sets its severity. Only correctness runs without configuration; the rest are off until you enable them:

Category Default Rules
correctness error 13
security off 11
reproducibility off 4
style off 2

Documentation

  • Getting started — install decolint, turn on the checks you want, name the platforms you target, lint the configuration your Features and base image actually produce, and wire it into CI as annotations or SARIF.
  • Rules — every rule, with why it exists, the configuration it accepts and rejects, and the part of the Dev Container specification it comes from.
  • Reference — every flag, config file member, output format, and exit code.

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

Directories ¶

Path Synopsis
cmd
decolint command
Command decolint lints devcontainer configuration files (devcontainer.json and friends).
Command decolint lints devcontainer configuration files (devcontainer.json and friends).
docgen command
Command docgen generates the documentation site's rule pages and landing page, and the README's category summary, from rules/*.go and README.md, so none of them has to be hand-kept in sync with the others.
Command docgen generates the documentation site's rule pages and landing page, and the README's category summary, from rules/*.go and README.md, so none of them has to be hand-kept in sync with the others.
Package discovery locates the configuration files inside a devcontainer directory.
Package discovery locates the configuration files inside a devcontainer directory.
Package dockerargs reads the parts of a devcontainer.json that Docker, rather than the devcontainer tooling, gives meaning to:
Package dockerargs reads the parts of a devcontainer.json that Docker, rather than the devcontainer tooling, gives meaning to:
Package feature fetches Dev Container Features referenced by a devcontainer.json, along with the metadata carried by the "devcontainer.metadata" label of the image it names, and merges the properties they contribute into the parsed configuration, producing the effective configuration defined by the Dev Container specification's merge logic (see https://containers.dev/implementors/spec/#merge-logic).
Package feature fetches Dev Container Features referenced by a devcontainer.json, along with the metadata carried by the "devcontainer.metadata" label of the image it names, and merges the properties they contribute into the parsed configuration, producing the effective configuration defined by the Dev Container specification's merge logic (see https://containers.dev/implementors/spec/#merge-logic).
Package format implements the output formats decolint can write a lint report in: human-readable text, a JSON object, GitHub Actions workflow command annotations, and a SARIF 2.1.0 log.
Package format implements the output formats decolint can write a lint report in: human-readable text, a JSON object, GitHub Actions workflow command annotations, and a SARIF 2.1.0 log.
Package linter implements the decolint engine: it parses configuration files as HuJSON (JSONC), runs lint rules against the syntax tree, and filters findings suppressed by ignore comments.
Package linter implements the decolint engine: it parses configuration files as HuJSON (JSONC), runs lint rules against the syntax tree, and filters findings suppressed by ignore comments.
Package ocitest provides test helpers for publishing Dev Container Features to an ephemeral, in-memory OCI registry, so tests across packages can exercise the Feature fetch and merge paths against real registry round-trips without a network dependency.
Package ocitest provides test helpers for publishing Dev Container Features to an ephemeral, in-memory OCI registry, so tests across packages can exercise the Feature fetch and merge paths against real registry round-trips without a network dependency.
Package rules provides the built-in lint rules bundled with decolint.
Package rules provides the built-in lint rules bundled with decolint.
Package substitute resolves the ${...} variables in a devcontainer.json at lint time, following the reference implementation (devcontainers/cli, src/spec-common/variableSubstitution.ts) with one deliberate divergence: a variable whose value the linter cannot know resolves to the empty string instead of being deferred to container creation, so no ${...} survives substitution.
Package substitute resolves the ${...} variables in a devcontainer.json at lint time, following the reference implementation (devcontainers/cli, src/spec-common/variableSubstitution.ts) with one deliberate divergence: a variable whose value the linter cannot know resolves to the empty string instead of being deferred to container creation, so no ${...} survives substitution.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL