Skip to content

feat: add feature merging support#12

Draft
nozaq wants to merge 6 commits into
mainfrom
claude/devcontainer-feature-lint-vs3nr3
Draft

feat: add feature merging support#12
nozaq wants to merge 6 commits into
mainfrom
claude/devcontainer-feature-lint-vs3nr3

Conversation

@nozaq

@nozaq nozaq commented Jul 12, 2026

Copy link
Copy Markdown
Member

This PR adds support for merging Dev Container Features into the effective configuration before linting, enabling decolint to catch security and configuration issues that only manifest after Features are applied.

Summary

Implements the Dev Container specification's merge logic for Features, allowing decolint to lint the final effective configuration rather than just the raw devcontainer.json. This is critical for security analysis since Features can contribute privileged mode, capability additions, mounts, and other properties that may violate linting rules.

Key Changes

New Feature Package (feature/)

  • ref.go: Parses Feature references (OCI, tarball, local paths)
  • fetch.go: Fetches Feature metadata with caching and size limits
  • metadata.go: Parses devcontainer-feature.json declarations
  • oci.go: Implements OCI distribution API for registry-hosted Features
  • tarball.go: Extracts Features from tar/gzip archives
  • merge.go: Orchestrates Feature resolution and installation ordering
  • apply.go: Implements the specification's merge logic for all property types
    • Boolean OR semantics (init, privileged)
    • Union arrays (capAdd, securityOpt)
    • Object merging (containerEnv, customizations)
    • Mount deduplication by target
    • Lifecycle hook aggregation
  • Comprehensive test coverage for all merge scenarios and fetch mechanisms

CLI Integration

  • -merge-features flag to enable Feature merging during linting
  • mergeFeatures config file option for persistent enablement
  • Transform hook in linter to apply Feature merging before rule evaluation
  • E2E test fixture demonstrating security rules firing on merged configurations

Linter Enhancement

  • Transform callback support in linter for pre-rule mutations
  • Anchored offsets preserved through transformations so findings point to original source

Implementation Details

  • Features are fetched with a 30-second timeout and cached per Fetcher instance
  • Archive downloads capped at 64MB, metadata at 4MB to prevent resource exhaustion
  • Dependency resolution handles cycles with clear error messages
  • Installation order respects dependsOn, installsAfter, and overrideFeatureInstallOrder
  • User configuration always wins merge conflicts per specification
  • All merged nodes carry byte offsets pointing to the Feature reference that contributed them

https://claude.ai/code/session_01UrZdqcow5W3yq4uGMMPS8D

Dev Container Features contribute properties (containerEnv, mounts,
capAdd, securityOpt, privileged, init, customizations, and lifecycle
hooks) that the tooling merges into the effective configuration per
the specification's merge logic, so issues a Feature introduces are
invisible when only the raw devcontainer.json is linted.

Add an opt-in -merge-features flag (and a "mergeFeatures" config
member) that fetches every referenced Feature - OCI artifacts with
anonymous pull, HTTP(S) tarballs, and local paths - resolves
dependsOn recursively, merges the contributed properties into the
parsed syntax tree, and lints the result. Merged-in nodes carry the
byte offset of the referencing "features" key, so findings on them
point at the Feature reference and the usual ignore comments apply.

A Feature that cannot be fetched is a runtime error (exit code 2).
Fetched metadata is cached in memory for the duration of the run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrZdqcow5W3yq4uGMMPS8D
@nozaq nozaq changed the title Add Feature merging support to decolint feat: add feature merging support to decolint Jul 12, 2026
@nozaq nozaq changed the title feat: add feature merging support to decolint feat: add feature merging support Jul 12, 2026
claude added 5 commits July 12, 2026 23:51
Resolves the conflict from #13's Options/Config separation refactor:
MergeFeatures now merges through the new mergeConfig function (as a
boolean OR, since either the -merge-features flag or the config
file's "mergeFeatures" member can enable it) instead of the removed
manual merge in run(). runLint now reads cfg.MergeFeatures rather
than opts.MergeFeatures, since mergeConfig already folds the flag
into cfg.
…tures

Previously -merge-features and the config file's "mergeFeatures"
member merged as a boolean OR, so the CLI flag could only enable
merging, never disable it. Track whether -merge-features was
explicitly passed (via flag.Visit) so mergeConfig can distinguish
"not given" (defer to the config file) from "given as false"
(override "mergeFeatures": true), matching how -platform already
overrides the config file's "platforms" member.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrZdqcow5W3yq4uGMMPS8D
Move the "-merge-features overrides mergeFeatures" and "-platform
overrides platforms" precedence notes out of their per-flag sections
and into a single general statement in the Config file section,
instead of repeating the rule for each flag.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrZdqcow5W3yq4uGMMPS8D
Resolves the conflict from #16's os.Root-based path traversal
hardening in linter.go/linter_test.go:

- linter/linter_test.go: PR #16 rewrote this file to use the
  internal "package linter" test convention (dropping the
  linter_test/rules imports); the Transform tests added on this
  branch (flagRule, TestLintTransform) are adapted to that
  convention, unqualifying references to the now-local package.
- cmd/decolint/main.go, linter/linter.go: auto-merged cleanly;
  runLint's SetTransform wiring for -merge-features is unaffected,
  since lintPath/LintDir's os.Root confinement only changes how the
  main devcontainer.json/Feature/Template files are discovered and
  read, not the *Context passed to Transform.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrZdqcow5W3yq4uGMMPS8D
#16 routed the main devcontainer.json/Feature/Template file discovery
through os.Root to prevent path traversal, but local Feature
references resolved during -merge-features (a relative path under
"features") still went through plain os.ReadFile/os.Stat, bypassing
that protection.

Thread the same os.Root boundary through: linter.Context gains Dir
(the os.Root LintDir already confines the file to) and FileDir (the
file's own directory relative to it), populated by lintConfig and
left zero when Lint is called directly on in-memory content (as
existing tests do). feature.Fetch/Merge now take that (dir, fileDir)
pair instead of a bare baseDir string, and resolve local references
by joining fileDir with the reference and reading it through dir, so
a reference cannot escape the same boundary the referencing
devcontainer.json was itself read through.

Note this changes local Feature semantics slightly: a Feature stored
outside the confining directory (e.g. a project-root sibling of
.devcontainer, referenced from .devcontainer/devcontainer.json via
"../") no longer resolves, even though it did before #16. This
mirrors the boundary #16 already established for the file discovery
its own comment anticipated extending to Feature resolution.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrZdqcow5W3yq4uGMMPS8D
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants