#1298·cli

Feature request: First-class array input for Feature options (array option type + multiple invocations) [blocked on spec#766]

Author: ThePlenkovCreated Aug 28, 2026Updated Aug 28, 2026

Feature request: First-class array input for Feature options (array option type + multiple invocations)

Complex feature request — depends on the spec change in devcontainers/spec#766 (RFC). This issue tracks the reference CLI implementation work once the spec RFC is accepted. Related long-standing issues: spec#57 (array option type, open since 2022) and spec#44 (install a feature more than once).


Problem

The dev container spec restricts Feature option values to boolean and string. There is no array type. As a result, every Feature that needs a list (packages, extensions, tools, versions) is forced to accept a comma-separated string and split it inside install.sh.

This CLI encodes that limitation directly in its TypeScript types, which is the root cause that blocks any progress:

// src/spec-configuration/containerFeaturesConfiguration.ts
export type FeatureOption = {
    type: 'boolean';
    default?: boolean;
    description?: string;
} | {
    type: 'string';
    enum?: string[];
    default?: string;
    description?: string;
} | {
    type: 'string';
    proposals?: string[];
    default?: string;
    description?: string;
};
// src/spec-configuration/configuration.ts
export interface DevContainerFeature {
    userFeatureId: string;
    options: boolean | string | Record<string, boolean | string | undefined>;
}
// ...
features?: Record<string, string | boolean | Record<string, string | boolean>>;

FeatureOption has no 'array' variant. DevContainerFeature.options and the features map only permit boolean | string for option values — arrays are not representable, so they can never reach install.sh no matter what a user writes in devcontainer.json.

Real-world impact (shipped Features, today)

Feature Option Today Symptom
ghcr.io/rocker-org/devcontainer-features/r-packages:1 packages "cli,rlang" Comma-joined; values containing commas are unrepresentable.
ghcr.io/devcontainers/features/github-cli extensions "github/gh-copilot" Comma-joined; extension refs/args with commas break.
mwmahlberg/devcontainer-features npm-packages packages "typescript,eslint" Accepts comma or whitespace or newline — three delimiters, because the spec gives no canonical list form.

The npm-packages case is the clearest failure signal: with no array type, every Feature author invents a different delimiter. Consumers cannot reason about a list option without reading each Feature's install.sh.

This was always meant to be temporary. From spec#57, maintainer @Chuxel (2022):

Right now things in devcontainers/features are using a comma separated string as a near term workaround. Converting this into an array is pretty easy…

That workaround has now been the de facto standard for 3+ years.


Requirements (hard — no alternatives)

This is a required capability, not a nice-to-have. Comma-separated strings are not an acceptable long-term substitute (they are ambiguous, lossy, and force per-Feature delimiter conventions). The CLI must implement:

  1. array option typedevcontainer-feature.json may declare "type": "array" for an option, with default as an array and proposals/enum constraining elements.
  2. Array option values in devcontainer.json"packages": ["curl","git","jq"] must be accepted, validated, and propagated.
  3. Multiple invocations via array of option objects — a Feature value may be an array of option objects ("dotnet": [{"version":"3.1"},{"version":"6.0"}]), invoking install.sh once per element in order (resolves spec#44).
  4. Option Resolution for arrays — array values are serialized to devcontainer-features.env as a JSON array string (PACKAGES='["curl","git","jq"]'), the only delimiter-free, unambiguous encoding.
  5. Backward-compatible string→array coercion — if a string is supplied for an array-typed option: parse as JSON if it looks like a JSON array, else split on commas. Existing comma-separated Features keep working when they migrate to type: "array".
  6. CLI flag support — accept JSON array values in --override-features, and support repeated flags (--feature-option <feature>.<option> <value>) that append to an array option.

Proposed implementation

1. Type changes

src/spec-configuration/containerFeaturesConfiguration.ts — extend FeatureOption:

export type FeatureOption = {
    type: 'boolean';
    default?: boolean;
    description?: string;
} | {
    type: 'string';
    enum?: string[];
    default?: string;
    description?: string;
} | {
    type: 'string';
    proposals?: string[];
    default?: string;
    description?: string;
} | {
    type: 'array';                          // NEW
    enum?: string[];                        // constrains elements
    proposals?: string[];                   // suggests elements
    default?: (string | boolean | number)[];// array default
    description?: string;
};

src/spec-configuration/configuration.ts — widen option value and Feature value types:

export interface DevContainerFeature {
    userFeatureId: string;
    // allow arrays of primitives as option values, and array-of-option-objects as the Feature value
    options: boolean | string | (string | boolean | number)[] | Record<string, boolean | string | (string | boolean | number)[] | undefined> | Record<string, boolean | string | (string | boolean | number)[]>[];
}
// ...
features?: Record<string, string | boolean | (string | boolean | number)[] | Record<string, boolean | string | (string | boolean | number)[]> | Record<string, boolean | string | (string | boolean | number)[]>[]>;

2. Option parsing & normalization

Where feature option values are read and normalized (the getFeatureValueDefaults / option-resolution path in containerFeaturesConfiguration.ts):

  • When option.type === 'array':
    • Accept a JSON array value as-is.
    • If the supplied value is a string, attempt JSON.parse; if it yields an array, use it; otherwise split on , and trim each element. Emit a warning recommending the array form.
    • Validate each element against enum/proposals when present.
    • Default to [] when omitted and no default.

3. Option Resolution (env serialization)

In the code that writes devcontainer-features.env (<OPTION_NAME>=<value>):

  • For an array option, serialize the value with JSON.stringify(value) so the env var holds the canonical JSON array string. Example output:
    PACKAGES='["curl","git","jq"]'
    
  • This keeps a single, unambiguous source of truth. install.sh parses with jq (already ubiquitous in dev container images):
    for pkg in $(printf '%s' "$PACKAGES" | jq -r '.[]'); do apt-get install -y "$pkg"; done
    

4. Multiple invocations (array of option objects)

In the feature install layer (getFeatureLayers / getFeatureInstallWrapperScript and the surrounding orchestration):

  • When a Feature's value is an array of option objects, emit one install layer/wrapper per element, in array order, each with its own devcontainer-features.env. All invocations of a given Feature run consecutively at that Feature's position in the install order (do not interleave with other Features).

5. CLI flags

  • --override-features: already accepts a JSON blob; ensure array option values and array-of-objects Feature values parse and flow through the new types.
  • Add/confirm a repeated-flag form: --feature-option <feature>.<option> <value> appends to an array-typed option (and sets/replaces for scalar options).

6. Validation & errors

  • Reject non-array values for array-typed options (after string coercion) with a clear error naming the Feature and option.
  • Validate enum elements; report the offending element.

Test plan

  • devcontainer-feature.json with type: "array" parses and surfaces default/enum/proposals.
  • devcontainer.json with "packages": ["curl","git"] reaches install.sh as PACKAGES='["curl","git"]'.
  • String "curl,git" for an array option coerces to ["curl","git"] with a warning.
  • String '["curl","git"]' (JSON) coerces to the array without warning.
  • enum-constrained array option rejects an out-of-set element.
  • Array-of-option-objects Feature value invokes install.sh twice with distinct env vars, in order.
  • Repeated --feature-option flags append to an array option.
  • Existing comma-separated Features (e.g. r-packages) continue to work unchanged after migrating their option to type: "array".
  • JSON Schema (devContainerFeature.schema.json) validates the new shapes.

Use cases

  1. Package listsr-packages, npm-packages, github-cli extensions, Homebrew formulae: explicit arrays, values may contain commas.
  2. Multiple runtime versions — install .NET 3.1 and 6.0 (or Node 18 + 20) in one image via array-of-option-objects, without bespoke per-Feature "multi-version" options.
  3. Multi-select tool bundles — a Feature offering a curated subset of tools; with enum elements the UX can render a multi-select picker.
  4. Tool-readable config — schema validation, IntelliSense, and diffs work natively on JSON arrays; comma-strings are opaque to every tool except the splitting install.sh.

Prior art (alternative stacks)

The dev container spec is the only major dev-environment format lacking a native list type for user-supplied option values:

Stack List input Notes
Coder coder_parameter type = "list(string)"; UI multi-select/tag-select; defaults via jsonencode([...]) First-class list type. Coder's docs warn that overriding list(string) on the CLI is "tricky" (CSV+JSON quoting) and offer a YAML workaround — exactly the ambiguity this CLI should avoid by defining array semantics up front.
Nix (mkShell) Native lists packages = [ curl git jq ]; First-class; no string parsing.
Gitpod (.gitpod.yml) Native YAML arrays (tasks, ports, vscode.extensions) First-class.
Docker Compose Native YAML arrays (volumes, ports, environment) First-class.
Helm Native YAML arrays in values.yaml, iterated with range First-class.
Terraform list(string), list(any) native variable types First-class.
Dev Containers (this CLI) ❌ No array option type — comma-separated string only Outlier.

Dependencies & unblocking

  • Blocked on spec acceptance: devcontainers/spec#766 (the RFC defining array option type, Option Resolution for arrays, and array-of-option-objects).
  • Closes the long-standing workaround: spec#57 (2022) and spec#44.

Once the spec RFC lands, this issue is the implementation tracker for the reference CLI. The type changes in §1 are the minimal unblocking step; everything else follows from the spec's normative requirements.


References