From c99bfc55212279f963b3ad185ae23d2c2ab134cd Mon Sep 17 00:00:00 2001 From: Hansehart <97880342+Hansehart@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:13:52 +0000 Subject: [PATCH 1/3] feat(claude-code): add settingsJson option merged into settings.json Merge a JSON object into Claude's settings.json at postCreate (as the user, after any stateDir volume mounts), mirroring docker-in-docker's daemonJson. Install jq for the merge. Bump to 1.1.0. --- src/claude-code/devcontainer-feature.json | 7 ++++++- src/claude-code/init.sh | 16 +++++++++++++--- src/claude-code/install.sh | 9 ++++++--- test/claude-code/scenarios.json | 10 ++++++++++ test/claude-code/settings_json.sh | 12 ++++++++++++ 5 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 test/claude-code/scenarios.json create mode 100644 test/claude-code/settings_json.sh diff --git a/src/claude-code/devcontainer-feature.json b/src/claude-code/devcontainer-feature.json index a633106..cd56552 100644 --- a/src/claude-code/devcontainer-feature.json +++ b/src/claude-code/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "claude-code", - "version": "1.0.2", + "version": "1.1.0", "name": "Claude Code CLI", "description": "Installs the Claude Code CLI on a selectable channel.", "documentationURL": "https://github.com/hansehart/devcontainer-features/tree/main/src/claude-code", @@ -20,6 +20,11 @@ "type": "boolean", "default": true, "description": "Set CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 to suppress telemetry and other non-essential network calls." + }, + "settingsJson": { + "type": "string", + "default": "", + "description": "Claude settings as a JSON object, merged into settings.json." } }, "postCreateCommand": "/usr/local/share/claude-code/init.sh" diff --git a/src/claude-code/init.sh b/src/claude-code/init.sh index 31bc13a..a6db1d1 100644 --- a/src/claude-code/init.sh +++ b/src/claude-code/init.sh @@ -1,7 +1,17 @@ #!/usr/bin/env bash set -euo pipefail -# postCreate hook: create the state dir once the volume is mounted. if [ -r /etc/profile.d/claude-code.sh ]; then . /etc/profile.d/claude-code.sh; fi -[ -n "${CLAUDE_CONFIG_DIR:-}" ] || exit 0 -mkdir -p "${CLAUDE_CONFIG_DIR}" + +# Create the state dir once its volume is mounted. +if [ -n "${CLAUDE_CONFIG_DIR:-}" ]; then mkdir -p "${CLAUDE_CONFIG_DIR}"; fi + +# Merge the requested settings into settings.json (empty leaves it untouched). +req=/usr/local/share/claude-code/requested-settings.json +if [ -s "$req" ]; then + target="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json" + mkdir -p "$(dirname "$target")" + prev='{}' + if [ -f "$target" ]; then prev="$(cat "$target")"; fi + printf '%s' "$prev" | jq --argjson add "$(cat "$req")" '. + $add' > "$target" +fi diff --git a/src/claude-code/install.sh b/src/claude-code/install.sh index c480414..b6512c8 100644 --- a/src/claude-code/install.sh +++ b/src/claude-code/install.sh @@ -1,9 +1,10 @@ #!/usr/bin/env bash set -euo pipefail -# Options (uppercased by the CLI): VERSION, STATEDIR, DISABLENONESSENTIALTRAFFIC. +# Options (uppercased by the CLI): VERSION, STATEDIR, DISABLENONESSENTIALTRAFFIC, SETTINGSJSON. STATE_DIR="$STATEDIR" DISABLE_NONESSENTIAL_TRAFFIC="$DISABLENONESSENTIALTRAFFIC" +SETTINGS_JSON="$SETTINGSJSON" export DEBIAN_FRONTEND=noninteractive @@ -11,7 +12,8 @@ export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install -y --no-install-recommends \ ca-certificates \ - curl + curl \ + jq rm -rf /var/lib/apt/lists/* # Install: run the upstream installer as the dev user so claude lands in ~/.local/bin. @@ -29,9 +31,10 @@ su - "$_REMOTE_USER" -c "curl -fsSL https://claude.ai/install.sh | bash -s -- '$ } > /etc/profile.d/claude-code.sh chmod 0644 /etc/profile.d/claude-code.sh -# Hook: install the create-state-dir hook to run once at container create. +# Hook: install the run-once hook and save the requested settings for it to merge. install -d /usr/local/share/claude-code install -m 0755 "$(dirname "$0")/init.sh" /usr/local/share/claude-code/init.sh +printf '%s' "$SETTINGS_JSON" > /usr/local/share/claude-code/requested-settings.json # Verify: claude resolves on PATH (as the dev user). su - "$_REMOTE_USER" -c "claude --version" diff --git a/test/claude-code/scenarios.json b/test/claude-code/scenarios.json new file mode 100644 index 0000000..c8e47a5 --- /dev/null +++ b/test/claude-code/scenarios.json @@ -0,0 +1,10 @@ +{ + "settings_json": { + "image": "ubuntu:24.04", + "features": { + "claude-code": { + "settingsJson": "{\"env\":{\"CLAUDE_TEST\":\"ok\"}}" + } + } + } +} diff --git a/test/claude-code/settings_json.sh b/test/claude-code/settings_json.sh new file mode 100644 index 0000000..d3b97ff --- /dev/null +++ b/test/claude-code/settings_json.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e + +source dev-container-features-test-lib + +# The merge runs at postCreate, so invoke the hook here to apply the baked settings. +/usr/local/share/claude-code/init.sh + +target="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json" +check "settings merged" bash -lc "jq -e '.env.CLAUDE_TEST == \"ok\"' '$target'" + +reportResults From 3b7e90f743f828c3bf03fdbcebb1af0cee13f1c1 Mon Sep 17 00:00:00 2001 From: Hansehart <97880342+Hansehart@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:31:35 +0000 Subject: [PATCH 2/3] test(claude-code): use a real setting and escape it for the CLI Assert a real setting (includeCoAuthoredBy) instead of an invented key, and triple-escape the JSON so its quotes survive the CLI's unescaped option-value emission. --- test/claude-code/scenarios.json | 2 +- test/claude-code/settings_json.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/claude-code/scenarios.json b/test/claude-code/scenarios.json index c8e47a5..e87baf6 100644 --- a/test/claude-code/scenarios.json +++ b/test/claude-code/scenarios.json @@ -3,7 +3,7 @@ "image": "ubuntu:24.04", "features": { "claude-code": { - "settingsJson": "{\"env\":{\"CLAUDE_TEST\":\"ok\"}}" + "settingsJson": "{\\\"includeCoAuthoredBy\\\":false}" } } } diff --git a/test/claude-code/settings_json.sh b/test/claude-code/settings_json.sh index d3b97ff..8154262 100644 --- a/test/claude-code/settings_json.sh +++ b/test/claude-code/settings_json.sh @@ -7,6 +7,6 @@ source dev-container-features-test-lib /usr/local/share/claude-code/init.sh target="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json" -check "settings merged" bash -lc "jq -e '.env.CLAUDE_TEST == \"ok\"' '$target'" +check "settings merged" bash -lc "jq -e '.includeCoAuthoredBy == false' '$target'" reportResults From deb80bba033894910aa42d86211ab2236247d225 Mon Sep 17 00:00:00 2001 From: Hansehart <97880342+Hansehart@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:41:03 +0000 Subject: [PATCH 3/3] test(claude-code): assert a real IS_DEMO env setting Use a real setting (env.IS_DEMO) and check it directly with jq. --- test/claude-code/scenarios.json | 2 +- test/claude-code/settings_json.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/claude-code/scenarios.json b/test/claude-code/scenarios.json index e87baf6..2f351dd 100644 --- a/test/claude-code/scenarios.json +++ b/test/claude-code/scenarios.json @@ -3,7 +3,7 @@ "image": "ubuntu:24.04", "features": { "claude-code": { - "settingsJson": "{\\\"includeCoAuthoredBy\\\":false}" + "settingsJson": "{\\\"env\\\":{\\\"IS_DEMO\\\":\\\"1\\\"}}" } } } diff --git a/test/claude-code/settings_json.sh b/test/claude-code/settings_json.sh index 8154262..4660953 100644 --- a/test/claude-code/settings_json.sh +++ b/test/claude-code/settings_json.sh @@ -7,6 +7,6 @@ source dev-container-features-test-lib /usr/local/share/claude-code/init.sh target="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json" -check "settings merged" bash -lc "jq -e '.includeCoAuthoredBy == false' '$target'" +check "settings merged" jq -e '.env.IS_DEMO == "1"' "$target" reportResults