From 8e61cd7f357be2712c6c6a9de6af35a6dab1ff95 Mon Sep 17 00:00:00 2001 From: Serge Gatezh <2880401+gatezh@users.noreply.github.com> Date: Wed, 16 Sep 2026 16:32:17 -0600 Subject: [PATCH 1/6] feat(devcontainers): switch to zsh, install gh upstream, widen Renovate coverage Debian trixie freezes gh at 2.46.0 (Apr 2024) and always will, so no Renovate config could have fixed it -- the install source had to change. gh now comes from the upstream .deb at 2.100.0, using the same dpkg -i idiom these images already used for local .deb packages. Renovate coverage grows from 4 packages to 11: gh, zsh-in-docker, docker, go and the hugo-bun-node musl node build join rtk, ralphex, claude-code, agent-browser, bun and hugo. All auto-merge on the existing 3-day soak. node is constrained to ^24 so major bumps stay a deliberate call. Docker is tracked with github-tags on docker/cli rather than github-releases on moby/moby: moby tags its releases "docker-v29.8.0", which extractVersion cannot parse, so every candidate would have been silently discarded -- the same trap the ralphex-fe bun rule documents. fish is replaced by zsh + oh-my-zsh + powerlevel10k (git and fzf plugins) in claude-code and the repo devcontainer, so every image now defaults to zsh. fish pulled 26 apt packages and 59 MB, including the entire Python 3.13 runtime; zsh pulls 2 and 20 MB. starship is dropped -- powerlevel10k replaces it, which also removes an unpinned "curl | sh" install. git-delta is removed from all three images. Reliability fix: the zsh-in-docker installer ran as sh -c "$(curl ...)", which exits 0 when the download fails -- shipping an image with no .zshrc and no oh-my-zsh while CI's "zsh --version" still passed. It now downloads to a file first, so a failed fetch aborts the build. Verified by building with a bogus version: the old form succeeded, the new one fails. The publish workflows now verify gh (claude-code, claude-bun) and go and docker (ralphex-fe), matching what ci.yml checks. Host-level Docker runbooks moved to the wiki upstream; the volume rename from *-fish-data-* to *-zsh-history-* still needs applying there. --- .claude/rules/devcontainer.md | 2 +- .devcontainer/Dockerfile | 58 +++++++++-------- .../claude-sandbox/devcontainer.json | 8 +-- .devcontainer/devcontainer.json | 10 +-- .github/renovate.json5 | 26 ++++++-- .github/workflows/build-claude-bun.yml | 2 +- .github/workflows/build-claude-code.yml | 8 +-- .github/workflows/ci.yml | 8 +-- claude-bun/.devcontainer/Dockerfile | 28 ++++---- claude-bun/.devcontainer/devcontainer.json | 4 +- claude-bun/README.md | 2 +- claude-code/.devcontainer/Dockerfile | 64 +++++++++++-------- .../claude-sandbox/devcontainer.json | 8 +-- claude-code/.devcontainer/devcontainer.json | 8 +-- claude-code/README.md | 25 ++++---- ...2026-07-20-renovate-tool-updates-design.md | 6 ++ hugo-bun-node/.devcontainer/Dockerfile | 1 + ralphex-fe/Dockerfile | 2 + 18 files changed, 159 insertions(+), 111 deletions(-) diff --git a/.claude/rules/devcontainer.md b/.claude/rules/devcontainer.md index 6b1a9d9..c34a46f 100644 --- a/.claude/rules/devcontainer.md +++ b/.claude/rules/devcontainer.md @@ -40,7 +40,7 @@ Common categories: `**Claude Code**`, `**Bun**`, `**Code Quality**` (OXC), `**Gi ```jsonc "settings": { - "terminal.integrated.defaultProfile.linux": "fish", + "terminal.integrated.defaultProfile.linux": "zsh", "extensions.ignoreRecommendations": true } ``` diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index d88f9e2..528ae3f 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -68,30 +68,31 @@ RUN set -eux; \ # ─── BASE ───────────────────────────────────────────────────────────────────── FROM node:24-trixie-slim AS base -ARG GIT_DELTA_VERSION=0.18.2 +# renovate: datasource=github-releases depName=cli/cli +ARG GH_VERSION=2.100.0 +# renovate: datasource=github-releases depName=deluan/zsh-in-docker +ARG ZSH_IN_DOCKER_VERSION=1.2.0 # System packages (each justified — see claude-code Dockerfile for rationale) # - ca-certificates: SSL/TLS for HTTPS connections # - curl: downloading tools and installers -# - fish: interactive shell (built-in syntax highlighting, autosuggestions, completions) -# - fzf: fuzzy finder (fish integration) -# - gh: GitHub CLI +# - fzf: fuzzy finder (zsh key-bindings + completion sourced into .zshrc) +# - gh: GitHub CLI (installed from upstream .deb below, not apt) # - git: version control # - jq: JSON processing (onboarding patch, firewall script) -# - less: pager for git delta output +# - less: pager for git and other CLI output # - sudo: privilege escalation (chown for named volumes, firewall setup) RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ curl \ - fish \ fzf \ - gh \ git \ jq \ less \ - sudo + sudo \ + zsh # npm global directory with proper permissions for node user # Pre-create /lib to prevent "ENOENT" errors during npx commands @@ -101,37 +102,42 @@ RUN mkdir -p /usr/local/share/npm-global/lib \ ENV DEVCONTAINER=true # Create workspace and config directories with proper ownership -RUN mkdir -p /workspace /home/node/.claude /home/node/.local/share/fish \ - && chown -R node:node /workspace /home/node/.claude /home/node/.local +RUN mkdir -p /workspace /home/node/.claude /home/node/.local /commandhistory \ + && chown -R node:node /workspace /home/node/.claude /home/node/.local /commandhistory WORKDIR /workspace -# Install git-delta (pinned — v0.19.0 dropped arm64 .deb) +# gh (GitHub CLI) — installed from the upstream .deb, not apt: Debian trixie +# freezes gh at 2.46.0 (Apr 2024). Version kept current by Renovate. +# Must stay AFTER the apt block: the .deb depends on git, and dpkg -i does not +# resolve dependencies — it fails loudly ("dependency problems") if git is absent. RUN ARCH=$(dpkg --print-architecture) \ - && curl -fsSL -o "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" \ - "https://github.com/dandavison/delta/releases/download/${GIT_DELTA_VERSION}/git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" \ - && dpkg -i "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" \ - && rm "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" + && curl -fsSL -o "gh_${GH_VERSION}_linux_${ARCH}.deb" \ + "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_${ARCH}.deb" \ + && dpkg -i "gh_${GH_VERSION}_linux_${ARCH}.deb" \ + && rm "gh_${GH_VERSION}_linux_${ARCH}.deb" # ── Non-root user setup ────────────────────────────────────────────────────── USER node ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global ENV PATH=$PATH:/usr/local/share/npm-global/bin -ENV SHELL=/usr/bin/fish +ENV SHELL=/usr/bin/zsh ENV EDITOR="code --wait" ENV VISUAL="code --wait" -# ── Starship prompt ────────────────────────────────────────────────────────── -USER root -SHELL ["/bin/bash", "-o", "pipefail", "-c"] -RUN curl -sS https://starship.rs/install.sh | sh -s -- --yes -SHELL ["/bin/sh", "-c"] - -USER node -RUN mkdir -p /home/node/.config/fish \ - && starship preset no-runtime-versions -o /home/node/.config/starship.toml \ - && printf '%s\n' 'set -g fish_greeting' 'starship init fish | source' > /home/node/.config/fish/config.fish +# ── zsh + oh-my-zsh + powerlevel10k ────────────────────────────────────────── +# -x skips the script's own dependency install: zsh already comes from the apt +# block above with --no-install-recommends, which also avoids dragging in +# locales-all. Still running as node (set above), so oh-my-zsh lands in /home/node. +RUN curl -fsSL -o /tmp/zsh-in-docker.sh \ + "https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh" \ + && sh /tmp/zsh-in-docker.sh \ + -p git \ + -p fzf \ + -a "export HISTFILE=/commandhistory/.zsh_history" \ + -x \ + && rm /tmp/zsh-in-docker.sh # ── Dev tools (copied from parallel download stages) ───────────────────────── # rtk (token-optimized CLI proxy) and ralphex (autonomous plan execution). diff --git a/.devcontainer/claude-sandbox/devcontainer.json b/.devcontainer/claude-sandbox/devcontainer.json index 8d7d9a2..4609482 100644 --- a/.devcontainer/claude-sandbox/devcontainer.json +++ b/.devcontainer/claude-sandbox/devcontainer.json @@ -26,9 +26,9 @@ "ms-azuretools.vscode-docker" ], "settings": { - "terminal.integrated.defaultProfile.linux": "fish", + "terminal.integrated.defaultProfile.linux": "zsh", "terminal.integrated.profiles.linux": { - "fish": { "path": "fish" }, + "zsh": { "path": "zsh" }, "bash": { "path": "bash", "icon": "terminal-bash" } }, "extensions.ignoreRecommendations": true, @@ -57,7 +57,7 @@ }, "mounts": [ "source=devcontainers-sandbox-config-${devcontainerId},target=/home/node/.claude,type=volume", - "source=devcontainers-sandbox-fish-${devcontainerId},target=/home/node/.local/share/fish,type=volume", + "source=devcontainers-sandbox-zsh-history-${devcontainerId},target=/commandhistory,type=volume", // Mount firewall script into the expected path "source=${localWorkspaceFolder}/.devcontainer/claude-sandbox/init-firewall.sh,target=/usr/local/bin/init-firewall.sh,type=bind" ], @@ -69,7 +69,7 @@ "CLAUDE_CODE_OAUTH_TOKEN": "${localEnv:CLAUDE_CODE_OAUTH_TOKEN}" }, // Plugins before firewall (network still open during postCreateCommand) - "postCreateCommand": "sudo chown -R node /home/node/.claude && bash /workspace/.devcontainer/init-plugins.sh", + "postCreateCommand": "sudo chown -R node /home/node/.claude /commandhistory && bash /workspace/.devcontainer/init-plugins.sh", // Firewall locks down the network "postStartCommand": "sudo /usr/local/bin/init-firewall.sh", "waitFor": "postStartCommand" diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index fa4b858..dd06dda 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -27,9 +27,9 @@ "ms-azuretools.vscode-docker" ], "settings": { - "terminal.integrated.defaultProfile.linux": "fish", + "terminal.integrated.defaultProfile.linux": "zsh", "terminal.integrated.profiles.linux": { - "fish": { "path": "fish" }, + "zsh": { "path": "zsh" }, "bash": { "path": "bash", "icon": "terminal-bash" } }, // Suppress extension recommendation prompts @@ -58,8 +58,8 @@ "mounts": [ // Persist Claude Code configuration between container rebuilds "source=devcontainers-claude-config-${devcontainerId},target=/home/node/.claude,type=volume", - // Persist fish shell history between container rebuilds - "source=devcontainers-fish-data-${devcontainerId},target=/home/node/.local/share/fish,type=volume" + // Persist zsh history between container rebuilds + "source=devcontainers-zsh-history-${devcontainerId},target=/commandhistory,type=volume" ], "containerEnv": { "TZ": "${localEnv:TZ:America/Edmonton}", @@ -71,6 +71,6 @@ // VS Code installs extensions, so claude commands race with the // Claude Code extension's OAuth flow and can corrupt auth state. // Run .devcontainer/init-plugins.sh manually after first login. - "postCreateCommand": "sudo chown -R node /home/node/.claude /home/node/.local/share/fish", + "postCreateCommand": "sudo chown -R node /home/node/.claude /commandhistory", "waitFor": "postCreateCommand" } diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 47f08a0..33e21fd 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -22,13 +22,14 @@ packageRules: [ { - // rtk / ralphex release tags look like "v0.43.0"; strip the leading "v" - // so the datasource version matches the bare ARG value ("0.43.0"). - matchDatasources: ['github-releases'], + // Most GitHub tags carry a leading "v" ("v0.48.0", "v29.8.1"); strip it so the + // datasource version matches the bare ARG value ("0.48.0"). Harmless for the + // ones that don't (dandavison/delta-style bare tags pass through unchanged). + matchDatasources: ['github-releases', 'github-tags'], extractVersion: '^v?(?.+)$', }, { - // Group the four tools into one PR and auto-merge once CI passes. + // Group the tracked tools into one PR and auto-merge once CI passes. // // Relies on Renovate's default platformAutomerge:true — GitHub's native // auto-merge merges on green with no second Renovate run. The previous @@ -45,8 +46,16 @@ 'umputun/ralphex', '@anthropic-ai/claude-code', 'agent-browser', + 'cli/cli', // gh — upstream .deb; apt's trixie build is frozen at 2.46.0 + 'deluan/zsh-in-docker', + 'docker/cli', // docker CLI static binary (download.docker.com). + // github-tags, NOT github-releases: moby/moby tags its + // releases 'docker-v29.8.0', which extractVersion cannot + // parse, so every candidate is silently discarded. + 'go', // ralphex-fe Go toolchain (go.dev) + 'node', // hugo-bun-node musl build; base-image node is NOT managed here ], - groupName: 'devcontainer agent tools', + groupName: 'devcontainer tools', automerge: true, // These bumps merge unreviewed and publish straight to ghcr.io, so let a @@ -73,5 +82,12 @@ automerge: true, minimumReleaseAge: '3 days', }, + { + // hugo-bun-node installs Node from the unofficial musl builds — its base + // image (oven/bun:*-alpine) ships no node at all. The image is Node 24 LTS + // by design, so major bumps stay a deliberate call, not a Renovate PR. + matchDatasources: ['node-version'], + allowedVersions: '^24', + }, ], } diff --git a/.github/workflows/build-claude-bun.yml b/.github/workflows/build-claude-bun.yml index 7001914..d71ceda 100644 --- a/.github/workflows/build-claude-bun.yml +++ b/.github/workflows/build-claude-bun.yml @@ -41,5 +41,5 @@ jobs: context: claude-bun/.devcontainer dockerfile: claude-bun/.devcontainer/Dockerfile version-tag: ${{ needs.prepare.outputs.version-tag }} - verify-command: 'bun --version' + verify-command: 'bun --version && gh --version && zsh --version' secrets: inherit diff --git a/.github/workflows/build-claude-code.yml b/.github/workflows/build-claude-code.yml index 4977ba6..9a1b488 100644 --- a/.github/workflows/build-claude-code.yml +++ b/.github/workflows/build-claude-code.yml @@ -75,19 +75,19 @@ jobs: matrix: include: - image-suffix: claude-code - verify-command: "bun --version || true && claude --version && mise --version && fish --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium" + verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium" runner: ubuntu-24.04 arch: amd64 - image-suffix: claude-code - verify-command: "bun --version || true && claude --version && mise --version && fish --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium" + verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium" runner: ubuntu-24.04-arm arch: arm64 - image-suffix: claude-code-sandbox - verify-command: "claude --version && mise --version && fish --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp" + verify-command: "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp" runner: ubuntu-24.04 arch: amd64 - image-suffix: claude-code-sandbox - verify-command: "claude --version && mise --version && fish --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp" + verify-command: "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp" runner: ubuntu-24.04-arm arch: arm64 runs-on: ${{ matrix.runner }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08e8c04..b913ee1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,19 +110,19 @@ jobs: add_image "claude-bun" \ "claude-bun/.devcontainer" \ "claude-bun/.devcontainer/Dockerfile" \ - "bun --version" + "bun --version && gh --version" fi if [ "$CHANGED_CLAUDE_CODE" = "true" ]; then add_image "claude-code" \ "claude-code/.devcontainer" \ "claude-code/.devcontainer/Dockerfile" \ - "bun --version || true && claude --version && mise --version && fish --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium" \ + "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium" \ "default" add_image "claude-code-sandbox" \ "claude-code/.devcontainer" \ "claude-code/.devcontainer/Dockerfile" \ - "claude --version && mise --version && fish --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp" \ + "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp" \ "sandbox" fi @@ -144,7 +144,7 @@ jobs: add_image "ralphex-fe" \ "ralphex-fe" \ "ralphex-fe/Dockerfile" \ - "bun --version && hugo version && /srv/ralphex --version && rtk --version" + "bun --version && hugo version && go version && docker --version && /srv/ralphex --version && rtk --version" fi if [ "$INCLUDES" = "[]" ]; then diff --git a/claude-bun/.devcontainer/Dockerfile b/claude-bun/.devcontainer/Dockerfile index cc34081..6a33881 100644 --- a/claude-bun/.devcontainer/Dockerfile +++ b/claude-bun/.devcontainer/Dockerfile @@ -6,8 +6,10 @@ ARG TZ=UTC ENV TZ="$TZ" ARG CLAUDE_CODE_VERSION=latest -ARG GIT_DELTA_VERSION=0.18.2 +# renovate: datasource=github-releases depName=deluan/zsh-in-docker ARG ZSH_IN_DOCKER_VERSION=1.2.0 +# renovate: datasource=github-releases depName=cli/cli +ARG GH_VERSION=2.100.0 # Install basic development tools and iptables/ipset RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ @@ -20,7 +22,6 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ sudo \ fzf \ zsh \ - gh \ iptables \ ipset \ iproute2 \ @@ -49,11 +50,15 @@ RUN mkdir -p /workspace /home/bun/.claude /home/bun/.bun && \ WORKDIR /workspace +# gh (GitHub CLI) — installed from the upstream .deb, not apt: Debian trixie +# freezes gh at 2.46.0 (Apr 2024). Version kept current by Renovate. +# Must stay AFTER the apt block: the .deb depends on git, and dpkg -i does not +# resolve dependencies — it fails loudly ("dependency problems") if git is absent. RUN ARCH=$(dpkg --print-architecture) && \ - curl -fsSL -o "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" \ - "https://github.com/dandavison/delta/releases/download/${GIT_DELTA_VERSION}/git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \ - dpkg -i "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \ - rm "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" + curl -fsSL -o "gh_${GH_VERSION}_linux_${ARCH}.deb" \ + "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_${ARCH}.deb" && \ + dpkg -i "gh_${GH_VERSION}_linux_${ARCH}.deb" && \ + rm "gh_${GH_VERSION}_linux_${ARCH}.deb" # Set up non-root user USER bun @@ -67,13 +72,14 @@ ENV PATH="$BUN_INSTALL/bin:$PATH" ENV SHELL=/bin/zsh # Default powerline10k theme -RUN sh -c "$(curl -fsSL https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh)" -- \ +RUN curl -fsSL -o /tmp/zsh-in-docker.sh \ + "https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh" \ + && sh /tmp/zsh-in-docker.sh \ -p git \ -p fzf \ - -a "source /usr/share/doc/fzf/examples/key-bindings.zsh" \ - -a "source /usr/share/doc/fzf/examples/completion.zsh" \ - -a "export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \ - -x + -a "export HISTFILE=/commandhistory/.bash_history" \ + -x \ + && rm /tmp/zsh-in-docker.sh # Install Claude Code globally using bun (run as root to avoid EACCES linking) USER root diff --git a/claude-bun/.devcontainer/devcontainer.json b/claude-bun/.devcontainer/devcontainer.json index ff5e2c3..30408d9 100644 --- a/claude-bun/.devcontainer/devcontainer.json +++ b/claude-bun/.devcontainer/devcontainer.json @@ -7,9 +7,7 @@ "args": { "TZ": "${localEnv:TZ:America/Edmonton}", "BUN_VERSION": "1.3.5", - "CLAUDE_CODE_VERSION": "latest", - "GIT_DELTA_VERSION": "0.18.2", - "ZSH_IN_DOCKER_VERSION": "1.2.0" + "CLAUDE_CODE_VERSION": "latest" } }, // Required for firewall configuration diff --git a/claude-bun/README.md b/claude-bun/README.md index ed97f80..b8f3730 100644 --- a/claude-bun/README.md +++ b/claude-bun/README.md @@ -7,7 +7,7 @@ Claude Code development container based on the [official Anthropic devcontainer - **Bun runtime** (slim Debian-based image) for fast JavaScript/TypeScript execution - **Claude Code CLI** pre-installed globally via `bun add -g` - **Security by design** with custom firewall restricting network access to necessary services only -- **Developer-friendly tools**: git, ZSH with Powerline10k theme, fzf, vim, nano, git-delta +- **Developer-friendly tools**: git, ZSH with Powerlevel10k theme, fzf, vim, nano - **VS Code integration** with pre-configured extensions (Claude Code, Bun, Biome, Tailwind CSS) - **Session persistence** for command history and Claude configuration between restarts - **Multi-platform support** (linux/amd64, linux/arm64) diff --git a/claude-code/.devcontainer/Dockerfile b/claude-code/.devcontainer/Dockerfile index 206824d..5d2a066 100644 --- a/claude-code/.devcontainer/Dockerfile +++ b/claude-code/.devcontainer/Dockerfile @@ -46,17 +46,19 @@ RUN set -eux; \ # ─── BASE ───────────────────────────────────────────────────────────────────── FROM node:24-trixie-slim AS base -ARG GIT_DELTA_VERSION=0.18.2 +# renovate: datasource=github-releases depName=cli/cli +ARG GH_VERSION=2.100.0 +# renovate: datasource=github-releases depName=deluan/zsh-in-docker +ARG ZSH_IN_DOCKER_VERSION=1.2.0 # System packages shared by all targets # - ca-certificates: SSL/TLS for HTTPS connections # - curl: downloading tools and installers -# - fish: interactive shell (built-in syntax highlighting, autosuggestions, completions) -# - fzf: fuzzy finder (fish integration via fzf.fish or built-in) -# - gh: GitHub CLI +# - fzf: fuzzy finder (zsh key-bindings + completion sourced into .zshrc) +# - gh: GitHub CLI (installed from upstream .deb below, not apt) # - git: version control # - jq: JSON processing (firewall script, onboarding patch) -# - less: pager for git delta output +# - less: pager for git and other CLI output # - openssh-client: provides ssh-keygen, needed for SSH-format commit signing # (gpg.format=ssh + commit.gpgsign=true, copied in via VS Code's # dev.containers.copyGitConfig); also restores ssh/ssh-add. git only @@ -70,14 +72,13 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ curl \ - fish \ fzf \ - gh \ git \ jq \ less \ openssh-client \ - sudo + sudo \ + zsh # npm global directory with proper permissions for node user # Pre-create /lib to prevent "ENOENT" errors during npx commands @@ -98,45 +99,56 @@ RUN mkdir -p /workspace/node_modules \ /workspace/packages/shared/node_modules \ /workspace/packages/database/node_modules \ /home/node/.claude \ - /home/node/.local/share/fish \ - && chown -R node:node /workspace /home/node/.claude /home/node/.local + /home/node/.local \ + /commandhistory \ + && chown -R node:node /workspace /home/node/.claude /home/node/.local /commandhistory WORKDIR /workspace -# Install git-delta (better git diffs) +# gh (GitHub CLI) — installed from the upstream .deb, not apt: Debian trixie +# freezes gh at 2.46.0 (Apr 2024). Version kept current by Renovate. +# Must stay AFTER the apt block: the .deb depends on git, and dpkg -i does not +# resolve dependencies — it fails loudly ("dependency problems") if git is absent. RUN ARCH=$(dpkg --print-architecture) \ - && curl -fsSL -o "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" \ - "https://github.com/dandavison/delta/releases/download/${GIT_DELTA_VERSION}/git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" \ - && dpkg -i "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" \ - && rm "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" + && curl -fsSL -o "gh_${GH_VERSION}_linux_${ARCH}.deb" \ + "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_${ARCH}.deb" \ + && dpkg -i "gh_${GH_VERSION}_linux_${ARCH}.deb" \ + && rm "gh_${GH_VERSION}_linux_${ARCH}.deb" # ── Non-root user setup ────────────────────────────────────────────────────── USER node ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global ENV PATH=$PATH:/usr/local/share/npm-global/bin -ENV SHELL=/usr/bin/fish +ENV SHELL=/usr/bin/zsh ENV EDITOR="code --wait" ENV VISUAL="code --wait" -# ── Starship + Mise (install as root, configure as node) ─────────────────────── +# ── Mise (install as root) ──────────────────────────────────────────────────── +# Mise is installed as a tool manager — projects run `mise install` at container +# creation to install their specific tool versions from .mise.toml. +# Node is NOT installed via mise — it's provided by the base image; mise shims +# would shadow the base image's npx, breaking global npm-installed CLIs. USER root SHELL ["/bin/bash", "-o", "pipefail", "-c"] -RUN curl -sS https://starship.rs/install.sh | sh -s -- --yes RUN curl https://mise.run | sh \ && cp /root/.local/bin/mise /usr/local/bin/mise \ && rm -rf /root/.local SHELL ["/bin/sh", "-c"] -# Configure starship and fish shell. -# Mise is installed as a tool manager — projects run `mise install` at container -# creation to install their specific tool versions from .mise.toml. -# Node is NOT installed via mise — it's provided by the base image; mise shims -# would shadow the base image's npx, breaking global npm-installed CLIs. +# ── zsh + oh-my-zsh + powerlevel10k ────────────────────────────────────────── +# -x skips the script's own dependency install: zsh already comes from the apt +# block above with --no-install-recommends, which also avoids dragging in +# locales-all. Runs as node so oh-my-zsh lands in /home/node. USER node -RUN mkdir -p /home/node/.config/fish \ - && starship preset no-runtime-versions -o /home/node/.config/starship.toml \ - && printf '%s\n' 'set -g fish_greeting' 'starship init fish | source' > /home/node/.config/fish/config.fish +RUN curl -fsSL -o /tmp/zsh-in-docker.sh \ + "https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh" \ + && sh /tmp/zsh-in-docker.sh \ + -p git \ + -p fzf \ + -a "export HISTFILE=/commandhistory/.zsh_history" \ + -x \ + && rm /tmp/zsh-in-docker.sh ENV PATH="/home/node/.local/share/mise/shims:$PATH" ENV MISE_TRUSTED_CONFIG_PATHS="/workspace" diff --git a/claude-code/.devcontainer/claude-sandbox/devcontainer.json b/claude-code/.devcontainer/claude-sandbox/devcontainer.json index 9df9cfe..8067bde 100644 --- a/claude-code/.devcontainer/claude-sandbox/devcontainer.json +++ b/claude-code/.devcontainer/claude-sandbox/devcontainer.json @@ -74,9 +74,9 @@ "SamiHindi.claude-theme-sami-hindi" ], "settings": { - "terminal.integrated.defaultProfile.linux": "fish", + "terminal.integrated.defaultProfile.linux": "zsh", "terminal.integrated.profiles.linux": { - "fish": { "path": "fish" }, + "zsh": { "path": "zsh" }, "bash": { "path": "bash", "icon": "terminal-bash" } }, "extensions.ignoreRecommendations": true, @@ -111,7 +111,7 @@ // "source=myproject-node-modules-web-${localWorkspaceFolderBasename},target=/workspace/apps/web/node_modules,type=volume", // ── Persistent config ────────────────────────────────────────────── "source=myproject-claude-config-${localWorkspaceFolderBasename},target=/home/node/.claude,type=volume", - "source=myproject-fish-data-${localWorkspaceFolderBasename},target=/home/node/.local/share/fish,type=volume", + "source=myproject-zsh-history-${localWorkspaceFolderBasename},target=/commandhistory,type=volume", // ── Firewall script ──────────────────────────────────────────────── // The image provides iptables/ipset packages and sudo rule but NOT the script itself. // Each project provides its own script via bind mount to customize the domain allowlist. @@ -128,7 +128,7 @@ }, // The find command chowns all node_modules volume mount points in one pass. // Chromium is baked into the sandbox image (firewall blocks runtime install). - "postCreateCommand": "sudo find /workspace -maxdepth 4 -name node_modules -type d -exec chown node {} + && sudo chown -R node /home/node/.claude && mise install && bun install", + "postCreateCommand": "sudo find /workspace -maxdepth 4 -name node_modules -type d -exec chown node {} + && sudo chown -R node /home/node/.claude /commandhistory && mise install && bun install", // Firewall init (bind-mounted from project) + re-patch the Playwright MCP // plugin's .mcp.json. The patch is defense in depth alongside the SessionStart // hook in /etc/claude-code/managed-settings.json, which handles the case where diff --git a/claude-code/.devcontainer/devcontainer.json b/claude-code/.devcontainer/devcontainer.json index 8f0ef4d..1fe25bf 100644 --- a/claude-code/.devcontainer/devcontainer.json +++ b/claude-code/.devcontainer/devcontainer.json @@ -68,9 +68,9 @@ "ms-playwright.playwright" ], "settings": { - "terminal.integrated.defaultProfile.linux": "fish", + "terminal.integrated.defaultProfile.linux": "zsh", "terminal.integrated.profiles.linux": { - "fish": { "path": "fish" }, + "zsh": { "path": "zsh" }, "bash": { "path": "bash", "icon": "terminal-bash" } }, // Suppress extension recommendation prompts @@ -103,7 +103,7 @@ // "source=myproject-node-modules-web-${localWorkspaceFolderBasename},target=/workspace/apps/web/node_modules,type=volume", // ── Persistent config ────────────────────────────────────────────── "source=myproject-claude-config-${localWorkspaceFolderBasename},target=/home/node/.claude,type=volume", - "source=myproject-fish-data-${localWorkspaceFolderBasename},target=/home/node/.local/share/fish,type=volume" + "source=myproject-zsh-history-${localWorkspaceFolderBasename},target=/commandhistory,type=volume" ], "containerEnv": { "TZ": "${localEnv:TZ:America/Edmonton}", @@ -116,7 +116,7 @@ // mise install reads .mise.toml and installs project-specific tool versions. // Chromium is baked into the image via apt — no playwright install step needed. // Projects' playwright.config.ts should use process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH. - "updateContentCommand": "sudo find /workspace -maxdepth 4 -name node_modules -type d -exec chown node {} + && sudo chown -R node /home/node/.claude && mise install && bun install", + "updateContentCommand": "sudo find /workspace -maxdepth 4 -name node_modules -type d -exec chown node {} + && sudo chown -R node /home/node/.claude /commandhistory && mise install && bun install", // Re-patch the Playwright MCP plugin's .mcp.json on every start. Defense in // depth alongside the SessionStart hook in /etc/claude-code/managed-settings.json, // which handles the case where the plugin auto-updates mid-container-run. diff --git a/claude-code/README.md b/claude-code/README.md index 173c0e9..5ff41d2 100644 --- a/claude-code/README.md +++ b/claude-code/README.md @@ -16,8 +16,8 @@ Projects consume these pre-built images and control their own tool versions via | Layer | What | Why | |-------|------|-----| | OS | `node:24-trixie-slim` + system packages | Node is needed during the build (Playwright, npm globals) | -| Shell | Fish, Starship, fzf | Built-in syntax highlighting, autosuggestions, completions | -| Tools | git-delta, gh CLI, jq, nano, vim, wget, unzip, less, man-db, procps, openssh-client | Standard dev utilities (`openssh-client` provides `ssh`/`ssh-keygen` — enables SSH-format commit signing) | +| Shell | zsh, oh-my-zsh (`git`, `fzf` plugins), powerlevel10k | Completions, git aliases and prompt integration | +| Tools | gh CLI, jq, nano, vim, wget, unzip, less, man-db, procps, openssh-client | Standard dev utilities (`openssh-client` provides `ssh`/`ssh-keygen` — enables SSH-format commit signing) | | Mise | The tool manager itself (not the tools) | Projects run `mise install` at container creation for their tool versions | | rtk, ralphex | Pinned `ARG`s, bumped by Renovate on each GitHub release | Dev infrastructure (like Claude Code) — the image tracks the versions so projects don't have to | | Claude Code | npm global install | npm avoids rate limiting that affects the native installer in parallel CI builds | @@ -55,9 +55,9 @@ Copy the example files into your project's `.devcontainer/` directory and custom Copy these to your project's `.devcontainer/`: - [`.devcontainer/docker-compose.yml`](.devcontainer/docker-compose.yml) — image reference (kept fresh by the `initializeCommand` pull in `devcontainer.json`) -- [`.devcontainer/devcontainer.json`](.devcontainer/devcontainer.json) — full config with VS Code extensions, fish shell, OXC formatter, node_modules volume isolation, and lifecycle commands +- [`.devcontainer/devcontainer.json`](.devcontainer/devcontainer.json) — full config with VS Code extensions, zsh shell, OXC formatter, node_modules volume isolation, and lifecycle commands -**Key settings included:** fish + bash terminal profiles, OXC formatter (with comments for switching to Biome/Prettier), node_modules/Claude config/fish history volume mounts, and `updateContentCommand` for mise/bun setup. +**Key settings included:** zsh + bash terminal profiles, OXC formatter (with comments for switching to Biome/Prettier), node_modules/Claude config/zsh history volume mounts, and `updateContentCommand` for mise/bun setup. ### Sandbox variant @@ -68,7 +68,7 @@ Copy these to your project's `.devcontainer/claude-sandbox/`: **Sandbox differences from default:** `capAdd` for iptables, `postStartCommand` runs the firewall script, `claudeCode.allowDangerouslySkipPermissions` enabled, and OAuth token must be injected from the host (see [Sandbox Authentication](#sandbox-authentication)). -**Shared volumes:** Both variants use `${localWorkspaceFolderBasename}` in volume names, so they share node_modules, Claude config, and fish history. Install packages in one variant and both benefit. Docker named volumes support multi-container access, so both can run simultaneously — just avoid running `bun install` in both at the same time. +**Shared volumes:** Both variants use `${localWorkspaceFolderBasename}` in volume names, so they share node_modules, Claude config, and zsh history. Install packages in one variant and both benefit. Docker named volumes support multi-container access, so both can run simultaneously — just avoid running `bun install` in both at the same time. ## Project Setup Guide @@ -404,13 +404,14 @@ cat ~/.claude/plugins/cache/claude-plugins-official/playwright/*/.mcp.json | Arg | Default | Description | |-----|---------|-------------| -| `GIT_DELTA_VERSION` | `0.18.2` | git-delta version | -| `RTK_VERSION` | `0.43.0` | rtk version (Renovate-managed) | -| `RALPHEX_VERSION` | `1.6.0` | ralphex version (Renovate-managed) | -| `CLAUDE_CODE_VERSION` | `2.1.216` | Claude Code CLI version (Renovate-managed) | -| `AGENT_BROWSER_VERSION` | `0.32.3` | agent-browser version, default target only (Renovate-managed) | - -The four Renovate-managed args carry `# renovate:` annotations in the Dockerfile; edit them by +| `RTK_VERSION` | `0.49.0` | rtk version (Renovate-managed) | +| `RALPHEX_VERSION` | `1.7.0` | ralphex version (Renovate-managed) | +| `CLAUDE_CODE_VERSION` | `2.1.273` | Claude Code CLI version (Renovate-managed) | +| `AGENT_BROWSER_VERSION` | `0.37.1` | agent-browser version, default target only (Renovate-managed) | +| `GH_VERSION` | `2.100.0` | GitHub CLI version — installed from the upstream `.deb`, not apt (Renovate-managed) | +| `ZSH_IN_DOCKER_VERSION` | `1.2.0` | zsh + oh-my-zsh + powerlevel10k installer (Renovate-managed) | + +The six Renovate-managed args carry `# renovate:` annotations in the Dockerfile; edit them by hand only for a local build. Bumps land as auto-merged PRs — see [Automatic Rebuilds](#automatic-rebuilds). ## Building Locally / Local Fallback diff --git a/docs/superpowers/specs/2026-07-20-renovate-tool-updates-design.md b/docs/superpowers/specs/2026-07-20-renovate-tool-updates-design.md index 57ce75a..3784029 100644 --- a/docs/superpowers/specs/2026-07-20-renovate-tool-updates-design.md +++ b/docs/superpowers/specs/2026-07-20-renovate-tool-updates-design.md @@ -29,6 +29,12 @@ version** — no more time-based rebuilds. ## Non-goals +> **Partly superseded.** `Go`, `Docker` and the `hugo-bun-node` musl `Node` +> build were later brought under Renovate, alongside `gh` and `zsh-in-docker`; +> `git-delta` was removed from every image. `Bun`, `Hugo` and base images remain +> out of scope as written below. See `.github/renovate.json5` for the current +> package list. + - Managing `Bun` / `Hugo` / `Go` / `Docker` / `git-delta` / base images with Renovate (explicitly out of scope — they stay as they are today, including the manual `update-and-build-ralphex-fe.yml` version-bump path). diff --git a/hugo-bun-node/.devcontainer/Dockerfile b/hugo-bun-node/.devcontainer/Dockerfile index 8092800..bef1d1d 100644 --- a/hugo-bun-node/.devcontainer/Dockerfile +++ b/hugo-bun-node/.devcontainer/Dockerfile @@ -3,6 +3,7 @@ ARG BUN_VERSION=1.3.8 FROM oven/bun:${BUN_VERSION}-alpine ARG HUGO_VERSION=0.155.1 +# renovate: datasource=node-version depName=node ARG NODE_VERSION=24.13.0 # Platform architecture (automatically set by Docker based on build platform) diff --git a/ralphex-fe/Dockerfile b/ralphex-fe/Dockerfile index 9ca4ab9..4974a55 100644 --- a/ralphex-fe/Dockerfile +++ b/ralphex-fe/Dockerfile @@ -1,6 +1,8 @@ # renovate: datasource=npm depName=bun ARG BUN_VERSION=1.4.2 +# renovate: datasource=github-tags depName=docker/cli ARG DOCKER_VERSION=29.3.0 +# renovate: datasource=golang-version depName=go ARG GO_VERSION=1.24.4 # renovate: datasource=github-releases depName=gohugoio/hugo ARG HUGO_VERSION=0.166.0 From 01565e1e5cfd0bc3214b85414d0af65244991901 Mon Sep 17 00:00:00 2001 From: Serge Gatezh <2880401+gatezh@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:02:47 -0600 Subject: [PATCH 2/6] fix(devcontainers): repair zsh locale and TERM, drop dead code, close verify gaps Post-rebase review of the zsh migration. Two user-visible regressions, both reproduced in a built image before and after the fix. The zsh-in-docker template hardcodes en_US.UTF-8 and TERM=xterm, and -x skips the locale-gen that would create that locale. The slim bases ship only C/C.utf8/POSIX, so glibc silently fell back to the C locale: `echo "hello" | wc -m` counted bytes, not characters, mangling multibyte output in git log and TUIs. The baked TERM=xterm also clobbered the xterm-256color the terminal provides, dropping `tput colors` from 256 to 8 -- a visible downgrade on the flagship image, since the fish/starship setup left TERM alone. Both are now rewritten out of .zshrc: C.UTF-8 is a real UTF-8 locale needing no extra package, and dropping the TERM line lets the terminal decide. Dead code: claude-bun assigned SNIPPET and never used it. The upstream Anthropic pattern ends with `echo "$SNIPPET" >> ~/.bashrc`, which was never copied across -- so bash history never persisted despite the comment saying it did. Removing it also retires the SC2034 hadolint suppression, which existed solely to hide that variable; no findings return without it. Verify gaps: ci.yml checked docker for ralphex-fe but build-ralphex-fe.yml did not, and build-claude-bun.yml checked zsh but ci.yml did not -- so a regression could pass one gate and fail the other. Both directions closed. Comment accuracy: zsh was installed but absent from the package-rationale blocks, gh was still listed there though it no longer comes from apt, and the fzf entry claimed an explicit .zshrc source that this branch removed. The supply-chain trade is now stated in the Dockerfiles: ZSH_IN_DOCKER_VERSION pins only the wrapper, which fetches oh-my-zsh and powerlevel10k unpinned at build time. The stale devcontainer plan doc gets the same superseded banner the renovate spec already carries. --- .devcontainer/Dockerfile | 25 ++++++++++-- .github/workflows/build-ralphex-fe.yml | 1 + .github/workflows/ci.yml | 2 +- .hadolint.yaml | 3 -- claude-bun/.devcontainer/Dockerfile | 38 +++++++++++++++---- claude-bun/.devcontainer/devcontainer.json | 3 +- claude-code/.devcontainer/Dockerfile | 25 ++++++++++-- .../plans/2026-03-21-devcontainer-for-repo.md | 5 +++ 8 files changed, 83 insertions(+), 19 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 528ae3f..f579f82 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -76,12 +76,12 @@ ARG ZSH_IN_DOCKER_VERSION=1.2.0 # System packages (each justified — see claude-code Dockerfile for rationale) # - ca-certificates: SSL/TLS for HTTPS connections # - curl: downloading tools and installers -# - fzf: fuzzy finder (zsh key-bindings + completion sourced into .zshrc) -# - gh: GitHub CLI (installed from upstream .deb below, not apt) +# - fzf: fuzzy finder; zsh integration comes from the oh-my-zsh fzf plugin # - git: version control # - jq: JSON processing (onboarding patch, firewall script) # - less: pager for git and other CLI output # - sudo: privilege escalation (chown for named volumes, firewall setup) +# - zsh: interactive shell; oh-my-zsh + powerlevel10k configured below RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ apt-get update && apt-get install -y --no-install-recommends \ @@ -127,9 +127,22 @@ ENV EDITOR="code --wait" ENV VISUAL="code --wait" # ── zsh + oh-my-zsh + powerlevel10k ────────────────────────────────────────── +# Supply chain: ZSH_IN_DOCKER_VERSION pins only this ~190-line wrapper. The +# wrapper then curls oh-my-zsh's install.sh from master and clones powerlevel10k +# at HEAD -- both unpinned, both executed at build time, and its inner curl has +# no -f, so an upstream 5xx body would be run as shell. These bumps auto-merge +# and publish to ghcr.io unreviewed. Accepted deliberately for shell tooling; +# pin both refs to commit SHAs if that trade stops being acceptable. +# # -x skips the script's own dependency install: zsh already comes from the apt # block above with --no-install-recommends, which also avoids dragging in # locales-all. Still running as node (set above), so oh-my-zsh lands in /home/node. +# The installer's .zshrc template hardcodes en_US.UTF-8 and TERM=xterm. With -x +# we skip its locale-gen, and the slim base ships only C/C.utf8/POSIX — so the +# en_US export leaves glibc in the C locale and mangles multibyte I/O. TERM=xterm +# likewise clobbers the xterm-256color the terminal provides, dropping colour +# support to 8. Rewrite both: C.UTF-8 is a real UTF-8 locale needing no extra +# package, and dropping the TERM line lets the terminal decide. RUN curl -fsSL -o /tmp/zsh-in-docker.sh \ "https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh" \ && sh /tmp/zsh-in-docker.sh \ @@ -137,7 +150,13 @@ RUN curl -fsSL -o /tmp/zsh-in-docker.sh \ -p fzf \ -a "export HISTFILE=/commandhistory/.zsh_history" \ -x \ - && rm /tmp/zsh-in-docker.sh + && rm /tmp/zsh-in-docker.sh \ + && sed -i \ + -e "s|^export LANG=.*|export LANG='C.UTF-8'|" \ + -e "s|^export LC_ALL=.*|export LC_ALL='C.UTF-8'|" \ + -e "/^export LANGUAGE=/d" \ + -e "/^export TERM=xterm$/d" \ + "$HOME/.zshrc" # ── Dev tools (copied from parallel download stages) ───────────────────────── # rtk (token-optimized CLI proxy) and ralphex (autonomous plan execution). diff --git a/.github/workflows/build-ralphex-fe.yml b/.github/workflows/build-ralphex-fe.yml index 1a7785c..4859849 100644 --- a/.github/workflows/build-ralphex-fe.yml +++ b/.github/workflows/build-ralphex-fe.yml @@ -98,6 +98,7 @@ jobs: hugo version && python3 --version && go version && + docker --version && node --version && /srv/ralphex --version && claude --version && diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b913ee1..b936ac3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,7 +110,7 @@ jobs: add_image "claude-bun" \ "claude-bun/.devcontainer" \ "claude-bun/.devcontainer/Dockerfile" \ - "bun --version && gh --version" + "bun --version && gh --version && zsh --version" fi if [ "$CHANGED_CLAUDE_CODE" = "true" ]; then diff --git a/.hadolint.yaml b/.hadolint.yaml index eec7f41..fa473b9 100644 --- a/.hadolint.yaml +++ b/.hadolint.yaml @@ -15,9 +15,6 @@ ignored: # and curl -f already fails on HTTP errors. - DL4006 # set SHELL option -o pipefail before RUN with pipe - # SNIPPET variable in devcontainer-claude-bun follows the official - # Anthropic Claude Code devcontainer pattern (anthropics/claude-code). - - SC2034 # variable appears unused # ralphex-fe entrypoint runs as root to handle APP_UID remapping, # then drops to app user via gosu. No final USER directive is correct. diff --git a/claude-bun/.devcontainer/Dockerfile b/claude-bun/.devcontainer/Dockerfile index 6a33881..8b0498a 100644 --- a/claude-bun/.devcontainer/Dockerfile +++ b/claude-bun/.devcontainer/Dockerfile @@ -35,9 +35,10 @@ RUN mkdir -p /usr/local/share/bun-global && \ ARG USERNAME=bun -# Persist bash history -RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \ - && mkdir /commandhistory \ +# Shared history volume mount point. Despite the .bash_history name — kept so +# existing /commandhistory volumes don't lose their history — zsh is what writes +# here, via HISTFILE below. bash history is not persisted in this image. +RUN mkdir /commandhistory \ && touch /commandhistory/.bash_history \ && chown -R $USERNAME /commandhistory @@ -68,10 +69,25 @@ ENV HOME=/home/bun ENV BUN_INSTALL=/home/bun/.bun ENV PATH="$BUN_INSTALL/bin:$PATH" -# Set the default shell to zsh rather than sh -ENV SHELL=/bin/zsh - -# Default powerline10k theme +# Set the default shell to zsh rather than sh (/usr/bin/zsh, matching the other +# images; /bin/zsh is the same binary via usrmerge) +ENV SHELL=/usr/bin/zsh + +# zsh + oh-my-zsh + powerlevel10k. +# +# Supply chain: ZSH_IN_DOCKER_VERSION pins only this ~190-line wrapper. The +# wrapper then curls oh-my-zsh's install.sh from master and clones powerlevel10k +# at HEAD -- both unpinned, both executed at build time, and its inner curl has +# no -f, so an upstream 5xx body would be run as shell. Accepted deliberately +# for shell tooling; pin both refs to commit SHAs if that trade stops being +# acceptable. +# +# The installer's .zshrc template hardcodes en_US.UTF-8 and TERM=xterm. With -x +# we skip its locale-gen, and the slim base ships only C/C.utf8/POSIX — so the +# en_US export leaves glibc in the C locale and mangles multibyte I/O. TERM=xterm +# likewise clobbers the xterm-256color the terminal provides, dropping colour +# support to 8. Rewrite both: C.UTF-8 is a real UTF-8 locale needing no extra +# package, and dropping the TERM line lets the terminal decide. RUN curl -fsSL -o /tmp/zsh-in-docker.sh \ "https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh" \ && sh /tmp/zsh-in-docker.sh \ @@ -79,7 +95,13 @@ RUN curl -fsSL -o /tmp/zsh-in-docker.sh \ -p fzf \ -a "export HISTFILE=/commandhistory/.bash_history" \ -x \ - && rm /tmp/zsh-in-docker.sh + && rm /tmp/zsh-in-docker.sh \ + && sed -i \ + -e "s|^export LANG=.*|export LANG='C.UTF-8'|" \ + -e "s|^export LC_ALL=.*|export LC_ALL='C.UTF-8'|" \ + -e "/^export LANGUAGE=/d" \ + -e "/^export TERM=xterm$/d" \ + "$HOME/.zshrc" # Install Claude Code globally using bun (run as root to avoid EACCES linking) USER root diff --git a/claude-bun/.devcontainer/devcontainer.json b/claude-bun/.devcontainer/devcontainer.json index 30408d9..a4bb0f0 100644 --- a/claude-bun/.devcontainer/devcontainer.json +++ b/claude-bun/.devcontainer/devcontainer.json @@ -18,7 +18,8 @@ "mounts": [ // Keep node_modules out of a host machine "source=${localWorkspaceFolderBasename}-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume", - // Persist bash history between container rebuilds + // Persist shell history between container rebuilds. zsh writes here + // (HISTFILE in the Dockerfile); the volume name is legacy. "source=claude-code-bashhistory-${devcontainerId},target=/commandhistory,type=volume", // Persist Claude Code configuration between container rebuilds "source=claude-code-config-${devcontainerId},target=/home/bun/.claude,type=volume" diff --git a/claude-code/.devcontainer/Dockerfile b/claude-code/.devcontainer/Dockerfile index 5d2a066..b44eb32 100644 --- a/claude-code/.devcontainer/Dockerfile +++ b/claude-code/.devcontainer/Dockerfile @@ -54,8 +54,7 @@ ARG ZSH_IN_DOCKER_VERSION=1.2.0 # System packages shared by all targets # - ca-certificates: SSL/TLS for HTTPS connections # - curl: downloading tools and installers -# - fzf: fuzzy finder (zsh key-bindings + completion sourced into .zshrc) -# - gh: GitHub CLI (installed from upstream .deb below, not apt) +# - fzf: fuzzy finder; zsh integration comes from the oh-my-zsh fzf plugin # - git: version control # - jq: JSON processing (firewall script, onboarding patch) # - less: pager for git and other CLI output @@ -67,6 +66,7 @@ ARG ZSH_IN_DOCKER_VERSION=1.2.0 # sign too. Must be in base: the sandbox firewall blocks deb.debian.org, so # it can't be added at runtime (same reasoning as chromium). See issue #110. # - sudo: privilege escalation for firewall setup +# - zsh: interactive shell; oh-my-zsh + powerlevel10k configured below RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ apt-get update && apt-get install -y --no-install-recommends \ @@ -137,10 +137,23 @@ RUN curl https://mise.run | sh \ SHELL ["/bin/sh", "-c"] # ── zsh + oh-my-zsh + powerlevel10k ────────────────────────────────────────── +# Supply chain: ZSH_IN_DOCKER_VERSION pins only this ~190-line wrapper. The +# wrapper then curls oh-my-zsh's install.sh from master and clones powerlevel10k +# at HEAD -- both unpinned, both executed at build time, and its inner curl has +# no -f, so an upstream 5xx body would be run as shell. These bumps auto-merge +# and publish to ghcr.io unreviewed. Accepted deliberately for shell tooling; +# pin both refs to commit SHAs if that trade stops being acceptable. +# # -x skips the script's own dependency install: zsh already comes from the apt # block above with --no-install-recommends, which also avoids dragging in # locales-all. Runs as node so oh-my-zsh lands in /home/node. USER node +# The installer's .zshrc template hardcodes en_US.UTF-8 and TERM=xterm. With -x +# we skip its locale-gen, and the slim base ships only C/C.utf8/POSIX — so the +# en_US export leaves glibc in the C locale and mangles multibyte I/O. TERM=xterm +# likewise clobbers the xterm-256color the terminal provides, dropping colour +# support to 8. Rewrite both: C.UTF-8 is a real UTF-8 locale needing no extra +# package, and dropping the TERM line lets the terminal decide. RUN curl -fsSL -o /tmp/zsh-in-docker.sh \ "https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh" \ && sh /tmp/zsh-in-docker.sh \ @@ -148,7 +161,13 @@ RUN curl -fsSL -o /tmp/zsh-in-docker.sh \ -p fzf \ -a "export HISTFILE=/commandhistory/.zsh_history" \ -x \ - && rm /tmp/zsh-in-docker.sh + && rm /tmp/zsh-in-docker.sh \ + && sed -i \ + -e "s|^export LANG=.*|export LANG='C.UTF-8'|" \ + -e "s|^export LC_ALL=.*|export LC_ALL='C.UTF-8'|" \ + -e "/^export LANGUAGE=/d" \ + -e "/^export TERM=xterm$/d" \ + "$HOME/.zshrc" ENV PATH="/home/node/.local/share/mise/shims:$PATH" ENV MISE_TRUSTED_CONFIG_PATHS="/workspace" diff --git a/docs/superpowers/plans/2026-03-21-devcontainer-for-repo.md b/docs/superpowers/plans/2026-03-21-devcontainer-for-repo.md index a1bac09..4c923b4 100644 --- a/docs/superpowers/plans/2026-03-21-devcontainer-for-repo.md +++ b/docs/superpowers/plans/2026-03-21-devcontainer-for-repo.md @@ -6,6 +6,11 @@ **Architecture:** Multi-stage Dockerfile following `claude-code/.devcontainer/Dockerfile` pattern: shared `base` stage with all common tooling, then `default` (passwordless sudo) and `sandbox` (firewall packages + restricted sudo) targets. rtk and ralphex fetch latest release at build time. git-delta is pinned (v0.19.0 dropped arm64 .deb and changed naming). +> **Partly superseded.** Fish and Starship were replaced by zsh + oh-my-zsh + +> powerlevel10k, and git-delta was removed from every image. The Dockerfile +> excerpts below still show the original fish/starship blocks. See +> `.devcontainer/Dockerfile` for the current shape. + **Tech Stack:** Docker multi-stage builds, Node 24 LTS (trixie-slim), Fish shell + Starship prompt, iptables (sandbox), Claude Code plugins --- From 5b6a11ebe913f592b4ab9aecdc8a8dd4788fb4ca Mon Sep 17 00:00:00 2001 From: Serge Gatezh <2880401+gatezh@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:39:39 -0600 Subject: [PATCH 3/6] fix(devcontainers): make the zsh fixup survive upstream template changes The previous commit patched .zshrc by matching the exact lines zsh-in-docker v1.2.0 emits. That was already broken against the currently released v1.2.1, which writes `[ -z "$TERM" ] && export TERM=xterm` inside an UNQUOTED heredoc -- so $TERM expands at build time, when it is unset, to `[ -z "" ] && export TERM=xterm`, unconditionally true and invisible to a pattern anchored on the old text. sed exits 0 on a non-match, so the fix would have silently stopped working. Verified by building against both releases: on v1.2.1 the old sed leaves TERM clobbered to xterm. Since zsh-in-docker auto-merges after its soak, Renovate would have adopted v1.2.1 and reintroduced the 8-colour regression the previous commit fixed. Now the whole template preamble is cut at the `##### Zsh/Oh-my-Zsh Configuration` marker, which is identical in both releases, instead of matching individual lines. A grep guard precedes it: without one, a marker rename would make sed delete the entire file rather than nothing. Verified against v1.2.0 and v1.2.1 -- TERM survives and the theme, plugins and history all still load. Locale moves from .zshrc to ENV LANG/LC_ALL on the image. The .zshrc-only fix covered interactive zsh and nothing else: postCreateCommand and updateContentCommand run under sh, as do the VS Code server, docker exec and every bash Claude Code spawns -- all still saw ANSI_X3.4-1968 and mangled multibyte output, which is the bug the previous commit set out to fix. CI now asserts the outcome rather than trusting the transformation, on all eight verify paths: LC_ALL resolves to C.UTF-8 and a sentinel TERM survives shell startup. Both were checked negatively -- they fail against a v1.2.1 image and against a hand-regressed .zshrc. The sentinel is deliberately not xterm-256color, which would also pass if a template ever hardcoded it. Also corrected: both READMEs advertised tools no image contains (nano, vim, wget, unzip, man-db for claude-code; vim, nano for claude-bun -- all verified absent in a build), and the renovate.json5 extractVersion rationale cited git-delta, which this branch deletes everywhere. --- .devcontainer/Dockerfile | 29 +++++++++++++++---------- .github/renovate.json5 | 4 ++-- .github/workflows/build-claude-bun.yml | 2 +- .github/workflows/build-claude-code.yml | 8 +++---- .github/workflows/ci.yml | 6 ++--- claude-bun/.devcontainer/Dockerfile | 29 +++++++++++++++---------- claude-bun/README.md | 2 +- claude-code/.devcontainer/Dockerfile | 29 +++++++++++++++---------- claude-code/README.md | 2 +- 9 files changed, 63 insertions(+), 48 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index f579f82..1a09036 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -123,6 +123,8 @@ USER node ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global ENV PATH=$PATH:/usr/local/share/npm-global/bin ENV SHELL=/usr/bin/zsh +ENV LANG=C.UTF-8 +ENV LC_ALL=C.UTF-8 ENV EDITOR="code --wait" ENV VISUAL="code --wait" @@ -137,12 +139,19 @@ ENV VISUAL="code --wait" # -x skips the script's own dependency install: zsh already comes from the apt # block above with --no-install-recommends, which also avoids dragging in # locales-all. Still running as node (set above), so oh-my-zsh lands in /home/node. -# The installer's .zshrc template hardcodes en_US.UTF-8 and TERM=xterm. With -x -# we skip its locale-gen, and the slim base ships only C/C.utf8/POSIX — so the -# en_US export leaves glibc in the C locale and mangles multibyte I/O. TERM=xterm -# likewise clobbers the xterm-256color the terminal provides, dropping colour -# support to 8. Rewrite both: C.UTF-8 is a real UTF-8 locale needing no extra -# package, and dropping the TERM line lets the terminal decide. +# The template opens with a preamble of env exports we don't want: en_US.UTF-8 +# (which -x never generates — the slim bases carry only C/C.utf8/POSIX, so glibc +# falls back to C and mangles multibyte I/O) and a TERM that overrides whatever +# the terminal provides, cutting colour support from 256 to 8. +# +# Delete the whole preamble rather than matching individual lines. v1.2.0 wrote +# `export TERM=xterm`; v1.2.1 writes `[ -z "$TERM" ] && export TERM=xterm` inside +# an UNQUOTED heredoc, so $TERM expands at build time (unset) to `[ -z "" ] && ...` +# — always true, and invisible to a pattern anchored on the old text. Cutting to +# the `##### Zsh/Oh-my-Zsh Configuration` marker is stable across both. The grep +# guard matters: without it a marker rename would make sed delete the whole file. +# Locale now comes from ENV above, which also covers sh/bash, lifecycle commands +# and the VS Code server — not just interactive zsh. RUN curl -fsSL -o /tmp/zsh-in-docker.sh \ "https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh" \ && sh /tmp/zsh-in-docker.sh \ @@ -151,12 +160,8 @@ RUN curl -fsSL -o /tmp/zsh-in-docker.sh \ -a "export HISTFILE=/commandhistory/.zsh_history" \ -x \ && rm /tmp/zsh-in-docker.sh \ - && sed -i \ - -e "s|^export LANG=.*|export LANG='C.UTF-8'|" \ - -e "s|^export LC_ALL=.*|export LC_ALL='C.UTF-8'|" \ - -e "/^export LANGUAGE=/d" \ - -e "/^export TERM=xterm$/d" \ - "$HOME/.zshrc" + && grep -q '^##### Zsh/Oh-my-Zsh Configuration' "$HOME/.zshrc" \ + && sed -i '/^##### Zsh\/Oh-my-Zsh Configuration/,$!d' "$HOME/.zshrc" # ── Dev tools (copied from parallel download stages) ───────────────────────── # rtk (token-optimized CLI proxy) and ralphex (autonomous plan execution). diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 33e21fd..ad8b660 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -23,8 +23,8 @@ packageRules: [ { // Most GitHub tags carry a leading "v" ("v0.48.0", "v29.8.1"); strip it so the - // datasource version matches the bare ARG value ("0.48.0"). Harmless for the - // ones that don't (dandavison/delta-style bare tags pass through unchanged). + // datasource version matches the bare ARG value ("0.48.0"). A tag with no "v" + // passes through unchanged, so this is safe to apply datasource-wide. matchDatasources: ['github-releases', 'github-tags'], extractVersion: '^v?(?.+)$', }, diff --git a/.github/workflows/build-claude-bun.yml b/.github/workflows/build-claude-bun.yml index d71ceda..ab82f2d 100644 --- a/.github/workflows/build-claude-bun.yml +++ b/.github/workflows/build-claude-bun.yml @@ -41,5 +41,5 @@ jobs: context: claude-bun/.devcontainer dockerfile: claude-bun/.devcontainer/Dockerfile version-tag: ${{ needs.prepare.outputs.version-tag }} - verify-command: 'bun --version && gh --version && zsh --version' + verify-command: "bun --version && gh --version && zsh --version && zsh -ic 'printenv LC_ALL' | grep -qx C.UTF-8 && TERM=zsh-verify-sentinel zsh -ic 'printenv TERM' | grep -qx zsh-verify-sentinel" secrets: inherit diff --git a/.github/workflows/build-claude-code.yml b/.github/workflows/build-claude-code.yml index 9a1b488..88226bf 100644 --- a/.github/workflows/build-claude-code.yml +++ b/.github/workflows/build-claude-code.yml @@ -75,19 +75,19 @@ jobs: matrix: include: - image-suffix: claude-code - verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium" + verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'printenv LC_ALL' | grep -qx C.UTF-8 && TERM=zsh-verify-sentinel zsh -ic 'printenv TERM' | grep -qx zsh-verify-sentinel" runner: ubuntu-24.04 arch: amd64 - image-suffix: claude-code - verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium" + verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'printenv LC_ALL' | grep -qx C.UTF-8 && TERM=zsh-verify-sentinel zsh -ic 'printenv TERM' | grep -qx zsh-verify-sentinel" runner: ubuntu-24.04-arm arch: arm64 - image-suffix: claude-code-sandbox - verify-command: "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp" + verify-command: "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'printenv LC_ALL' | grep -qx C.UTF-8 && TERM=zsh-verify-sentinel zsh -ic 'printenv TERM' | grep -qx zsh-verify-sentinel" runner: ubuntu-24.04 arch: amd64 - image-suffix: claude-code-sandbox - verify-command: "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp" + verify-command: "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'printenv LC_ALL' | grep -qx C.UTF-8 && TERM=zsh-verify-sentinel zsh -ic 'printenv TERM' | grep -qx zsh-verify-sentinel" runner: ubuntu-24.04-arm arch: arm64 runs-on: ${{ matrix.runner }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b936ac3..d4342e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,19 +110,19 @@ jobs: add_image "claude-bun" \ "claude-bun/.devcontainer" \ "claude-bun/.devcontainer/Dockerfile" \ - "bun --version && gh --version && zsh --version" + "bun --version && gh --version && zsh --version && zsh -ic 'printenv LC_ALL' | grep -qx C.UTF-8 && TERM=zsh-verify-sentinel zsh -ic 'printenv TERM' | grep -qx zsh-verify-sentinel" fi if [ "$CHANGED_CLAUDE_CODE" = "true" ]; then add_image "claude-code" \ "claude-code/.devcontainer" \ "claude-code/.devcontainer/Dockerfile" \ - "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium" \ + "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'printenv LC_ALL' | grep -qx C.UTF-8 && TERM=zsh-verify-sentinel zsh -ic 'printenv TERM' | grep -qx zsh-verify-sentinel" \ "default" add_image "claude-code-sandbox" \ "claude-code/.devcontainer" \ "claude-code/.devcontainer/Dockerfile" \ - "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp" \ + "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'printenv LC_ALL' | grep -qx C.UTF-8 && TERM=zsh-verify-sentinel zsh -ic 'printenv TERM' | grep -qx zsh-verify-sentinel" \ "sandbox" fi diff --git a/claude-bun/.devcontainer/Dockerfile b/claude-bun/.devcontainer/Dockerfile index 8b0498a..8853c21 100644 --- a/claude-bun/.devcontainer/Dockerfile +++ b/claude-bun/.devcontainer/Dockerfile @@ -72,6 +72,8 @@ ENV PATH="$BUN_INSTALL/bin:$PATH" # Set the default shell to zsh rather than sh (/usr/bin/zsh, matching the other # images; /bin/zsh is the same binary via usrmerge) ENV SHELL=/usr/bin/zsh +ENV LANG=C.UTF-8 +ENV LC_ALL=C.UTF-8 # zsh + oh-my-zsh + powerlevel10k. # @@ -82,12 +84,19 @@ ENV SHELL=/usr/bin/zsh # for shell tooling; pin both refs to commit SHAs if that trade stops being # acceptable. # -# The installer's .zshrc template hardcodes en_US.UTF-8 and TERM=xterm. With -x -# we skip its locale-gen, and the slim base ships only C/C.utf8/POSIX — so the -# en_US export leaves glibc in the C locale and mangles multibyte I/O. TERM=xterm -# likewise clobbers the xterm-256color the terminal provides, dropping colour -# support to 8. Rewrite both: C.UTF-8 is a real UTF-8 locale needing no extra -# package, and dropping the TERM line lets the terminal decide. +# The template opens with a preamble of env exports we don't want: en_US.UTF-8 +# (which -x never generates — the slim bases carry only C/C.utf8/POSIX, so glibc +# falls back to C and mangles multibyte I/O) and a TERM that overrides whatever +# the terminal provides, cutting colour support from 256 to 8. +# +# Delete the whole preamble rather than matching individual lines. v1.2.0 wrote +# `export TERM=xterm`; v1.2.1 writes `[ -z "$TERM" ] && export TERM=xterm` inside +# an UNQUOTED heredoc, so $TERM expands at build time (unset) to `[ -z "" ] && ...` +# — always true, and invisible to a pattern anchored on the old text. Cutting to +# the `##### Zsh/Oh-my-Zsh Configuration` marker is stable across both. The grep +# guard matters: without it a marker rename would make sed delete the whole file. +# Locale now comes from ENV above, which also covers sh/bash, lifecycle commands +# and the VS Code server — not just interactive zsh. RUN curl -fsSL -o /tmp/zsh-in-docker.sh \ "https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh" \ && sh /tmp/zsh-in-docker.sh \ @@ -96,12 +105,8 @@ RUN curl -fsSL -o /tmp/zsh-in-docker.sh \ -a "export HISTFILE=/commandhistory/.bash_history" \ -x \ && rm /tmp/zsh-in-docker.sh \ - && sed -i \ - -e "s|^export LANG=.*|export LANG='C.UTF-8'|" \ - -e "s|^export LC_ALL=.*|export LC_ALL='C.UTF-8'|" \ - -e "/^export LANGUAGE=/d" \ - -e "/^export TERM=xterm$/d" \ - "$HOME/.zshrc" + && grep -q '^##### Zsh/Oh-my-Zsh Configuration' "$HOME/.zshrc" \ + && sed -i '/^##### Zsh\/Oh-my-Zsh Configuration/,$!d' "$HOME/.zshrc" # Install Claude Code globally using bun (run as root to avoid EACCES linking) USER root diff --git a/claude-bun/README.md b/claude-bun/README.md index b8f3730..eebcae7 100644 --- a/claude-bun/README.md +++ b/claude-bun/README.md @@ -7,7 +7,7 @@ Claude Code development container based on the [official Anthropic devcontainer - **Bun runtime** (slim Debian-based image) for fast JavaScript/TypeScript execution - **Claude Code CLI** pre-installed globally via `bun add -g` - **Security by design** with custom firewall restricting network access to necessary services only -- **Developer-friendly tools**: git, ZSH with Powerlevel10k theme, fzf, vim, nano +- **Developer-friendly tools**: git, gh CLI, ZSH with oh-my-zsh + Powerlevel10k, fzf, jq - **VS Code integration** with pre-configured extensions (Claude Code, Bun, Biome, Tailwind CSS) - **Session persistence** for command history and Claude configuration between restarts - **Multi-platform support** (linux/amd64, linux/arm64) diff --git a/claude-code/.devcontainer/Dockerfile b/claude-code/.devcontainer/Dockerfile index b44eb32..4807d90 100644 --- a/claude-code/.devcontainer/Dockerfile +++ b/claude-code/.devcontainer/Dockerfile @@ -121,6 +121,8 @@ USER node ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global ENV PATH=$PATH:/usr/local/share/npm-global/bin ENV SHELL=/usr/bin/zsh +ENV LANG=C.UTF-8 +ENV LC_ALL=C.UTF-8 ENV EDITOR="code --wait" ENV VISUAL="code --wait" @@ -148,12 +150,19 @@ SHELL ["/bin/sh", "-c"] # block above with --no-install-recommends, which also avoids dragging in # locales-all. Runs as node so oh-my-zsh lands in /home/node. USER node -# The installer's .zshrc template hardcodes en_US.UTF-8 and TERM=xterm. With -x -# we skip its locale-gen, and the slim base ships only C/C.utf8/POSIX — so the -# en_US export leaves glibc in the C locale and mangles multibyte I/O. TERM=xterm -# likewise clobbers the xterm-256color the terminal provides, dropping colour -# support to 8. Rewrite both: C.UTF-8 is a real UTF-8 locale needing no extra -# package, and dropping the TERM line lets the terminal decide. +# The template opens with a preamble of env exports we don't want: en_US.UTF-8 +# (which -x never generates — the slim bases carry only C/C.utf8/POSIX, so glibc +# falls back to C and mangles multibyte I/O) and a TERM that overrides whatever +# the terminal provides, cutting colour support from 256 to 8. +# +# Delete the whole preamble rather than matching individual lines. v1.2.0 wrote +# `export TERM=xterm`; v1.2.1 writes `[ -z "$TERM" ] && export TERM=xterm` inside +# an UNQUOTED heredoc, so $TERM expands at build time (unset) to `[ -z "" ] && ...` +# — always true, and invisible to a pattern anchored on the old text. Cutting to +# the `##### Zsh/Oh-my-Zsh Configuration` marker is stable across both. The grep +# guard matters: without it a marker rename would make sed delete the whole file. +# Locale now comes from ENV above, which also covers sh/bash, lifecycle commands +# and the VS Code server — not just interactive zsh. RUN curl -fsSL -o /tmp/zsh-in-docker.sh \ "https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh" \ && sh /tmp/zsh-in-docker.sh \ @@ -162,12 +171,8 @@ RUN curl -fsSL -o /tmp/zsh-in-docker.sh \ -a "export HISTFILE=/commandhistory/.zsh_history" \ -x \ && rm /tmp/zsh-in-docker.sh \ - && sed -i \ - -e "s|^export LANG=.*|export LANG='C.UTF-8'|" \ - -e "s|^export LC_ALL=.*|export LC_ALL='C.UTF-8'|" \ - -e "/^export LANGUAGE=/d" \ - -e "/^export TERM=xterm$/d" \ - "$HOME/.zshrc" + && grep -q '^##### Zsh/Oh-my-Zsh Configuration' "$HOME/.zshrc" \ + && sed -i '/^##### Zsh\/Oh-my-Zsh Configuration/,$!d' "$HOME/.zshrc" ENV PATH="/home/node/.local/share/mise/shims:$PATH" ENV MISE_TRUSTED_CONFIG_PATHS="/workspace" diff --git a/claude-code/README.md b/claude-code/README.md index 5ff41d2..d8e88ef 100644 --- a/claude-code/README.md +++ b/claude-code/README.md @@ -17,7 +17,7 @@ Projects consume these pre-built images and control their own tool versions via |-------|------|-----| | OS | `node:24-trixie-slim` + system packages | Node is needed during the build (Playwright, npm globals) | | Shell | zsh, oh-my-zsh (`git`, `fzf` plugins), powerlevel10k | Completions, git aliases and prompt integration | -| Tools | gh CLI, jq, nano, vim, wget, unzip, less, man-db, procps, openssh-client | Standard dev utilities (`openssh-client` provides `ssh`/`ssh-keygen` — enables SSH-format commit signing) | +| Tools | gh CLI, git, curl, jq, less, fzf, procps, openssh-client | Standard dev utilities (`openssh-client` provides `ssh`/`ssh-keygen` — enables SSH-format commit signing) | | Mise | The tool manager itself (not the tools) | Projects run `mise install` at container creation for their tool versions | | rtk, ralphex | Pinned `ARG`s, bumped by Renovate on each GitHub release | Dev infrastructure (like Claude Code) — the image tracks the versions so projects don't have to | | Claude Code | npm global install | npm avoids rate limiting that affects the native installer in parallel CI builds | From e998a571c602512964c9002e187a9ec6965f6ab6 Mon Sep 17 00:00:00 2001 From: Serge Gatezh <2880401+gatezh@users.noreply.github.com> Date: Wed, 16 Sep 2026 19:09:28 -0600 Subject: [PATCH 4/6] refactor(devcontainers): install oh-my-zsh directly instead of via zsh-in-docker zsh-in-docker generates a .zshrc from its own template, and every fix in this branch so far has been a skirmish with that template: an anchored sed to strip its TERM line, then a preamble cut when v1.2.1 changed the line, then CI assertions to catch the next change. The template was the problem, not any of the individual patches. Now oh-my-zsh and powerlevel10k are fetched as tarballs pinned to commit SHAs in a parallel download stage, and .zshrc is written here. Nothing upstream can inject env overrides that then have to be patched back out. Pinned by SHA rather than tracked by Renovate, deliberately. Neither project tags usefully -- oh-my-zsh has no tags at all and powerlevel10k's last release predates its master by years -- and auto-adopting shell-framework changes is exactly what kept breaking this image. A prompt theme does not need a 3-day release cadence. Environment is now what the official images do: ENV LANG=C.UTF-8 and nothing else. LC_ALL is dropped -- it outranks every LC_* category, so a consuming project's containerEnv could not override one. TERM is dropped too: no official image sets it, an image ENV beats the tty value docker exec -t supplies, and tput failing without a terminal is correct behaviour rather than a bug to paper over. The two locale/TERM verify assertions are replaced by one that exercises the whole chain -- an interactive zsh reporting the powerlevel10k theme, which is only true if both pinned tarballs and the generated .zshrc are intact. Checked negatively: it fails against a hand-broken .zshrc. Removes deluan/zsh-in-docker as a dependency, its Renovate entry and its ARG. Renovate now tracks 10 packages. Verified on all four image variants (claude-code default and sandbox, claude-bun, the repo devcontainer): theme, plugins, git aliases, fzf widgets, HISTFILE and locale all correct; no configuration wizard; prompt renders in a git repo with --network none, so nothing is fetched at first shell. --- .devcontainer/Dockerfile | 85 +++++++++++-------- .github/renovate.json5 | 1 - .github/workflows/build-claude-bun.yml | 2 +- .github/workflows/build-claude-code.yml | 8 +- .github/workflows/ci.yml | 6 +- claude-bun/.devcontainer/Dockerfile | 82 +++++++++++------- claude-code/.devcontainer/Dockerfile | 84 ++++++++++-------- claude-code/README.md | 5 +- ...2026-07-20-renovate-tool-updates-design.md | 3 +- 9 files changed, 159 insertions(+), 117 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 1a09036..69ab28f 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -18,6 +18,23 @@ # ═════════════════════════════════════════════════════════════════════════════ # ── rtk (token-optimized CLI proxy) ────────────────────────────────────── +# ── oh-my-zsh + powerlevel10k ───────────────────────────────── +# Pinned to commit SHAs and fetched as tarballs. Neither project tags usefully -- +# oh-my-zsh has no tags at all, and powerlevel10k's last release predates its +# current master by years -- so a SHA is the only real pin. Bump deliberately: +# auto-adopting upstream shell-framework changes is what repeatedly broke this +# image, and a prompt theme does not need a 3-day release cadence. +FROM alpine:3.21 AS ohmyzsh-download +RUN apk add --no-cache curl tar +ARG OH_MY_ZSH_REF=0ee67f042872d1dfab74270c31867771ca35aef4 +ARG POWERLEVEL10K_REF=d05a1b00f9a61f9578bf9dc19b8451942dde8734 +RUN set -eux; \ + mkdir -p /omz/custom/themes/powerlevel10k; \ + curl -fsSL "https://github.com/ohmyzsh/ohmyzsh/archive/${OH_MY_ZSH_REF}.tar.gz" \ + | tar -xz -C /omz --strip-components=1; \ + curl -fsSL "https://github.com/romkatv/powerlevel10k/archive/${POWERLEVEL10K_REF}.tar.gz" \ + | tar -xz -C /omz/custom/themes/powerlevel10k --strip-components=1 + FROM alpine:3.21 AS rtk-download RUN apk add --no-cache curl jq RUN set -eux; \ @@ -70,8 +87,6 @@ FROM node:24-trixie-slim AS base # renovate: datasource=github-releases depName=cli/cli ARG GH_VERSION=2.100.0 -# renovate: datasource=github-releases depName=deluan/zsh-in-docker -ARG ZSH_IN_DOCKER_VERSION=1.2.0 # System packages (each justified — see claude-code Dockerfile for rationale) # - ca-certificates: SSL/TLS for HTTPS connections @@ -123,45 +138,43 @@ USER node ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global ENV PATH=$PATH:/usr/local/share/npm-global/bin ENV SHELL=/usr/bin/zsh +# Only LANG, and only C.UTF-8 — what the official images do (ruby sets exactly +# this; node/debian/python set nothing). LC_ALL would override every LC_* category +# and silently defeat a consuming project's containerEnv. TERM is deliberately +# unset: no official image sets it, an image ENV beats the tty value `docker exec -t` +# supplies, and tput/clear failing without a terminal is correct behaviour. ENV LANG=C.UTF-8 -ENV LC_ALL=C.UTF-8 ENV EDITOR="code --wait" ENV VISUAL="code --wait" # ── zsh + oh-my-zsh + powerlevel10k ────────────────────────────────────────── -# Supply chain: ZSH_IN_DOCKER_VERSION pins only this ~190-line wrapper. The -# wrapper then curls oh-my-zsh's install.sh from master and clones powerlevel10k -# at HEAD -- both unpinned, both executed at build time, and its inner curl has -# no -f, so an upstream 5xx body would be run as shell. These bumps auto-merge -# and publish to ghcr.io unreviewed. Accepted deliberately for shell tooling; -# pin both refs to commit SHAs if that trade stops being acceptable. -# -# -x skips the script's own dependency install: zsh already comes from the apt -# block above with --no-install-recommends, which also avoids dragging in -# locales-all. Still running as node (set above), so oh-my-zsh lands in /home/node. -# The template opens with a preamble of env exports we don't want: en_US.UTF-8 -# (which -x never generates — the slim bases carry only C/C.utf8/POSIX, so glibc -# falls back to C and mangles multibyte I/O) and a TERM that overrides whatever -# the terminal provides, cutting colour support from 256 to 8. -# -# Delete the whole preamble rather than matching individual lines. v1.2.0 wrote -# `export TERM=xterm`; v1.2.1 writes `[ -z "$TERM" ] && export TERM=xterm` inside -# an UNQUOTED heredoc, so $TERM expands at build time (unset) to `[ -z "" ] && ...` -# — always true, and invisible to a pattern anchored on the old text. Cutting to -# the `##### Zsh/Oh-my-Zsh Configuration` marker is stable across both. The grep -# guard matters: without it a marker rename would make sed delete the whole file. -# Locale now comes from ENV above, which also covers sh/bash, lifecycle commands -# and the VS Code server — not just interactive zsh. -RUN curl -fsSL -o /tmp/zsh-in-docker.sh \ - "https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh" \ - && sh /tmp/zsh-in-docker.sh \ - -p git \ - -p fzf \ - -a "export HISTFILE=/commandhistory/.zsh_history" \ - -x \ - && rm /tmp/zsh-in-docker.sh \ - && grep -q '^##### Zsh/Oh-my-Zsh Configuration' "$HOME/.zshrc" \ - && sed -i '/^##### Zsh\/Oh-my-Zsh Configuration/,$!d' "$HOME/.zshrc" +# Written as node: $HOME during RUN follows USER, so this must not run as root. +USER node +# oh-my-zsh tree is COPYed from the parallel download stage above, so the final +# image needs no git or curl for it and nothing is fetched at build time here. +COPY --from=ohmyzsh-download --chown=node:node /omz /home/node/.oh-my-zsh + +# .zshrc is written here rather than by an installer, so nothing upstream can +# inject env overrides (locale/TERM) that then have to be patched back out. +# The wizard flag matters: without a POWERLEVEL9K_* config powerlevel10k starts +# its interactive configurator on first shell, which blocks a container. +# $HOME and $ZSH are single-quoted on purpose: they must reach .zshrc +# unexpanded so zsh resolves them at runtime, not at build time. +# hadolint ignore=SC2016 +RUN printf '%s\n' \ + 'export ZSH="$HOME/.oh-my-zsh"' \ + 'ZSH_THEME="powerlevel10k/powerlevel10k"' \ + 'plugins=(git fzf)' \ + "zstyle ':omz:update' mode disabled" \ + 'POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD=true' \ + 'export HISTFILE=/commandhistory/.zsh_history' \ + 'source $ZSH/oh-my-zsh.sh' \ + 'POWERLEVEL9K_SHORTEN_STRATEGY=truncate_to_last' \ + 'POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=(user dir vcs status)' \ + 'POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=()' \ + 'POWERLEVEL9K_STATUS_OK=false' \ + 'POWERLEVEL9K_STATUS_CROSS=true' \ + > "$HOME/.zshrc" # ── Dev tools (copied from parallel download stages) ───────────────────────── # rtk (token-optimized CLI proxy) and ralphex (autonomous plan execution). diff --git a/.github/renovate.json5 b/.github/renovate.json5 index ad8b660..e25b934 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -47,7 +47,6 @@ '@anthropic-ai/claude-code', 'agent-browser', 'cli/cli', // gh — upstream .deb; apt's trixie build is frozen at 2.46.0 - 'deluan/zsh-in-docker', 'docker/cli', // docker CLI static binary (download.docker.com). // github-tags, NOT github-releases: moby/moby tags its // releases 'docker-v29.8.0', which extractVersion cannot diff --git a/.github/workflows/build-claude-bun.yml b/.github/workflows/build-claude-bun.yml index ab82f2d..f4f200b 100644 --- a/.github/workflows/build-claude-bun.yml +++ b/.github/workflows/build-claude-bun.yml @@ -41,5 +41,5 @@ jobs: context: claude-bun/.devcontainer dockerfile: claude-bun/.devcontainer/Dockerfile version-tag: ${{ needs.prepare.outputs.version-tag }} - verify-command: "bun --version && gh --version && zsh --version && zsh -ic 'printenv LC_ALL' | grep -qx C.UTF-8 && TERM=zsh-verify-sentinel zsh -ic 'printenv TERM' | grep -qx zsh-verify-sentinel" + verify-command: "bun --version && gh --version && zsh --version && zsh -ic 'echo $ZSH_THEME' | grep -qx powerlevel10k/powerlevel10k" secrets: inherit diff --git a/.github/workflows/build-claude-code.yml b/.github/workflows/build-claude-code.yml index 88226bf..370055e 100644 --- a/.github/workflows/build-claude-code.yml +++ b/.github/workflows/build-claude-code.yml @@ -75,19 +75,19 @@ jobs: matrix: include: - image-suffix: claude-code - verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'printenv LC_ALL' | grep -qx C.UTF-8 && TERM=zsh-verify-sentinel zsh -ic 'printenv TERM' | grep -qx zsh-verify-sentinel" + verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'echo $ZSH_THEME' | grep -qx powerlevel10k/powerlevel10k" runner: ubuntu-24.04 arch: amd64 - image-suffix: claude-code - verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'printenv LC_ALL' | grep -qx C.UTF-8 && TERM=zsh-verify-sentinel zsh -ic 'printenv TERM' | grep -qx zsh-verify-sentinel" + verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'echo $ZSH_THEME' | grep -qx powerlevel10k/powerlevel10k" runner: ubuntu-24.04-arm arch: arm64 - image-suffix: claude-code-sandbox - verify-command: "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'printenv LC_ALL' | grep -qx C.UTF-8 && TERM=zsh-verify-sentinel zsh -ic 'printenv TERM' | grep -qx zsh-verify-sentinel" + verify-command: "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'echo $ZSH_THEME' | grep -qx powerlevel10k/powerlevel10k" runner: ubuntu-24.04 arch: amd64 - image-suffix: claude-code-sandbox - verify-command: "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'printenv LC_ALL' | grep -qx C.UTF-8 && TERM=zsh-verify-sentinel zsh -ic 'printenv TERM' | grep -qx zsh-verify-sentinel" + verify-command: "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'echo $ZSH_THEME' | grep -qx powerlevel10k/powerlevel10k" runner: ubuntu-24.04-arm arch: arm64 runs-on: ${{ matrix.runner }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d4342e5..3f43caf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,19 +110,19 @@ jobs: add_image "claude-bun" \ "claude-bun/.devcontainer" \ "claude-bun/.devcontainer/Dockerfile" \ - "bun --version && gh --version && zsh --version && zsh -ic 'printenv LC_ALL' | grep -qx C.UTF-8 && TERM=zsh-verify-sentinel zsh -ic 'printenv TERM' | grep -qx zsh-verify-sentinel" + "bun --version && gh --version && zsh --version && zsh -ic 'echo $ZSH_THEME' | grep -qx powerlevel10k/powerlevel10k" fi if [ "$CHANGED_CLAUDE_CODE" = "true" ]; then add_image "claude-code" \ "claude-code/.devcontainer" \ "claude-code/.devcontainer/Dockerfile" \ - "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'printenv LC_ALL' | grep -qx C.UTF-8 && TERM=zsh-verify-sentinel zsh -ic 'printenv TERM' | grep -qx zsh-verify-sentinel" \ + "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'echo $ZSH_THEME' | grep -qx powerlevel10k/powerlevel10k" \ "default" add_image "claude-code-sandbox" \ "claude-code/.devcontainer" \ "claude-code/.devcontainer/Dockerfile" \ - "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'printenv LC_ALL' | grep -qx C.UTF-8 && TERM=zsh-verify-sentinel zsh -ic 'printenv TERM' | grep -qx zsh-verify-sentinel" \ + "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'echo $ZSH_THEME' | grep -qx powerlevel10k/powerlevel10k" \ "sandbox" fi diff --git a/claude-bun/.devcontainer/Dockerfile b/claude-bun/.devcontainer/Dockerfile index 8853c21..0e660da 100644 --- a/claude-bun/.devcontainer/Dockerfile +++ b/claude-bun/.devcontainer/Dockerfile @@ -1,13 +1,28 @@ ARG BUN_VERSION=1.3.5 +# ── oh-my-zsh + powerlevel10k ───────────────────────────────── +# Pinned to commit SHAs and fetched as tarballs. Neither project tags usefully -- +# oh-my-zsh has no tags at all, and powerlevel10k's last release predates its +# current master by years -- so a SHA is the only real pin. Bump deliberately: +# auto-adopting upstream shell-framework changes is what repeatedly broke this +# image, and a prompt theme does not need a 3-day release cadence. +FROM alpine:3.21 AS ohmyzsh-download +RUN apk add --no-cache curl tar +ARG OH_MY_ZSH_REF=0ee67f042872d1dfab74270c31867771ca35aef4 +ARG POWERLEVEL10K_REF=d05a1b00f9a61f9578bf9dc19b8451942dde8734 +RUN set -eux; \ + mkdir -p /omz/custom/themes/powerlevel10k; \ + curl -fsSL "https://github.com/ohmyzsh/ohmyzsh/archive/${OH_MY_ZSH_REF}.tar.gz" \ + | tar -xz -C /omz --strip-components=1; \ + curl -fsSL "https://github.com/romkatv/powerlevel10k/archive/${POWERLEVEL10K_REF}.tar.gz" \ + | tar -xz -C /omz/custom/themes/powerlevel10k --strip-components=1 + FROM oven/bun:${BUN_VERSION}-slim ARG TZ=UTC ENV TZ="$TZ" ARG CLAUDE_CODE_VERSION=latest -# renovate: datasource=github-releases depName=deluan/zsh-in-docker -ARG ZSH_IN_DOCKER_VERSION=1.2.0 # renovate: datasource=github-releases depName=cli/cli ARG GH_VERSION=2.100.0 @@ -72,41 +87,42 @@ ENV PATH="$BUN_INSTALL/bin:$PATH" # Set the default shell to zsh rather than sh (/usr/bin/zsh, matching the other # images; /bin/zsh is the same binary via usrmerge) ENV SHELL=/usr/bin/zsh +# Only LANG, and only C.UTF-8 — what the official images do (ruby sets exactly +# this; node/debian/python set nothing). LC_ALL would override every LC_* category +# and silently defeat a consuming project's containerEnv. TERM is deliberately +# unset: no official image sets it, an image ENV beats the tty value `docker exec -t` +# supplies, and tput/clear failing without a terminal is correct behaviour. ENV LANG=C.UTF-8 -ENV LC_ALL=C.UTF-8 # zsh + oh-my-zsh + powerlevel10k. # -# Supply chain: ZSH_IN_DOCKER_VERSION pins only this ~190-line wrapper. The -# wrapper then curls oh-my-zsh's install.sh from master and clones powerlevel10k -# at HEAD -- both unpinned, both executed at build time, and its inner curl has -# no -f, so an upstream 5xx body would be run as shell. Accepted deliberately -# for shell tooling; pin both refs to commit SHAs if that trade stops being -# acceptable. -# -# The template opens with a preamble of env exports we don't want: en_US.UTF-8 -# (which -x never generates — the slim bases carry only C/C.utf8/POSIX, so glibc -# falls back to C and mangles multibyte I/O) and a TERM that overrides whatever -# the terminal provides, cutting colour support from 256 to 8. -# -# Delete the whole preamble rather than matching individual lines. v1.2.0 wrote -# `export TERM=xterm`; v1.2.1 writes `[ -z "$TERM" ] && export TERM=xterm` inside -# an UNQUOTED heredoc, so $TERM expands at build time (unset) to `[ -z "" ] && ...` -# — always true, and invisible to a pattern anchored on the old text. Cutting to -# the `##### Zsh/Oh-my-Zsh Configuration` marker is stable across both. The grep -# guard matters: without it a marker rename would make sed delete the whole file. -# Locale now comes from ENV above, which also covers sh/bash, lifecycle commands -# and the VS Code server — not just interactive zsh. -RUN curl -fsSL -o /tmp/zsh-in-docker.sh \ - "https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh" \ - && sh /tmp/zsh-in-docker.sh \ - -p git \ - -p fzf \ - -a "export HISTFILE=/commandhistory/.bash_history" \ - -x \ - && rm /tmp/zsh-in-docker.sh \ - && grep -q '^##### Zsh/Oh-my-Zsh Configuration' "$HOME/.zshrc" \ - && sed -i '/^##### Zsh\/Oh-my-Zsh Configuration/,$!d' "$HOME/.zshrc" +# Written as bun: $HOME during RUN follows USER, so this must not run as root. +USER bun +# oh-my-zsh tree is COPYed from the parallel download stage above, so the final +# image needs no git or curl for it and nothing is fetched at build time here. +COPY --from=ohmyzsh-download --chown=bun:bun /omz /home/bun/.oh-my-zsh + +# .zshrc is written here rather than by an installer, so nothing upstream can +# inject env overrides (locale/TERM) that then have to be patched back out. +# The wizard flag matters: without a POWERLEVEL9K_* config powerlevel10k starts +# its interactive configurator on first shell, which blocks a container. +# $HOME and $ZSH are single-quoted on purpose: they must reach .zshrc +# unexpanded so zsh resolves them at runtime, not at build time. +# hadolint ignore=SC2016 +RUN printf '%s\n' \ + 'export ZSH="$HOME/.oh-my-zsh"' \ + 'ZSH_THEME="powerlevel10k/powerlevel10k"' \ + 'plugins=(git fzf)' \ + "zstyle ':omz:update' mode disabled" \ + 'POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD=true' \ + 'export HISTFILE=/commandhistory/.bash_history' \ + 'source $ZSH/oh-my-zsh.sh' \ + 'POWERLEVEL9K_SHORTEN_STRATEGY=truncate_to_last' \ + 'POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=(user dir vcs status)' \ + 'POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=()' \ + 'POWERLEVEL9K_STATUS_OK=false' \ + 'POWERLEVEL9K_STATUS_CROSS=true' \ + > "$HOME/.zshrc" # Install Claude Code globally using bun (run as root to avoid EACCES linking) USER root diff --git a/claude-code/.devcontainer/Dockerfile b/claude-code/.devcontainer/Dockerfile index 4807d90..9c65524 100644 --- a/claude-code/.devcontainer/Dockerfile +++ b/claude-code/.devcontainer/Dockerfile @@ -17,6 +17,23 @@ # ── rtk (token-optimized CLI proxy) ────────────────────────────────────── # Version pinned and kept up to date by Renovate (see .github/renovate.json5). +# ── oh-my-zsh + powerlevel10k ───────────────────────────────── +# Pinned to commit SHAs and fetched as tarballs. Neither project tags usefully -- +# oh-my-zsh has no tags at all, and powerlevel10k's last release predates its +# current master by years -- so a SHA is the only real pin. Bump deliberately: +# auto-adopting upstream shell-framework changes is what repeatedly broke this +# image, and a prompt theme does not need a 3-day release cadence. +FROM alpine:3.21 AS ohmyzsh-download +RUN apk add --no-cache curl tar +ARG OH_MY_ZSH_REF=0ee67f042872d1dfab74270c31867771ca35aef4 +ARG POWERLEVEL10K_REF=d05a1b00f9a61f9578bf9dc19b8451942dde8734 +RUN set -eux; \ + mkdir -p /omz/custom/themes/powerlevel10k; \ + curl -fsSL "https://github.com/ohmyzsh/ohmyzsh/archive/${OH_MY_ZSH_REF}.tar.gz" \ + | tar -xz -C /omz --strip-components=1; \ + curl -fsSL "https://github.com/romkatv/powerlevel10k/archive/${POWERLEVEL10K_REF}.tar.gz" \ + | tar -xz -C /omz/custom/themes/powerlevel10k --strip-components=1 + FROM alpine:3.21 AS rtk-download RUN apk add --no-cache curl # renovate: datasource=github-releases depName=rtk-ai/rtk @@ -48,8 +65,6 @@ FROM node:24-trixie-slim AS base # renovate: datasource=github-releases depName=cli/cli ARG GH_VERSION=2.100.0 -# renovate: datasource=github-releases depName=deluan/zsh-in-docker -ARG ZSH_IN_DOCKER_VERSION=1.2.0 # System packages shared by all targets # - ca-certificates: SSL/TLS for HTTPS connections @@ -121,8 +136,12 @@ USER node ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global ENV PATH=$PATH:/usr/local/share/npm-global/bin ENV SHELL=/usr/bin/zsh +# Only LANG, and only C.UTF-8 — what the official images do (ruby sets exactly +# this; node/debian/python set nothing). LC_ALL would override every LC_* category +# and silently defeat a consuming project's containerEnv. TERM is deliberately +# unset: no official image sets it, an image ENV beats the tty value `docker exec -t` +# supplies, and tput/clear failing without a terminal is correct behaviour. ENV LANG=C.UTF-8 -ENV LC_ALL=C.UTF-8 ENV EDITOR="code --wait" ENV VISUAL="code --wait" @@ -139,40 +158,33 @@ RUN curl https://mise.run | sh \ SHELL ["/bin/sh", "-c"] # ── zsh + oh-my-zsh + powerlevel10k ────────────────────────────────────────── -# Supply chain: ZSH_IN_DOCKER_VERSION pins only this ~190-line wrapper. The -# wrapper then curls oh-my-zsh's install.sh from master and clones powerlevel10k -# at HEAD -- both unpinned, both executed at build time, and its inner curl has -# no -f, so an upstream 5xx body would be run as shell. These bumps auto-merge -# and publish to ghcr.io unreviewed. Accepted deliberately for shell tooling; -# pin both refs to commit SHAs if that trade stops being acceptable. -# -# -x skips the script's own dependency install: zsh already comes from the apt -# block above with --no-install-recommends, which also avoids dragging in -# locales-all. Runs as node so oh-my-zsh lands in /home/node. +# Written as node: $HOME during RUN follows USER, so this must not run as root. USER node -# The template opens with a preamble of env exports we don't want: en_US.UTF-8 -# (which -x never generates — the slim bases carry only C/C.utf8/POSIX, so glibc -# falls back to C and mangles multibyte I/O) and a TERM that overrides whatever -# the terminal provides, cutting colour support from 256 to 8. -# -# Delete the whole preamble rather than matching individual lines. v1.2.0 wrote -# `export TERM=xterm`; v1.2.1 writes `[ -z "$TERM" ] && export TERM=xterm` inside -# an UNQUOTED heredoc, so $TERM expands at build time (unset) to `[ -z "" ] && ...` -# — always true, and invisible to a pattern anchored on the old text. Cutting to -# the `##### Zsh/Oh-my-Zsh Configuration` marker is stable across both. The grep -# guard matters: without it a marker rename would make sed delete the whole file. -# Locale now comes from ENV above, which also covers sh/bash, lifecycle commands -# and the VS Code server — not just interactive zsh. -RUN curl -fsSL -o /tmp/zsh-in-docker.sh \ - "https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh" \ - && sh /tmp/zsh-in-docker.sh \ - -p git \ - -p fzf \ - -a "export HISTFILE=/commandhistory/.zsh_history" \ - -x \ - && rm /tmp/zsh-in-docker.sh \ - && grep -q '^##### Zsh/Oh-my-Zsh Configuration' "$HOME/.zshrc" \ - && sed -i '/^##### Zsh\/Oh-my-Zsh Configuration/,$!d' "$HOME/.zshrc" +# oh-my-zsh tree is COPYed from the parallel download stage above, so the final +# image needs no git or curl for it and nothing is fetched at build time here. +COPY --from=ohmyzsh-download --chown=node:node /omz /home/node/.oh-my-zsh + +# .zshrc is written here rather than by an installer, so nothing upstream can +# inject env overrides (locale/TERM) that then have to be patched back out. +# The wizard flag matters: without a POWERLEVEL9K_* config powerlevel10k starts +# its interactive configurator on first shell, which blocks a container. +# $HOME and $ZSH are single-quoted on purpose: they must reach .zshrc +# unexpanded so zsh resolves them at runtime, not at build time. +# hadolint ignore=SC2016 +RUN printf '%s\n' \ + 'export ZSH="$HOME/.oh-my-zsh"' \ + 'ZSH_THEME="powerlevel10k/powerlevel10k"' \ + 'plugins=(git fzf)' \ + "zstyle ':omz:update' mode disabled" \ + 'POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD=true' \ + 'export HISTFILE=/commandhistory/.zsh_history' \ + 'source $ZSH/oh-my-zsh.sh' \ + 'POWERLEVEL9K_SHORTEN_STRATEGY=truncate_to_last' \ + 'POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=(user dir vcs status)' \ + 'POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=()' \ + 'POWERLEVEL9K_STATUS_OK=false' \ + 'POWERLEVEL9K_STATUS_CROSS=true' \ + > "$HOME/.zshrc" ENV PATH="/home/node/.local/share/mise/shims:$PATH" ENV MISE_TRUSTED_CONFIG_PATHS="/workspace" diff --git a/claude-code/README.md b/claude-code/README.md index d8e88ef..342309e 100644 --- a/claude-code/README.md +++ b/claude-code/README.md @@ -409,9 +409,10 @@ cat ~/.claude/plugins/cache/claude-plugins-official/playwright/*/.mcp.json | `CLAUDE_CODE_VERSION` | `2.1.273` | Claude Code CLI version (Renovate-managed) | | `AGENT_BROWSER_VERSION` | `0.37.1` | agent-browser version, default target only (Renovate-managed) | | `GH_VERSION` | `2.100.0` | GitHub CLI version — installed from the upstream `.deb`, not apt (Renovate-managed) | -| `ZSH_IN_DOCKER_VERSION` | `1.2.0` | zsh + oh-my-zsh + powerlevel10k installer (Renovate-managed) | +| `OH_MY_ZSH_REF` | commit SHA | oh-my-zsh, pinned by commit — **not** Renovate-managed, bump deliberately | +| `POWERLEVEL10K_REF` | commit SHA | powerlevel10k, pinned by commit — **not** Renovate-managed, bump deliberately | -The six Renovate-managed args carry `# renovate:` annotations in the Dockerfile; edit them by +The five Renovate-managed args carry `# renovate:` annotations in the Dockerfile; edit them by hand only for a local build. Bumps land as auto-merged PRs — see [Automatic Rebuilds](#automatic-rebuilds). ## Building Locally / Local Fallback diff --git a/docs/superpowers/specs/2026-07-20-renovate-tool-updates-design.md b/docs/superpowers/specs/2026-07-20-renovate-tool-updates-design.md index 3784029..3942fb7 100644 --- a/docs/superpowers/specs/2026-07-20-renovate-tool-updates-design.md +++ b/docs/superpowers/specs/2026-07-20-renovate-tool-updates-design.md @@ -30,7 +30,8 @@ version** — no more time-based rebuilds. ## Non-goals > **Partly superseded.** `Go`, `Docker` and the `hugo-bun-node` musl `Node` -> build were later brought under Renovate, alongside `gh` and `zsh-in-docker`; +> build were later brought under Renovate, alongside `gh`; oh-my-zsh and +> powerlevel10k are pinned by commit SHA and deliberately left unmanaged; > `git-delta` was removed from every image. `Bun`, `Hugo` and base images remain > out of scope as written below. See `.github/renovate.json5` for the current > package list. From d9b8e08811257dd92708560a6bdc8b7fef67b283 Mon Sep 17 00:00:00 2001 From: Serge Gatezh <2880401+gatezh@users.noreply.github.com> Date: Wed, 16 Sep 2026 20:42:07 -0600 Subject: [PATCH 5/6] fix(ci): stop the shell expanding $ZSH_THEME out of the verify command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The theme assertion added in ce1f39f was written as zsh -ic 'echo $ZSH_THEME' | grep -qx powerlevel10k/powerlevel10k but in ci.yml it sits inside a double-quoted bash argument to add_image, so the runner's shell expanded $ZSH_THEME -- to empty, since it is a zsh variable the runner has never heard of -- before the string was ever stored. What actually ran in the container was zsh -ic 'echo ' | grep -qx powerlevel10k/powerlevel10k which prints an empty line and fails. Build · claude-bun went red on amd64. Uses typeset -p ZSH_THEME instead, which contains no $ at all and so survives every quoting layer identically -- ci.yml builds its string in bash while the publish workflows pass theirs through an env var, and escaping would have had to differ between the two. This slipped through local verification because the harness extracted the verify string from the YAML text and re-quoted it in single quotes, which preserved $ZSH_THEME. The harness now assigns the original double-quoted literal through bash first, so runner-side expansion is reproduced; confirmed by feeding it the broken form, which it now fails. --- .github/workflows/build-claude-bun.yml | 2 +- .github/workflows/build-claude-code.yml | 8 ++++---- .github/workflows/ci.yml | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-claude-bun.yml b/.github/workflows/build-claude-bun.yml index f4f200b..e296ea0 100644 --- a/.github/workflows/build-claude-bun.yml +++ b/.github/workflows/build-claude-bun.yml @@ -41,5 +41,5 @@ jobs: context: claude-bun/.devcontainer dockerfile: claude-bun/.devcontainer/Dockerfile version-tag: ${{ needs.prepare.outputs.version-tag }} - verify-command: "bun --version && gh --version && zsh --version && zsh -ic 'echo $ZSH_THEME' | grep -qx powerlevel10k/powerlevel10k" + verify-command: "bun --version && gh --version && zsh --version && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k" secrets: inherit diff --git a/.github/workflows/build-claude-code.yml b/.github/workflows/build-claude-code.yml index 370055e..fc78ddf 100644 --- a/.github/workflows/build-claude-code.yml +++ b/.github/workflows/build-claude-code.yml @@ -75,19 +75,19 @@ jobs: matrix: include: - image-suffix: claude-code - verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'echo $ZSH_THEME' | grep -qx powerlevel10k/powerlevel10k" + verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k" runner: ubuntu-24.04 arch: amd64 - image-suffix: claude-code - verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'echo $ZSH_THEME' | grep -qx powerlevel10k/powerlevel10k" + verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k" runner: ubuntu-24.04-arm arch: arm64 - image-suffix: claude-code-sandbox - verify-command: "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'echo $ZSH_THEME' | grep -qx powerlevel10k/powerlevel10k" + verify-command: "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k" runner: ubuntu-24.04 arch: amd64 - image-suffix: claude-code-sandbox - verify-command: "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'echo $ZSH_THEME' | grep -qx powerlevel10k/powerlevel10k" + verify-command: "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k" runner: ubuntu-24.04-arm arch: arm64 runs-on: ${{ matrix.runner }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f43caf..8d7ffc0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,19 +110,19 @@ jobs: add_image "claude-bun" \ "claude-bun/.devcontainer" \ "claude-bun/.devcontainer/Dockerfile" \ - "bun --version && gh --version && zsh --version && zsh -ic 'echo $ZSH_THEME' | grep -qx powerlevel10k/powerlevel10k" + "bun --version && gh --version && zsh --version && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k" fi if [ "$CHANGED_CLAUDE_CODE" = "true" ]; then add_image "claude-code" \ "claude-code/.devcontainer" \ "claude-code/.devcontainer/Dockerfile" \ - "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'echo $ZSH_THEME' | grep -qx powerlevel10k/powerlevel10k" \ + "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k" \ "default" add_image "claude-code-sandbox" \ "claude-code/.devcontainer" \ "claude-code/.devcontainer/Dockerfile" \ - "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'echo $ZSH_THEME' | grep -qx powerlevel10k/powerlevel10k" \ + "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k" \ "sandbox" fi From 606d1a2aa36505eca9613ba4ee52dd39851ff7b8 Mon Sep 17 00:00:00 2001 From: Serge Gatezh <2880401+gatezh@users.noreply.github.com> Date: Wed, 16 Sep 2026 20:45:03 -0600 Subject: [PATCH 6/6] docs(claude-code): stop duplicating version numbers in the build-args table The table restated each ARG's current value, so every Renovate bump silently falsified it -- it drifted twice during this branch alone, once for rtk and claude-code and again for a claude-code bump landing mid-review. A number that is wrong more often than right is worse than no number. The table now says who updates each arg and why it exists, and points at the Dockerfile for current values. --- claude-code/README.md | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/claude-code/README.md b/claude-code/README.md index 342309e..fc00253 100644 --- a/claude-code/README.md +++ b/claude-code/README.md @@ -402,15 +402,19 @@ cat ~/.claude/plugins/cache/claude-plugins-official/playwright/*/.mcp.json ## Build Args -| Arg | Default | Description | -|-----|---------|-------------| -| `RTK_VERSION` | `0.49.0` | rtk version (Renovate-managed) | -| `RALPHEX_VERSION` | `1.7.0` | ralphex version (Renovate-managed) | -| `CLAUDE_CODE_VERSION` | `2.1.273` | Claude Code CLI version (Renovate-managed) | -| `AGENT_BROWSER_VERSION` | `0.37.1` | agent-browser version, default target only (Renovate-managed) | -| `GH_VERSION` | `2.100.0` | GitHub CLI version — installed from the upstream `.deb`, not apt (Renovate-managed) | -| `OH_MY_ZSH_REF` | commit SHA | oh-my-zsh, pinned by commit — **not** Renovate-managed, bump deliberately | -| `POWERLEVEL10K_REF` | commit SHA | powerlevel10k, pinned by commit — **not** Renovate-managed, bump deliberately | +Current values live in [the Dockerfile](.devcontainer/Dockerfile) and are not repeated +here — Renovate bumps several of them weekly, so any number written below would be wrong +more often than right. + +| Arg | Updated by | Description | +|-----|------------|-------------| +| `RTK_VERSION` | Renovate | rtk | +| `RALPHEX_VERSION` | Renovate | ralphex | +| `CLAUDE_CODE_VERSION` | Renovate | Claude Code CLI | +| `AGENT_BROWSER_VERSION` | Renovate | agent-browser, default target only | +| `GH_VERSION` | Renovate | GitHub CLI — from the upstream `.deb`, not apt (trixie freezes gh at 2.46.0) | +| `OH_MY_ZSH_REF` | by hand | oh-my-zsh, pinned to a commit SHA | +| `POWERLEVEL10K_REF` | by hand | powerlevel10k, pinned to a commit SHA | The five Renovate-managed args carry `# renovate:` annotations in the Dockerfile; edit them by hand only for a local build. Bumps land as auto-merged PRs — see [Automatic Rebuilds](#automatic-rebuilds).