Repository navigation
Conversation
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 <noreply@anthropic.com>
Resolves the renovate.json5 conflict: master (#126) rewrote the "devcontainer agent tools" rationale to explain why platformAutomerge must stay at its default and added a 3-day minimumReleaseAge soak with a claude-code exemption. Keep master's version verbatim and carry over only this branch's "four tools" -> "five tools" count. Effect on happy: it inherits the 3-day soak, so a happy release is not adopted until it has been public for three days.
Code reviewVerified against the published Must fix1.
The deeper issue is that the Default column is a hand-maintained mirror of state Renovate rewrites on every bump, so it will drift again next week. Consider replacing the values for Renovate-managed args with a pointer to the Dockerfile and keeping literal defaults only for Security2. The sandbox allowlist advice understates what it opens. The README tells sandbox users to add Recommend the README say so in one sentence, and that this reinforces finding "base vs default placement" above: the network-restricted image is the wrong place to ship this by default. 3. Supply chain — note why the pin must stay exact. Upstream's README says the package migrated from 4. Verified correct — no change neededRecording these so they are not re-litigated:
Not reviewedThe image was not built for |
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.
Review items addressed in 6d22bd1
One correction to my earlier note on #3: the old Also worth doing, not in this PR
|
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.
What
Adds the happy.engineering CLI to the
claude-codeDockerfile as a third build target, published asghcr.io/gatezh/devcontainers/claude-code-happy.defaultandsandboxare unchanged.Why
happy lets the Happy phone/web app start and drive Claude Code sessions inside a devcontainer. Installing it at container-create time would cost an npm fetch on every rebuild, so it wants to be baked in — but it is expensive enough (+785 MB) that it should not ride along in images that never use it.
Changes
claude-code/.devcontainer/Dockerfile— newFROM default AS happytarget:ARG HAPPY_VERSION=1.2.3+npm install -g happy@${HAPPY_VERSION}, withtools/archivesremoval andnpm cache clean --forcein the sameRUN. Nothing added tobase..github/workflows/build-claude-code.yml—build-happypublish job for the new target and tag, plus amd64/arm64 verify matrix entries..github/workflows/ci.yml—claude-code-happyadded to the PR build matrix. The two existing verify strings are untouched (pure+5againstmaster)..github/workflows/cleanup-claude-code-ghcr.yml— new package added to the retention matrix, so the heaviest image does not accumulatesha/date tags forever..github/renovate.json5—happyjoins the grouped, auto-mergeddevcontainer agent toolsrule.claude-code/README.md,README.md— third variant documented; the happy section covers the~/.happyvolume, running the daemon as the composecommand:,shutdownAction: none, the pairing blast radius, and why the sandbox deliberately does not get it.Sizing
Measured on
linux/arm64, layer-sum:defaultclaude-codesandboxclaude-code-sandboxhappyclaude-code-happydefaultInstalling happy the naive way — no cleanup, in
base— cost 1638 MB and hit both existing images. Of that, 106 MB wastools/archives(prebuilt difftastic + ripgrep for all six platform/arch combos; thepostinstallunpacks only this platform's pair intotools/unpacked, which is the only path the runtime resolvesdifft/rgfrom) and 680 MB was the npm cache. Both are removed inside the installRUN.The remaining ~850 MB is upstream packaging and not reachable from a Dockerfile: happy vendors its own
@anthropic-ai/claude-agent-sdk+sandbox-runtime(a second copy of whatbasealready 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. That cost is exactly why this is a separate target.Notes
Verification does not use the happy CLI.
happy --versionprints its banner, falls through into the interactive auth TUI, fails on non-TTY stdin, and still exits 0 — and so does every other subcommand, including an unknown flag. No invocation of it can assert anything, so the check isnpm ls -g --depth=0 happy(exit 1 when absent, prints the exact version, never starts the TUI).--versionnot exiting is worth reporting upstream.Renovate soak applies. This branch merges current
master, which addedminimumReleaseAge: '3 days'to the tools group with an exemption for@anthropic-ai/claude-codeonly, sohappywaits three days after a release. The exact pin also guards the npm name transfer — this package was renamed fromhappy-coder, which is still published — so it should never be relaxed to a range.Nothing runs by default, even in the happy variant:
CMDis stillsleep infinity. Starting the daemon is opt-in per project via composecommand:, documented but not wired up here.Security posture. Pairing is an inbound control channel — whoever holds it can drive Claude Code against the mounted workspace, and happy's bypass modes pass
--dangerously-skip-permissionswith no per-tool prompt. The README says so, and the sandbox variant deliberately does not ship happy rather than inviting anapi.cluster-fluster.comfirewall exception.Out of scope (intentional). Three other Dockerfiles here also install Claude Code — the legacy bun-based image, the standalone
ralphex-ferunner, and this repo's own maintainer devcontainer. None is the template projects consume, so none gained happy. Shipping a daemon-loop script plus opt-in compose lines in the template depends on the compose-first split and stays a separate PR.Verification performed
All three targets built locally for
linux/arm64:claude-code-happyverify string fails againstclaude-code, confirming it asserts something real;happyis absent from bothdefaultandsandbox;difft --versionandrg --versionstill work inside the happy image;DL3066infos, identical tomaster— no new findings; actionlint clean.Not done locally:
linux/amd64builds and an end-to-end pairing. CI covers the former.