What happened?
For Docker Compose–based devcontainers, customizations.vscode.extensions and customizations.vscode.settings are silently ignored — extensions are not installed and settings are not applied — when the generated devcontainer.metadata image label contains invalid JSON.
A quote-heavy lifecycle command (e.g. a postStartCommand using a single-quoted grep regex) is serialized into the devcontainer.metadata label. During the compose-override step, the label value is escaped such that every single quote ' is rewritten to \'\', which injects an illegal \' escape into the JSON. On the next up/attach, GetImageMetadataFromContainer calls json.Unmarshal, the unmarshal fails on the invalid escape, the error is logged but swallowed, and an empty metadata config is returned. MergeConfiguration then produces a merged config with no customizations, so the IDE setup step has nothing to install.
Observed CLI output:
error Error parsing image metadata: invalid character '\'' in string escape code
After this line the container starts normally, but none of the devcontainer.json extensions/settings are present in the editor.
What did you expect to happen instead?
customizations.vscode.extensions/settings declared in devcontainer.json should be applied regardless of quoting in lifecycle commands. The generated devcontainer.metadata label should always be valid JSON.
Steps to reproduce
- Create a Compose-based devcontainer (
dockerComposeFile + service in devcontainer.json).
- Add a
postStartCommand whose value contains an embedded single-quoted shell snippet (e.g. a quoted grep regex).
- Add
customizations.vscode.extensions with one or more extensions.
- Run
devpod up --ide <any web/desktop IDE>.
- Observe
Error parsing image metadata: invalid character '\'' in string escape code and that none of the declared extensions are installed.
devcontainer.json
{
"name": "compose-metadata-repro",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
"postStartCommand": "grep -qE '\"key\"\\s*:' /etc/example.conf || true",
"customizations": {
"vscode": {
"extensions": ["golang.go"]
}
}
}
Error output / logs
error Error parsing image metadata: invalid character '\'' in string escape code
Root cause
When writing the additional labels into the generated docker-compose override file, every single quote in the label value is rewritten to \'\'.
pkg/devcontainer/compose.go (in generateDockerComposeUpProject):
// Escape $ and ' to prevent substituting local environment variables!
label := regexp.MustCompile(`\$`).ReplaceAllString(v, "$$$$")
label = regexp.MustCompile(`'`).ReplaceAllString(label, `\'\'`)
This appears to be a mis-port of the devcontainers/cli reference, whose JavaScript replacement '\'\'' evaluates to '' (YAML single-quote doubling) — not the literal characters \'\'. Because devpod writes the override file with yaml.Marshal (which already handles YAML quoting), the manual single-quote escaping is unnecessary and corrupts the JSON. The $ → $$ escaping is correct and still needed for compose interpolation.
The resulting parse failure is then silently swallowed in pkg/devcontainer/metadata/metadata.go (GetImageMetadataFromContainer):
if err != nil {
log.Errorf("Error parsing image metadata: %v", err)
return &config.ImageMetadataConfig{}, nil
}
A self-contained round-trip (escape → yaml.Marshal → compose interpolation → json.Unmarshal) reproduces the exact error with the current escaping and parses cleanly once the single-quote rewrite is removed.
Impact
- Affects all IDEs that rely on
customizations (VS Code desktop, openvscode, code-server), not a single editor.
- Specific to the Compose path; the non-compose build path bakes customizations into the image differently and is unaffected.
- Silent: the only signal is one easily-missed log line; the container otherwise comes up fine, so it presents as "my extensions/settings aren't applied."
Suggested fix
- Stop rewriting single quotes when serializing the
devcontainer.metadata label, so the label is always valid JSON (the real fix).
- (Defense in depth) Do not silently swallow the unmarshal error in
GetImageMetadataFromContainer; surface it loudly and/or fall back to the parsed devcontainer.json customizations.
- Add a regression test asserting a quote-heavy
postStartCommand plus customizations.vscode.extensions round-trips through the compose override with customizations retained.
Environment
- DevPod: recent build
- Devcontainer: Compose-based (
dockerComposeFile + service)
- OS: Linux
- Provider: Docker / Podman
What happened?
For Docker Compose–based devcontainers,
customizations.vscode.extensionsandcustomizations.vscode.settingsare silently ignored — extensions are not installed and settings are not applied — when the generateddevcontainer.metadataimage label contains invalid JSON.A quote-heavy lifecycle command (e.g. a
postStartCommandusing a single-quotedgrepregex) is serialized into thedevcontainer.metadatalabel. During the compose-override step, the label value is escaped such that every single quote'is rewritten to\'\', which injects an illegal\'escape into the JSON. On the nextup/attach,GetImageMetadataFromContainercallsjson.Unmarshal, the unmarshal fails on the invalid escape, the error is logged but swallowed, and an empty metadata config is returned.MergeConfigurationthen produces a merged config with no customizations, so the IDE setup step has nothing to install.Observed CLI output:
After this line the container starts normally, but none of the
devcontainer.jsonextensions/settings are present in the editor.What did you expect to happen instead?
customizations.vscode.extensions/settingsdeclared indevcontainer.jsonshould be applied regardless of quoting in lifecycle commands. The generateddevcontainer.metadatalabel should always be valid JSON.Steps to reproduce
dockerComposeFile+serviceindevcontainer.json).postStartCommandwhose value contains an embedded single-quoted shell snippet (e.g. a quotedgrepregex).customizations.vscode.extensionswith one or more extensions.devpod up --ide <any web/desktop IDE>.Error parsing image metadata: invalid character '\'' in string escape codeand that none of the declared extensions are installed.devcontainer.json
{ "name": "compose-metadata-repro", "dockerComposeFile": "docker-compose.yml", "service": "app", "workspaceFolder": "/workspace", "postStartCommand": "grep -qE '\"key\"\\s*:' /etc/example.conf || true", "customizations": { "vscode": { "extensions": ["golang.go"] } } }Error output / logs
error Error parsing image metadata: invalid character '\'' in string escape codeRoot cause
When writing the additional labels into the generated docker-compose override file, every single quote in the label value is rewritten to
\'\'.pkg/devcontainer/compose.go(ingenerateDockerComposeUpProject):This appears to be a mis-port of the devcontainers/cli reference, whose JavaScript replacement
'\'\''evaluates to''(YAML single-quote doubling) — not the literal characters\'\'. Because devpod writes the override file withyaml.Marshal(which already handles YAML quoting), the manual single-quote escaping is unnecessary and corrupts the JSON. The$→$$escaping is correct and still needed for compose interpolation.The resulting parse failure is then silently swallowed in
pkg/devcontainer/metadata/metadata.go(GetImageMetadataFromContainer):A self-contained round-trip (escape →
yaml.Marshal→ compose interpolation →json.Unmarshal) reproduces the exact error with the current escaping and parses cleanly once the single-quote rewrite is removed.Impact
customizations(VS Code desktop, openvscode, code-server), not a single editor.Suggested fix
devcontainer.metadatalabel, so the label is always valid JSON (the real fix).GetImageMetadataFromContainer; surface it loudly and/or fall back to the parseddevcontainer.jsoncustomizations.postStartCommandpluscustomizations.vscode.extensionsround-trips through the compose override with customizations retained.Environment
dockerComposeFile+service)