From 788a0d6249402776c9ee0a8ea3ee244c4632a3b1 Mon Sep 17 00:00:00 2001 From: Serge Gatezh <2880401+gatezh@users.noreply.github.com> Date: Wed, 9 Sep 2026 09:05:52 -0600 Subject: [PATCH 1/3] feat(claude-code): bake happy CLI into default and sandbox images Install the happy.engineering CLI (npm `happy`) in the base stage of the claude-code Dockerfile so both targets carry it, pinned via HAPPY_VERSION and bumped by Renovate alongside the other agent tools. CLI only: the daemon stays opt-in per project (compose `command:` + a ~/.happy volume), documented in a new README section. CI verifies `happy --version` in both targets. Co-Authored-By: Claude Fable 5.1 --- .github/renovate.json5 | 5 +++-- .github/workflows/ci.yml | 4 ++-- README.md | 2 +- claude-code/.devcontainer/Dockerfile | 13 +++++++++++++ claude-code/README.md | 16 ++++++++++++++-- 5 files changed, 33 insertions(+), 7 deletions(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index d0d24c4..cd274e9 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -6,7 +6,7 @@ // github-actions managers open PRs for base images or action pins (out of scope). enabledManagers: ['custom.regex'], - // Pin + auto-update the four dev tools these images used to pull from + // Pin + auto-update the five dev tools these images used to pull from // "latest" at build time. Replaces the old daily rebuild cron: a Renovate // bump PR (auto-merged on green CI) triggers the existing push-based image // build. No upstream release -> no PR -> no rebuild. @@ -28,7 +28,7 @@ extractVersion: '^v?(?.+)$', }, { - // Group the four tools into one PR and auto-merge once CI passes. + // Group the five tools into one PR and auto-merge once CI passes. // platformAutomerge:false => Renovate performs the merge itself only // after it observes the branch tests are green, so CI gating needs no // branch-protection rule. (Optional hardening: enable branch protection @@ -38,6 +38,7 @@ 'rtk-ai/rtk', 'umputun/ralphex', '@anthropic-ai/claude-code', + 'happy', 'agent-browser', ], groupName: 'devcontainer agent tools', diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c559235..b9ceca0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -118,12 +118,12 @@ jobs: 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 && happy --version | grep -q 'happy 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" \ "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 && happy --version | grep -q 'happy 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" \ "sandbox" fi diff --git a/README.md b/README.md index fff3e7c..861f88b 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ Images from this repository are built and published to GitHub Container Registry ### Automatically, via Renovate The agent tooling in the `claude-code` and `ralphex-fe` images — `rtk`, `ralphex`, the Claude Code -CLI, and `agent-browser` — is pinned as `ARG`s carrying `# renovate:` annotations. Renovate watches +CLI, `happy`, and `agent-browser` — is pinned as `ARG`s carrying `# renovate:` annotations. Renovate watches their releases and opens a single grouped bump PR when one ships; CI verifies it, it auto-merges, and that merge rebuilds the affected images. No upstream release means no PR and no rebuild. Scope and grouping live in [`.github/renovate.json5`](./.github/renovate.json5); the Dependency Dashboard diff --git a/claude-code/.devcontainer/Dockerfile b/claude-code/.devcontainer/Dockerfile index 8b68054..8f32262 100644 --- a/claude-code/.devcontainer/Dockerfile +++ b/claude-code/.devcontainer/Dockerfile @@ -187,6 +187,19 @@ USER node ARG CLAUDE_CODE_VERSION=2.1.266 RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} +# ── happy CLI (remote control for Claude Code) ──────────────────────────────── +# happy.engineering lets the Happy phone/web app start and drive Claude Code +# sessions inside the container. Only the CLI is baked in; the daemon is opt-in +# per project (compose `command:` + a volume for ~/.happy — see README +# "Optional: remote control with happy"). Nothing runs unless a project asks. +# Installed in the base stage so both targets get it; as the node user for the +# same ownership reason as Claude Code above. +# Pinned: the daemon restarts itself whenever the installed version changes, so +# a floating install would turn every rebuild into a surprise restart. +# renovate: datasource=npm depName=happy +ARG HAPPY_VERSION=1.2.3 +RUN npm install -g happy@${HAPPY_VERSION} + # ─── DEFAULT — full dev environment ─────────────────────────────────────────── FROM base AS default diff --git a/claude-code/README.md b/claude-code/README.md index 173c0e9..5a5fb0e 100644 --- a/claude-code/README.md +++ b/claude-code/README.md @@ -21,6 +21,7 @@ Projects consume these pre-built images and control their own tool versions via | 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 | +| happy | npm global install, pinned `ARG` bumped by Renovate | [happy.engineering](https://happy.engineering) CLI — lets the Happy phone/web app start Claude Code sessions in the container. CLI only; the daemon is opt-in per project | **Both targets:** system Chromium + `fonts-freefont-ttf` (used by Playwright and the Playwright MCP plugin via `/usr/bin/chromium`) @@ -42,7 +43,7 @@ Both variants are built for: ## Automatic Rebuilds -The image rebuilds automatically whenever one of its pinned tools — Claude Code, agent-browser, rtk, or ralphex — publishes a new release: Renovate opens a version-bump PR, CI verifies it, it auto-merges, and the merge builds the image on native runners for both amd64 and arm64 (no QEMU emulation). Manual rebuilds can be triggered via the "Run workflow" button in the Actions UI. +The image rebuilds automatically whenever one of its pinned tools — Claude Code, happy, agent-browser, rtk, or ralphex — publishes a new release: Renovate opens a version-bump PR, CI verifies it, it auto-merges, and the merge builds the image on native runners for both amd64 and arm64 (no QEMU emulation). Manual rebuilds can be triggered via the "Run workflow" button in the Actions UI. ## Quick Start @@ -162,6 +163,16 @@ git add .claude/skills/devcontainer-upstream-sync/SKILL.md After the one-time copy, the skill manages its own updates. +### Optional: remote control with happy + +Both targets ship the [happy.engineering](https://happy.engineering) CLI (`happy`), pinned via `HAPPY_VERSION` and bumped by Renovate like the other agent tools. Nothing runs by default — the image `CMD` is still `sleep infinity`. To let the Happy phone/web app start Claude Code sessions in a project's container: + +1. **Persist the pairing.** happy keeps its credentials and machine id in `~/.happy` (`HAPPY_HOME_DIR` overrides). Add a named volume for `/home/node/.happy` next to the Claude config volume so pairing survives rebuilds. +2. **Run the daemon as the container's main process.** Point the compose service `command:` at a script that runs `happy daemon start-sync` and restarts it when it exits, add `restart: unless-stopped`, and set `"shutdownAction": "none"` in `devcontainer.json` so closing VS Code does not stop the container. +3. **Pair once:** `docker compose exec devcontainer happy auth login`, then scan the QR code in the app. Start desk sessions with `happy claude` instead of `claude` so they show up in the app too. + +The daemon needs no inbound ports; it opens an outbound connection to happy's backend (`api.cluster-fluster.com` by default, `HAPPY_SERVER_URL` overrides). **Sandbox:** add that host to your `init-firewall.sh` allowlist or the daemon cannot connect. The daemon restarts itself when the installed happy version changes, so a Renovate bump plus image pull restarts running daemons on the next rebuild — that is why the version is pinned rather than floating. + ### Sandbox Authentication The sandbox firewall blocks outbound traffic, so `claude login` (which opens a browser OAuth flow) won't work inside the container. Instead, generate a token on the host and inject it via environment variable. @@ -408,9 +419,10 @@ cat ~/.claude/plugins/cache/claude-plugins-official/playwright/*/.mcp.json | `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) | +| `HAPPY_VERSION` | `1.2.3` | happy.engineering 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 +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 From 6d22bd191e029973c4e74794e1748eb68a8d85a8 Mon Sep 17 00:00:00 2001 From: Serge Gatezh <2880401+gatezh@users.noreply.github.com> Date: Wed, 9 Sep 2026 12:01:31 -0600 Subject: [PATCH 2/3] refactor(claude-code): ship happy as its own target, not in base MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the review on #131. happy stays, but it stops being something every consumer of these images pays for. Size. Installed the way the first commit did it, happy added 1638 MB to the shared `base` layer (1844 MB -> 3482 MB, +89%), and both targets inherited it. Two cleanups inside the install RUN cut that to 785 MB: drop tools/archives (106 MB of prebuilt difftastic + ripgrep tarballs for all six platform/arch combos, dead once postinstall has unpacked this platform's pair into tools/unpacked, which is the only path the runtime resolves difft and rg from) and clean the npm cache (680 MB, unreclaimable from a later layer). The remaining ~850 MB is upstream packaging: happy vendors its own @anthropic-ai/claude-agent-sdk and sandbox-runtime — a second copy of what `base` already installs, with its Bedrock/Vertex/OpenTelemetry fan-out — and ships the self-hostable happy server (fastify, http-proxy, expo-server-sdk, drizzle-orm, libsql) in the same npm package as the CLI. Placement. Even at 785 MB it does not belong in `base`. The sandbox firewall blocks happy's relay, so that target was carrying a tool it cannot use, and allowlisting api.cluster-fluster.com to fix that would punch a hole in the one restriction the sandbox exists to enforce — for a channel that can execute code in the container. happy now builds `FROM default AS happy` and publishes as claude-code-happy; `default` and `sandbox` are byte-for-byte what they were before this branch. Verification. `happy --version` is not a usable probe: it prints the version banner, falls through into the interactive auth TUI, fails on a non-TTY stdin, and still exits 0 — as does every other subcommand and even an unknown flag. The check now asserts the installed package with `npm ls -g --depth=0 happy`, which exits 1 when absent and never starts the TUI. Added to build-claude-code.yml too, which the first commit missed entirely, so the published image was never checked. Docs. The Build Args table had drifted on four of six rows because it mirrors values Renovate rewrites; Renovate-managed rows now point at the Dockerfile instead of carrying a copy that goes stale in days. Measured on linux/arm64, layer-sum: default 1844 MB (unchanged), sandbox 1731 MB (unchanged), happy 2629 MB. All three targets build, all three CI verify strings pass against the built images, the happy string correctly fails against default, and happy is absent from both default and sandbox. hadolint findings unchanged from master (11 DL3066 infos, all pre-existing); actionlint clean. --- .github/workflows/build-claude-code.yml | 34 +++++++++- .github/workflows/ci.yml | 9 ++- .../workflows/cleanup-claude-code-ghcr.yml | 7 +- README.md | 12 +++- claude-code/.devcontainer/Dockerfile | 67 +++++++++++++++---- claude-code/README.md | 46 +++++++++---- 6 files changed, 140 insertions(+), 35 deletions(-) diff --git a/.github/workflows/build-claude-code.yml b/.github/workflows/build-claude-code.yml index 4977ba6..dabba71 100644 --- a/.github/workflows/build-claude-code.yml +++ b/.github/workflows/build-claude-code.yml @@ -67,9 +67,33 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + build-happy: + uses: docker/github-builder/.github/workflows/build.yml@v1 + permissions: + contents: read + packages: write + id-token: write + with: + output: image + push: true + target: happy + context: claude-code/.devcontainer + + platforms: linux/amd64,linux/arm64 + meta-images: ghcr.io/gatezh/devcontainers/claude-code-happy + meta-tags: | + type=raw,value=latest + type=sha,prefix= + type=raw,value={{date 'YYYYMMDD'}} + secrets: + registry-auths: | + - registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + verify: name: Verify ${{ matrix.image-suffix }} (${{ matrix.arch }}) - needs: [build-default, build-sandbox] + needs: [build-default, build-sandbox, build-happy] strategy: fail-fast: false matrix: @@ -90,6 +114,14 @@ jobs: 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" runner: ubuntu-24.04-arm arch: arm64 + - image-suffix: claude-code-happy + 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 && npm ls -g --depth=0 happy" + runner: ubuntu-24.04 + arch: amd64 + - image-suffix: claude-code-happy + 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 && npm ls -g --depth=0 happy" + runner: ubuntu-24.04-arm + arch: arm64 runs-on: ${{ matrix.runner }} steps: - name: Log in to GHCR diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b96e96..3985fca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -117,13 +117,18 @@ jobs: add_image "claude-code" \ "claude-code/.devcontainer" \ "claude-code/.devcontainer/Dockerfile" \ - "bun --version || true && claude --version && happy --version | grep -q 'happy 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 && 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" \ "default" add_image "claude-code-sandbox" \ "claude-code/.devcontainer" \ "claude-code/.devcontainer/Dockerfile" \ - "claude --version && happy --version | grep -q 'happy 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 && 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" \ "sandbox" + add_image "claude-code-happy" \ + "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 && npm ls -g --depth=0 happy" \ + "happy" fi if [ "$CHANGED_HUGO_BUN" = "true" ]; then diff --git a/.github/workflows/cleanup-claude-code-ghcr.yml b/.github/workflows/cleanup-claude-code-ghcr.yml index 34de036..733b342 100644 --- a/.github/workflows/cleanup-claude-code-ghcr.yml +++ b/.github/workflows/cleanup-claude-code-ghcr.yml @@ -1,7 +1,8 @@ # Manually-triggered cleanup of old GHCR versions for the claude-code images. # -# Scope: ghcr.io/gatezh/devcontainers/claude-code and -# ghcr.io/gatezh/devcontainers/claude-code-sandbox only. +# Scope: ghcr.io/gatezh/devcontainers/claude-code, +# ghcr.io/gatezh/devcontainers/claude-code-sandbox and +# ghcr.io/gatezh/devcontainers/claude-code-happy only. # Other packages in this repo (bun, hugo-bun, hugo-bun-node, ralphex-fe, # claude-bun) are unreachable from this workflow. # @@ -36,7 +37,7 @@ jobs: strategy: fail-fast: false matrix: - package: [claude-code, claude-code-sandbox] + package: [claude-code, claude-code-sandbox, claude-code-happy] steps: - name: Delete old container versions uses: dataaxiom/ghcr-cleanup-action@v1 diff --git a/README.md b/README.md index 861f88b..9357c91 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This repository contains Dockerfiles for custom Docker images hosted on GitHub C ### Devcontainer Images -- **[claude-code](./claude-code/README.md)** - Shared Claude Code devcontainer image (default + sandbox variants) +- **[claude-code](./claude-code/README.md)** - Shared Claude Code devcontainer image (default + sandbox + happy variants) - **[bun](./bun/README.md)** - Bun development container - **[claude-bun](./claude-bun/README.md)** - Claude Code development container with firewall sandbox - **[hugo-bun](./hugo-bun/README.md)** - Hugo Extended + Bun development container @@ -40,7 +40,7 @@ image-name/ ### claude-code -Shared devcontainer base image for Claude Code projects. Two variants from a single multi-stage Dockerfile: **default** (full dev environment with agent-browser) and **sandbox** (network-restricted with iptables firewall). Projects consume pre-built images and control tool versions via `.mise.toml`. Rebuilds when its pinned tools receive a new release (managed by Renovate), not on a schedule. +Shared devcontainer base image for Claude Code projects. Three variants from a single multi-stage Dockerfile: **default** (full dev environment with agent-browser), **sandbox** (network-restricted with iptables firewall), and **happy** (default plus the happy CLI for phone/web remote control). Projects consume pre-built images and control tool versions via `.mise.toml`. Rebuilds when its pinned tools receive a new release (managed by Renovate), not on a schedule. **Usage in other projects:** @@ -55,6 +55,12 @@ Shared devcontainer base image for Claude Code projects. Two variants from a sin "image": "ghcr.io/gatezh/devcontainers/claude-code-sandbox:latest", "capAdd": ["NET_ADMIN", "NET_RAW"] } + +// Happy variant — default plus the happy CLI. ~785 MB larger than default, +// so only worth pulling if you actually pair a phone or the web app. +{ + "image": "ghcr.io/gatezh/devcontainers/claude-code-happy:latest" +} ``` See the [claude-code README](./claude-code/README.md) for full setup guide. @@ -155,7 +161,7 @@ Images from this repository are built and published to GitHub Container Registry ### Automatically, via Renovate The agent tooling in the `claude-code` and `ralphex-fe` images — `rtk`, `ralphex`, the Claude Code -CLI, `happy`, and `agent-browser` — is pinned as `ARG`s carrying `# renovate:` annotations. Renovate watches +CLI, `agent-browser`, and `happy` — is pinned as `ARG`s carrying `# renovate:` annotations. Renovate watches their releases and opens a single grouped bump PR when one ships; CI verifies it, it auto-merges, and that merge rebuilds the affected images. No upstream release means no PR and no rebuild. Scope and grouping live in [`.github/renovate.json5`](./.github/renovate.json5); the Dependency Dashboard diff --git a/claude-code/.devcontainer/Dockerfile b/claude-code/.devcontainer/Dockerfile index 8f32262..e064b5d 100644 --- a/claude-code/.devcontainer/Dockerfile +++ b/claude-code/.devcontainer/Dockerfile @@ -187,19 +187,6 @@ USER node ARG CLAUDE_CODE_VERSION=2.1.266 RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} -# ── happy CLI (remote control for Claude Code) ──────────────────────────────── -# happy.engineering lets the Happy phone/web app start and drive Claude Code -# sessions inside the container. Only the CLI is baked in; the daemon is opt-in -# per project (compose `command:` + a volume for ~/.happy — see README -# "Optional: remote control with happy"). Nothing runs unless a project asks. -# Installed in the base stage so both targets get it; as the node user for the -# same ownership reason as Claude Code above. -# Pinned: the daemon restarts itself whenever the installed version changes, so -# a floating install would turn every rebuild into a surprise restart. -# renovate: datasource=npm depName=happy -ARG HAPPY_VERSION=1.2.3 -RUN npm install -g happy@${HAPPY_VERSION} - # ─── DEFAULT — full dev environment ─────────────────────────────────────────── FROM base AS default @@ -294,3 +281,57 @@ LABEL org.opencontainers.image.source="https://github.com/gatezh/devcontainers" org.opencontainers.image.url="https://github.com/gatezh/devcontainers" CMD ["sleep", "infinity"] + +# ─── HAPPY — default plus the happy CLI for phone/web remote control ────────── +# Its own target rather than part of `base`, for two reasons: +# +# Size. happy adds ~785 MB on top of `default` even after the trimming below +# (1844 MB -> 2629 MB, +43%). ~850 MB of that is upstream packaging we cannot +# influence: happy vendors its own copy of @anthropic-ai/claude-agent-sdk and +# sandbox-runtime (duplicating the claude-code install in `base`, and dragging +# in its Bedrock/Vertex/OpenTelemetry fan-out), and it ships the self-hostable +# happy server — fastify, http-proxy, expo-server-sdk, drizzle-orm, libsql — +# in the same npm package as the CLI. Projects that never pair a phone should +# not carry that. +# +# Reachability. The daemon needs egress to happy's relay, which the `sandbox` +# firewall blocks by default. Building on `default` keeps the network- +# restricted image free of a tool it cannot use. +# +# Consume it by pointing devcontainer.json at +# ghcr.io/gatezh/devcontainers/claude-code-happy instead of .../claude-code. +FROM default AS happy + +# happy.engineering lets the Happy phone/web app start and drive Claude Code +# sessions inside the container. Only the CLI is installed; the daemon is opt-in +# per project (compose `command:` + a volume for ~/.happy — see README +# "Optional: remote control with happy"). Nothing runs unless a project asks. +# Installed as the node user for the same ownership reason as Claude Code above. +# +# Pinned: the daemon restarts itself whenever the installed version changes, so +# a floating install would turn every rebuild into a surprise restart. The pin +# also guards against the npm name transfer — this package was renamed from +# `happy-coder` and the old name is still published, so never relax it to a +# range. Renovate bumps it under the 3-day soak (see .github/renovate.json5). +# +# Two cleanups inside the same RUN, so the bytes never enter a layer: +# tools/archives — happy ships prebuilt difftastic + ripgrep tarballs for all +# six platform/arch combos (106 MB). Its postinstall unpacks only this +# platform's pair into tools/unpacked, which is where the runtime resolves +# difft and rg from; the archives are dead weight afterwards. (This is also +# why --ignore-scripts is not an option: it would leave tools/unpacked empty +# and silently break happy's diffing.) +# npm cache — 680 MB of tarballs. A later layer cannot reclaim it. +# renovate: datasource=npm depName=happy +ARG HAPPY_VERSION=1.2.3 +RUN npm install -g happy@${HAPPY_VERSION} \ + && rm -rf "${NPM_CONFIG_PREFIX}/lib/node_modules/happy/tools/archives" \ + && npm cache clean --force + +LABEL org.opencontainers.image.source="https://github.com/gatezh/devcontainers" \ + org.opencontainers.image.description="Claude Code devcontainer — full dev environment plus the happy CLI for phone/web remote control" \ + org.opencontainers.image.licenses="MIT" \ + org.opencontainers.image.title="claude-code-happy" \ + org.opencontainers.image.url="https://github.com/gatezh/devcontainers" + +CMD ["sleep", "infinity"] diff --git a/claude-code/README.md b/claude-code/README.md index 5a5fb0e..f9ebd0c 100644 --- a/claude-code/README.md +++ b/claude-code/README.md @@ -1,6 +1,6 @@ # claude-code -Shared devcontainer image for Claude Code development environments. Two variants from a single multi-stage Dockerfile: **default** (full dev environment) and **sandbox** (network-restricted). +Shared devcontainer image for Claude Code development environments. Three variants from a single multi-stage Dockerfile: **default** (full dev environment), **sandbox** (network-restricted), and **happy** (default plus the happy CLI for phone/web remote control). Projects consume these pre-built images and control their own tool versions via `.mise.toml`. @@ -10,6 +10,7 @@ Projects consume these pre-built images and control their own tool versions via |---------|-------|----------| | **default** | `ghcr.io/gatezh/devcontainers/claude-code:latest` | Full dev environment with agent-browser and passwordless sudo | | **sandbox** | `ghcr.io/gatezh/devcontainers/claude-code-sandbox:latest` | Network-restricted environment with iptables firewall packages | +| **happy** | `ghcr.io/gatezh/devcontainers/claude-code-happy:latest` | Default plus the [happy](https://happy.engineering) CLI, for driving Claude Code from the Happy phone/web app. ~785 MB larger than **default** — only worth pulling if you actually pair a device | ## What's Included @@ -21,17 +22,18 @@ Projects consume these pre-built images and control their own tool versions via | 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 | -| happy | npm global install, pinned `ARG` bumped by Renovate | [happy.engineering](https://happy.engineering) CLI — lets the Happy phone/web app start Claude Code sessions in the container. CLI only; the daemon is opt-in per project | -**Both targets:** system Chromium + `fonts-freefont-ttf` (used by Playwright and the Playwright MCP plugin via `/usr/bin/chromium`) +**All targets:** system Chromium + `fonts-freefont-ttf` (used by Playwright and the Playwright MCP plugin via `/usr/bin/chromium`) **Default-only:** passwordless sudo, agent-browser +**Happy-only:** everything in **default**, plus the [happy.engineering](https://happy.engineering) CLI (npm global install, pinned `ARG` bumped by Renovate). CLI only; the daemon is opt-in per project — see [Optional: remote control with happy](#optional-remote-control-with-happy) + **Sandbox-only:** iptables, ipset, iproute2, dnsutils, aggregate, firewall sudo rule ## Multi-platform Support -Both variants are built for: +All three variants are built for: - `linux/amd64` (x86_64) - `linux/arm64` (ARM64/Apple Silicon) @@ -43,7 +45,7 @@ Both variants are built for: ## Automatic Rebuilds -The image rebuilds automatically whenever one of its pinned tools — Claude Code, happy, agent-browser, rtk, or ralphex — publishes a new release: Renovate opens a version-bump PR, CI verifies it, it auto-merges, and the merge builds the image on native runners for both amd64 and arm64 (no QEMU emulation). Manual rebuilds can be triggered via the "Run workflow" button in the Actions UI. +The images rebuild automatically whenever one of the pinned tools — Claude Code, agent-browser, rtk, ralphex, or happy — publishes a new release: Renovate opens a version-bump PR, CI verifies it, it auto-merges, and the merge builds all three variants on native runners for both amd64 and arm64 (no QEMU emulation). Manual rebuilds can be triggered via the "Run workflow" button in the Actions UI. ## Quick Start @@ -165,13 +167,26 @@ After the one-time copy, the skill manages its own updates. ### Optional: remote control with happy -Both targets ship the [happy.engineering](https://happy.engineering) CLI (`happy`), pinned via `HAPPY_VERSION` and bumped by Renovate like the other agent tools. Nothing runs by default — the image `CMD` is still `sleep infinity`. To let the Happy phone/web app start Claude Code sessions in a project's container: +The [happy.engineering](https://happy.engineering) CLI (`happy`) ships in **its own image variant**, not in `default` or `sandbox`: + +```yaml +# .devcontainer/docker-compose.yml +image: ghcr.io/gatezh/devcontainers/claude-code-happy:latest +``` + +It is a separate variant because it is expensive — ~785 MB on top of `default`, and roughly 850 MB of that is upstream packaging this repo cannot trim (happy vendors its own copy of the Claude Code agent SDK, and ships the self-hostable happy server in the same npm package as the CLI). Projects that never pair a device keep the smaller `default` image. + +Nothing runs by default even in this variant — the image `CMD` is still `sleep infinity`. To let the Happy phone/web app start Claude Code sessions in a project's container: 1. **Persist the pairing.** happy keeps its credentials and machine id in `~/.happy` (`HAPPY_HOME_DIR` overrides). Add a named volume for `/home/node/.happy` next to the Claude config volume so pairing survives rebuilds. -2. **Run the daemon as the container's main process.** Point the compose service `command:` at a script that runs `happy daemon start-sync` and restarts it when it exits, add `restart: unless-stopped`, and set `"shutdownAction": "none"` in `devcontainer.json` so closing VS Code does not stop the container. +2. **Run the daemon as the container's main process.** Point the compose service `command:` at a script that runs `happy daemon start-sync` and restarts it when it exits, add `restart: unless-stopped`, and set `"shutdownAction": "none"` in `devcontainer.json` so closing VS Code does not stop the container. (`happy daemon start` is only a wrapper that spawns `start-sync` detached, so `start-sync` is the form a supervised main process wants.) 3. **Pair once:** `docker compose exec devcontainer happy auth login`, then scan the QR code in the app. Start desk sessions with `happy claude` instead of `claude` so they show up in the app too. -The daemon needs no inbound ports; it opens an outbound connection to happy's backend (`api.cluster-fluster.com` by default, `HAPPY_SERVER_URL` overrides). **Sandbox:** add that host to your `init-firewall.sh` allowlist or the daemon cannot connect. The daemon restarts itself when the installed happy version changes, so a Renovate bump plus image pull restarts running daemons on the next rebuild — that is why the version is pinned rather than floating. +The daemon needs no inbound ports; it opens an outbound connection to happy's backend (`api.cluster-fluster.com` by default, `HAPPY_SERVER_URL` overrides). The daemon restarts itself when the installed happy version changes, so a Renovate bump plus image pull restarts running daemons on the next rebuild — that is why `HAPPY_VERSION` is pinned rather than floating. + +**Understand what pairing grants before you use this.** The relay is an inbound control channel: anyone holding the pairing can start and drive Claude Code sessions against your mounted workspace, and happy's bypass permission modes hand the agent `--dangerously-skip-permissions` with no per-tool approval prompt. Treat a pairing QR code like a credential, and prefer this variant on projects where that blast radius is acceptable. + +**Not available in the sandbox variant, deliberately.** The sandbox firewall blocks happy's relay, and allowlisting `api.cluster-fluster.com` in `init-firewall.sh` would punch a hole in exactly the egress restriction that variant exists to enforce — for a channel that can execute code in the container. If you need remote control, use the `happy` variant instead of loosening the sandbox. ### Sandbox Authentication @@ -416,15 +431,20 @@ 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) | -| `HAPPY_VERSION` | `1.2.3` | happy.engineering CLI version (Renovate-managed) | -| `AGENT_BROWSER_VERSION` | `0.32.3` | agent-browser version, default target only (Renovate-managed) | +| `RTK_VERSION` | see Dockerfile | rtk version (Renovate-managed) | +| `RALPHEX_VERSION` | see Dockerfile | ralphex version (Renovate-managed) | +| `CLAUDE_CODE_VERSION` | see Dockerfile | Claude Code CLI version (Renovate-managed) | +| `HAPPY_VERSION` | see Dockerfile | happy.engineering CLI version, happy target only (Renovate-managed) | +| `AGENT_BROWSER_VERSION` | see Dockerfile | agent-browser version, default target only (Renovate-managed) | 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). +Their current values are deliberately not repeated here: Renovate rewrites the `ARG` lines on every +release, so any copy in this table is stale within days. Read the pinned versions straight from +[`.devcontainer/Dockerfile`](./.devcontainer/Dockerfile), or from a running container with +`rtk --version`, `claude --version`, and so on. + ## Building Locally / Local Fallback If the pre-built image is unavailable (GHCR outage, rate limits, or you need to test image changes), build from the [devcontainers](https://github.com/gatezh/devcontainers) source: From 93a6b841b580cc4c88a0f139fc2244f30cadfdcd Mon Sep 17 00:00:00 2001 From: Serge Gatezh <2880401+gatezh@users.noreply.github.com> Date: Wed, 9 Sep 2026 12:15:33 -0600 Subject: [PATCH 3/3] ci: read image verify commands from one manifest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The claude-code verify commands lived in both ci.yml (pull-request builds) and build-claude-code.yml (post-publish checks) — nine copies of three unique strings once the happy target arrived. This branch already demonstrated the failure mode: its first commit updated ci.yml and left build-claude-code.yml alone, so the published image was never checked. .github/verify-commands.json is now the single source, keyed by published image name, and both workflows look commands up in it. The strings are unchanged: the generated ci.yml build matrix is byte identical to the previous one for all eight images, and the three commands removed from build-claude-code.yml's matrix match their new manifest entries exactly. Two things fall out of the lookup: An image added without a manifest entry now fails the job with a GHA error annotation instead of being built and silently verified against an empty command. Verified by pointing a call site at a missing key. build-claude-code.yml no longer interpolates ${{ matrix.verify-command }} into a run: script. The command reaches the shell as a variable read from the checked-out file, so nothing from the workflow context is expanded into shell text. Each image's paths filter also watches the manifest, so editing a verify command re-runs it against a real image rather than leaving it unexercised until the next Dockerfile change. A manifest edit rebuilds every image, which is rare enough to be worth paying for — and means this commit exercises the lookup for all six image families, not just claude-code. The remaining build-*.yml workflows still pass their own verify-command to reusable-docker-build.yml. Migrating them is mechanical but touches publish pipelines a pull request cannot exercise, so it is left for its own change; the manifest header records that. actionlint clean. --- .github/verify-commands.json | 31 ++++++++++++++++++++ .github/workflows/build-claude-code.yml | 24 ++++++++++----- .github/workflows/ci.yml | 39 ++++++++++++++++--------- 3 files changed, 72 insertions(+), 22 deletions(-) create mode 100644 .github/verify-commands.json diff --git a/.github/verify-commands.json b/.github/verify-commands.json new file mode 100644 index 0000000..9329f9d --- /dev/null +++ b/.github/verify-commands.json @@ -0,0 +1,31 @@ +{ + "$comment": [ + "Single source of truth for post-build image verification, keyed by published", + "image name (ghcr.io/gatezh/devcontainers/).", + + "Read by .github/workflows/ci.yml (pull-request builds) and by", + ".github/workflows/build-claude-code.yml (post-publish verification). Before", + "this file the claude-code commands lived in both, in nine copies of three", + "unique strings, and #131 shipped an update to one file and not the other.", + + "A command must be able to FAIL. Assert on something that is absent from an", + "image lacking the tool, and check that it does: a CLI that exits 0 whatever", + "you pass it verifies nothing. happy is the worked example — every one of its", + "subcommands exits 0, including unknown flags, so claude-code-happy asserts", + "the installed package with `npm ls -g` instead of running the binary.", + + "The remaining build-*.yml workflows still pass their own verify-command to", + "reusable-docker-build.yml. Migrating them is mechanical but touches publish", + "pipelines this repo cannot exercise from a pull request, so it is deliberately", + "left for a change of its own." + ], + + "bun": "bun --version", + "claude-bun": "bun --version", + "claude-code": "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", + "claude-code-sandbox": "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-code-happy": "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 && npm ls -g --depth=0 happy", + "hugo-bun": "bun --version && hugo version", + "hugo-bun-node": "bun --version && hugo version && node --version", + "ralphex-fe": "bun --version && hugo version && /srv/ralphex --version && rtk --version" +} diff --git a/.github/workflows/build-claude-code.yml b/.github/workflows/build-claude-code.yml index dabba71..91810b0 100644 --- a/.github/workflows/build-claude-code.yml +++ b/.github/workflows/build-claude-code.yml @@ -99,31 +99,28 @@ 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" 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" 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" 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" runner: ubuntu-24.04-arm arch: arm64 - image-suffix: claude-code-happy - 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 && npm ls -g --depth=0 happy" runner: ubuntu-24.04 arch: amd64 - image-suffix: claude-code-happy - 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 && npm ls -g --depth=0 happy" runner: ubuntu-24.04-arm arch: arm64 runs-on: ${{ matrix.runner }} steps: + - name: Checkout repository + uses: actions/checkout@v6 + - name: Log in to GHCR uses: docker/login-action@v4 with: @@ -131,7 +128,18 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + # The command comes from .github/verify-commands.json, the same file + # ci.yml reads, so pull-request and post-publish checks cannot drift. + # Reading it into a shell variable also keeps the command out of the + # `run:` script that Actions expands, so nothing from the workflow + # context is interpolated into shell. - name: Verify image + env: + IMAGE: ${{ matrix.image-suffix }} run: | - docker pull ghcr.io/gatezh/devcontainers/${{ matrix.image-suffix }}:latest - docker run --rm ghcr.io/gatezh/devcontainers/${{ matrix.image-suffix }}:latest bash -c "${{ matrix.verify-command }}" + VERIFY=$(jq -er --arg k "$IMAGE" '.[$k]' .github/verify-commands.json) || { + echo "::error::no verify command for '$IMAGE' in .github/verify-commands.json" + exit 1 + } + docker pull "ghcr.io/gatezh/devcontainers/${IMAGE}:latest" + docker run --rm "ghcr.io/gatezh/devcontainers/${IMAGE}:latest" bash -c "$VERIFY" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3985fca..779bf8c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,19 +62,29 @@ jobs: uses: dorny/paths-filter@v4 id: filter with: + # Each image also watches .github/verify-commands.json: editing a + # verify command should re-run it against a real image, not sit + # unexercised until the next Dockerfile change. A manifest edit + # therefore rebuilds every image — rare enough to be worth it. filters: | bun: - 'bun/**' + - '.github/verify-commands.json' claude-bun: - 'claude-bun/**' + - '.github/verify-commands.json' claude-code: - 'claude-code/**' + - '.github/verify-commands.json' hugo-bun: - 'hugo-bun/**' + - '.github/verify-commands.json' hugo-bun-node: - 'hugo-bun-node/**' + - '.github/verify-commands.json' ralphex-fe: - 'ralphex-fe/**' + - '.github/verify-commands.json' - name: Build matrix from changes id: set-matrix @@ -88,8 +98,17 @@ jobs: run: | INCLUDES="[]" + # Verify commands live in .github/verify-commands.json so that this + # workflow and build-claude-code.yml cannot drift apart. Looking the + # command up here (rather than passing it in) means an image added + # without an entry fails the job instead of silently verifying nothing. add_image() { - local image="$1" context="$2" dockerfile="$3" verify="$4" target="${5:-}" + local image="$1" context="$2" dockerfile="$3" target="${4:-}" + local verify + verify=$(jq -er --arg k "$image" '.[$k]' .github/verify-commands.json) || { + echo "::error::no verify command for '$image' in .github/verify-commands.json" + exit 1 + } INCLUDES=$(echo "$INCLUDES" | jq -c \ --arg img "$image" \ --arg ctx "$context" \ @@ -102,54 +121,46 @@ jobs: if [ "$CHANGED_BUN" = "true" ]; then add_image "bun" \ "bun/.devcontainer" \ - "bun/.devcontainer/Dockerfile" \ - "bun --version" + "bun/.devcontainer/Dockerfile" fi if [ "$CHANGED_CLAUDE_BUN" = "true" ]; then add_image "claude-bun" \ "claude-bun/.devcontainer" \ - "claude-bun/.devcontainer/Dockerfile" \ - "bun --version" + "claude-bun/.devcontainer/Dockerfile" 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" \ "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" \ "sandbox" add_image "claude-code-happy" \ "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 && npm ls -g --depth=0 happy" \ "happy" fi if [ "$CHANGED_HUGO_BUN" = "true" ]; then add_image "hugo-bun" \ "hugo-bun/.devcontainer" \ - "hugo-bun/.devcontainer/Dockerfile" \ - "bun --version && hugo version" + "hugo-bun/.devcontainer/Dockerfile" fi if [ "$CHANGED_HUGO_BUN_NODE" = "true" ]; then add_image "hugo-bun-node" \ "hugo-bun-node/.devcontainer" \ - "hugo-bun-node/.devcontainer/Dockerfile" \ - "bun --version && hugo version && node --version" + "hugo-bun-node/.devcontainer/Dockerfile" fi if [ "$CHANGED_RALPHEX" = "true" ]; then add_image "ralphex-fe" \ "ralphex-fe" \ - "ralphex-fe/Dockerfile" \ - "bun --version && hugo version && /srv/ralphex --version && rtk --version" + "ralphex-fe/Dockerfile" fi if [ "$INCLUDES" = "[]" ]; then