diff --git a/.github/renovate.json5 b/.github/renovate.json5 index ec3a5e4..cf03811 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. // // Relies on Renovate's default platformAutomerge:true — GitHub's native // auto-merge merges on green with no second Renovate run. The previous @@ -44,6 +44,7 @@ 'rtk-ai/rtk', 'umputun/ralphex', '@anthropic-ai/claude-code', + 'happy', 'agent-browser', ], groupName: 'devcontainer agent tools', 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 4977ba6..91810b0 100644 --- a/.github/workflows/build-claude-code.yml +++ b/.github/workflows/build-claude-code.yml @@ -67,31 +67,60 @@ 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: 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 + runner: ubuntu-24.04 + arch: amd64 + - image-suffix: claude-code-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: @@ -99,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 08e8c04..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,49 +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" \ + "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 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 fff3e7c..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, 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 8b68054..e064b5d 100644 --- a/claude-code/.devcontainer/Dockerfile +++ b/claude-code/.devcontainer/Dockerfile @@ -281,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 173c0e9..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 @@ -22,15 +23,17 @@ Projects consume these pre-built images and control their own tool versions via | 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 | -**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) @@ -42,7 +45,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 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 @@ -162,6 +165,29 @@ 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 + +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. (`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). 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 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. @@ -405,14 +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) | -| `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 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). +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: