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 Chuck Lantz (@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:
array option type — devcontainer-feature.json may declare "type": "array" for an
option, with default as an array and proposals/enum constraining elements.
- Array option values in
devcontainer.json — "packages": ["curl","git","jq"] must be
accepted, validated, and propagated.
- 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).
- 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.
- 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".
- 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
Use cases
- Package lists —
r-packages, npm-packages, github-cli extensions, Homebrew formulae:
explicit arrays, values may contain commas.
- 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.
- Multi-select tool bundles — a Feature offering a curated subset of tools; with
enum
elements the UX can render a multi-select picker.
- 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
Feature request: First-class array input for Feature options (array option type + multiple invocations)
Problem
The dev container spec restricts Feature option values to
booleanandstring. There is noarray 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:
FeatureOptionhas no'array'variant.DevContainerFeature.optionsand thefeaturesmaponly permit
boolean | stringfor option values — arrays are not representable, so they cannever reach
install.shno matter what a user writes indevcontainer.json.Real-world impact (shipped Features, today)
ghcr.io/rocker-org/devcontainer-features/r-packages:1packages"cli,rlang"ghcr.io/devcontainers/features/github-cli"github/gh-copilot"mwmahlberg/devcontainer-featuresnpm-packagespackages"typescript,eslint"The
npm-packagescase is the clearest failure signal: with no array type, every Feature authorinvents 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 Chuck Lantz (@Chuxel) (2022):
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:
arrayoption type —devcontainer-feature.jsonmay declare"type": "array"for anoption, with
defaultas an array andproposals/enumconstraining elements.devcontainer.json—"packages": ["curl","git","jq"]must beaccepted, validated, and propagated.
objects (
"dotnet": [{"version":"3.1"},{"version":"6.0"}]), invokinginstall.shonce perelement in order (resolves spec#44).
devcontainer-features.envasa JSON array string (
PACKAGES='["curl","git","jq"]'), the only delimiter-free, unambiguousencoding.
array-typedoption: 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".--override-features, and support repeatedflags (
--feature-option <feature>.<option> <value>) that append to an array option.Proposed implementation
1. Type changes
src/spec-configuration/containerFeaturesConfiguration.ts— extendFeatureOption:src/spec-configuration/configuration.ts— widen option value and Feature value types:2. Option parsing & normalization
Where feature option values are read and normalized (the
getFeatureValueDefaults/option-resolution path in
containerFeaturesConfiguration.ts):option.type === 'array':JSON.parse; if it yields an array, use it;otherwise split on
,and trim each element. Emit a warning recommending the array form.enum/proposalswhen present.[]when omitted and nodefault.3. Option Resolution (env serialization)
In the code that writes
devcontainer-features.env(<OPTION_NAME>=<value>):arrayoption, serialize the value withJSON.stringify(value)so the env var holds thecanonical JSON array string. Example output:
install.shparses withjq(alreadyubiquitous in dev container images):
4. Multiple invocations (array of option objects)
In the feature install layer (
getFeatureLayers/getFeatureInstallWrapperScriptand thesurrounding orchestration):
in array order, each with its own
devcontainer-features.env. All invocations of a given Featurerun 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-objectsFeature values parse and flow through the new types.
--feature-option <feature>.<option> <value>appends to anarray-typed option (and sets/replaces for scalar options).
6. Validation & errors
array-typed options (after string coercion) with a clear errornaming the Feature and option.
enumelements; report the offending element.Test plan
devcontainer-feature.jsonwithtype: "array"parses and surfacesdefault/enum/proposals.devcontainer.jsonwith"packages": ["curl","git"]reachesinstall.shasPACKAGES='["curl","git"]'."curl,git"for anarrayoption coerces to["curl","git"]with a warning.'["curl","git"]'(JSON) coerces to the array without warning.enum-constrained array option rejects an out-of-set element.install.shtwice with distinct env vars, inorder.
--feature-optionflags append to an array option.r-packages) continue to work unchanged aftermigrating their option to
type: "array".devContainerFeature.schema.json) validates the new shapes.Use cases
r-packages,npm-packages,github-cliextensions, Homebrew formulae:explicit arrays, values may contain commas.
.NET 3.1and6.0(or Node18+20) in one imagevia array-of-option-objects, without bespoke per-Feature "multi-version" options.
enumelements the UX can render a multi-select picker.
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:
coder_parametertype = "list(string)"; UImulti-select/tag-select; defaults viajsonencode([...])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.mkShell)packages = [ curl git jq ];.gitpod.yml)tasks,ports,vscode.extensions)volumes,ports,environment)values.yaml, iterated withrangelist(string),list(any)native variable typesstringonlyDependencies & unblocking
(the RFC defining
arrayoption type, Option Resolution for arrays, and array-of-option-objects).(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
optionspec#57featuremore than once in adevcontainer.jsonspec#44list(string)parameters: https://coder.com/docs/admin/templates/extending-templates/parameters