Dev containers, the open specification originally driven by Microsoft and now adopted across most major IDE vendors, are the dominant way to express a developer environment as code. The format is simple, the tooling is mature, and the adoption curve has been steep enough that most engineering organizations of meaningful size now have at least some teams running on it. The security baseline for these files has lagged the adoption, and most devcontainer.json configurations in the wild would not pass a careful review.
This post is the baseline. It is not aspirational; everything here is achievable with the spec and tooling as they exist in 2026.
What does base image selection look like in 2026?
The base image in a devcontainer is the foundation of the supply chain for that environment, and the choice deserves the same scrutiny as a production base image. The Microsoft Container Registry images at mcr.microsoft.com/devcontainers/* are the default for a reason: they are maintained by a team with a clear ownership story, they receive regular security updates, and they are signed. Use them as the starting point.
The pinning question is more interesting. The default devcontainer.json typically references a tag like mcr.microsoft.com/devcontainers/typescript-node:20, which floats with upstream updates. For consistency across the team, pin to a specific digest using the image field with a @sha256:... suffix. The trade-off is that pinned digests do not get security updates automatically, so you need a workflow, typically Renovate with digest pinning enabled, to open PRs when the upstream image is rebuilt. The 2024 supply chain incidents involving devcontainer base images, including the Alpine OpenSSL CVE that affected several popular bases, would have been caught by a pinning-plus-monitoring workflow but missed by either pinning alone or floating tags alone.
How should you vet devcontainer features?
The features field is the second-largest supply chain surface in a devcontainer, after the base image. Each feature is a versioned artifact published to an OCI registry, and each feature's install script runs as root during container build. The official feature set at ghcr.io/devcontainers/features/* is reasonably well-maintained and is the safe default. The third-party feature ecosystem is uneven, with the usual long tail of small publishers and abandoned projects.
The vetting questions for a third-party feature are the same as for any other dependency. Who maintains it. When was the last release. What does the install script do. Does the feature pull additional dependencies from npm, PyPI, or apt, and if so, are those dependencies on your allowlist. A common failure mode is a feature that installs a Node.js global package, which in turn pulls a tree of transitive dependencies that have never been reviewed. Pin features to a specific version, not to latest or to a major-version tag, and update them through a deliberate review process. The 2024 incident with a popular third-party feature, where an install script was modified to include a credential exfiltration step, illustrated why this matters.
What about lifecycle scripts?
The postCreateCommand, postStartCommand, onCreateCommand, and updateContentCommand fields in a devcontainer all execute arbitrary shell at various points in the container lifecycle. They run with whatever privileges the container has, which is usually root, and they have full access to the workspace and any mounted secrets. They are a common location for project-specific setup, and they are also a common location for security mistakes.
The rule worth following is that lifecycle scripts should call into checked-in scripts in the repository, not contain inline command sequences. An inline npm install && pip install -r requirements.txt && curl https://random-url.example.com/install.sh | bash is a security review failure in three places at once. A postCreateCommand: ".devcontainer/setup.sh" that references a checked-in script is reviewable, version-controlled, and consistent across the team. The script content itself still needs review, but at least it is in a place where the review can happen. Avoid curl | bash patterns entirely; if a tool needs an installer, vendor the installer or use a package manager.
How do you handle secrets in a devcontainer?
Devcontainer secrets, exposed through the secrets field in Codespaces or through environment variables in local devcontainers, are a frequent source of credential leaks. The pattern that causes problems is treating the devcontainer as a development-only environment and giving it production-grade credentials for convenience. A developer working on a feature branch should not have read access to the production database, regardless of how convenient that would make local testing.
The right pattern is a separate set of development credentials, scoped to development resources only, with revocation and rotation handled centrally. GitHub Codespaces secrets, GitHub Actions secrets, and similar platform features are appropriate for development credentials; they are not appropriate for production credentials, and the separation needs to be enforced by policy because it will not enforce itself. The 2023 GitHub blog post on the topic, and the subsequent incidents where development secrets turned out to be production secrets, established the pattern that any secret accessible to a development environment must be considered development-scoped by definition.
What about devcontainer scanning in CI?
The devcontainer is part of the supply chain inventory, and it should be scanned the same way you scan any other container artifact. Build the devcontainer in CI, generate an SBOM with Syft or equivalent, and run vulnerability scanning against it. The output goes into the same dashboard as your production image scans, with the same prioritization model.
The complication is that devcontainers are often heavier than production images, because they include tooling, debuggers, language servers, and other development conveniences that you would never put in a production base. This means the raw CVE count tends to be higher, which can distort prioritization if you treat all CVEs equally. Reachability analysis is the right tool here: a CVE in a developer-only language server is meaningfully less interesting than a CVE in the runtime that the developer is targeting, and your scanning output should reflect that difference. Without reachability filtering, devcontainer scanning produces too much noise to act on.
How Safeguard Helps
Safeguard ingests devcontainer.json files as part of the supply chain inventory and treats them with the same rigor as production manifests. Reachability analysis tells you which CVEs in the developer environment actually intersect with the code being written, filtering out the long tail of noise from developer tooling. Griffin AI evaluates each feature, base image, and lifecycle script against our TPRM scoring and zero-day feed, surfacing the configurations that warrant review. Policy gates can block prebuild promotion or Codespaces session creation when the devcontainer regresses against your security baseline. The zero-CVE image recommendations point teams at vetted bases that meet the standard, removing the choice paralysis that otherwise turns into floating tags and unpatched images.