@@ -41,13 +41,13 @@ RUN echo 'APT::Sandbox::User "root";' > /etc/apt/apt.conf.d/10no-sandbox \
4141
4242# Free UID/GID 1000. Ubuntu's base image ships a default `ubuntu` user at
4343# 1000:1000 — i.e. exactly the typical host UID. The entrypoint creates a
44- # fresh `claude ` user mapped to HOST_UID; without this step its `useradd`
45- # is skipped on collision and `runuser -u claude ` then fails. Reusing the
44+ # fresh `opencode ` user mapped to HOST_UID; without this step its `useradd`
45+ # is skipped on collision and `runuser -u opencode ` then fails. Reusing the
4646# baked-in `ubuntu` account would also silently inherit its supplementary
4747# groups (sudo, adm, plugdev, …). Guarded so a future base image without
4848# the default user doesn't break the build.
4949RUN if getent passwd ubuntu >/dev/null; then userdel -r ubuntu; fi \
50- && if getent group ubuntu >/dev/null; then groupdel ubuntu; fi
50+ && if getent group ubuntu >/dev/null; then groupdel ubuntu; fi
5151
5252# NodeSource ships Node 24 LTS pinned to upstream releases — Ubuntu's archive
5353# `nodejs` tracks an older minor and isn't LTS-pinned. `nodistro` is
@@ -141,35 +141,37 @@ RUN . /tmp/uv.env; set -e; ARCH=$(uname -m); \
141141
142142# npm-backed CLIs — pinned versions. Trust = npm's signed dist.integrity;
143143# run `npm audit signatures <pkg>@<ver>` when bumping.
144- # --ignore-scripts blocks lifecycle hooks for every package + transitive dep
145- # (hard security boundary, kept on). claude-code 2.1.x ships its real binary
146- # in a per-arch optional-dep package; the launcher's postinstall (install.cjs)
147- # copies it over bin/claude.exe. Without it `claude` is a stub that errors at
148- # exec. We invoke that one script ourselves — platform-detect + file copy,
149- # no network/exec, audit-verified for 2.1.131; re-read on each bump.
144+ # opencode-ai ships its real binary in a per-arch optional-dep package; the
145+ # launcher's postinstall copies it over. Without it `opencode` is a stub that
146+ # errors at exec. We install with --ignore-scripts (blocks lifecycle hooks for
147+ # every package + transitive dep, hard security boundary) then explicitly invoke
148+ # the postinstall for opencode-ai only. It selects and copies the installed
149+ # platform binary, verifies it with `--version`, and only falls back to a
150+ # separate `npm install --ignore-scripts` when npm omitted the optional binary.
151+ # Re-review that script whenever the OpenCode pin changes.
150152# `npm root -g` over a hardcoded path so we don't break on a different prefix.
151153# npm tools carry version-only pins (no sha256): npm install verifies the
152154# registry-advertised dist.integrity (registry-integrity, not provenance; CI
153155# runs `npm audit signatures`). All three share this layer, so they share a COPY.
154- COPY pins/claude-code .env pins/openspec.env pins/pnpm.env /tmp/
155- RUN . /tmp/claude-code .env && . /tmp/openspec.env && . /tmp/pnpm.env \
156- && npm install -g --ignore-scripts \
157- "@anthropic -ai/claude-code @${CLAUDE_CODE_VERSION }" \
158- "@fission-ai/openspec@${OPENSPEC_VERSION}" \
159- "pnpm@${PNPM_VERSION}" \
160- && node "$(npm root -g)/@anthropic -ai/claude-code/install.cjs " \
161- && rm /tmp/claude-code .env /tmp/openspec.env /tmp/pnpm.env
156+ COPY pins/opencode .env pins/openspec.env pins/pnpm.env /tmp/
157+ RUN . /tmp/opencode .env && . /tmp/openspec.env && . /tmp/pnpm.env \
158+ && npm install -g --ignore-scripts \
159+ "opencode -ai@${OPENCODE_VERSION }" \
160+ "@fission-ai/openspec@${OPENSPEC_VERSION}" \
161+ "pnpm@${PNPM_VERSION}" \
162+ && node "$(npm root -g)/opencode -ai/postinstall.mjs " \
163+ && rm /tmp/opencode .env /tmp/openspec.env /tmp/pnpm.env
162164
163165# tfenv — pure-bash terraform version manager. Arch-independent (just
164166# bash scripts), so a single sha256 covers amd64 and arm64. We deliberately
165167# ship NO terraform binary; the project's `.terraform-version` (or an
166168# interactive `tfenv install <v>`) fetches the right version from
167169# releases.hashicorp.com at runtime, in the same runtime-fetch class as
168170# `pnpm dlx`/`uvx`. Installed under /opt (not /root) so image-level
169- # version bumps aren't shadowed by the claude-code -root named volume.
171+ # version bumps aren't shadowed by the opencode -root named volume.
170172# Placed after the heavier npm install so a tfenv version bump doesn't
171173# invalidate that layer's cache (tfenv pins move far less often than the
172- # claude-code /openspec/pnpm pins above).
174+ # opencode-ai /openspec/pnpm pins above).
173175COPY pins/tfenv.env /tmp/tfenv.env
174176RUN . /tmp/tfenv.env \
175177 && curl -fsSL "$TFENV_URL" -o /tmp/tfenv.tar.gz \
@@ -180,11 +182,11 @@ RUN . /tmp/tfenv.env \
180182 && ln -s /opt/tfenv/bin/terraform /usr/local/bin/terraform \
181183 && rm /tmp/tfenv.tar.gz /tmp/tfenv.env
182184
183- # Plain `tmux` mode swallows Shift+Enter so Claude 's prompt sees only Enter,
185+ # Plain `tmux` mode swallows Shift+Enter so OpenCode 's prompt sees only Enter,
184186# forcing users to type `\` for a literal newline. `always` is required
185- # (not `on`) because Claude does not send the kitty activation request that
186- # `on` waits for — see claude-code#26629 . /etc/tmux.conf, not
187- # /root/.tmux.conf, because /root is masked by the claude-code -root named
187+ # (not `on`) because OpenCode does not send the kitty activation request that
188+ # `on` waits for. /etc/tmux.conf, not
189+ # /root/.tmux.conf, because /root is masked by the opencode -root named
188190# volume at runtime. Harmless under tmux -CC: iTerm2 control mode bypasses
189191# tmux's input layer. Placed after npm install so edits don't invalidate
190192# the heavy AWS CLI / uv / glab / npm download layers above.
@@ -193,17 +195,17 @@ set -s extended-keys always
193195set -as terminal-features "*:extkeys"
194196EOF
195197
196- # DISABLE_AUTOUPDATER =1 keeps the pinned CLAUDE_CODE_VERSION authoritative —
197- # without it, claude auto-replaces itself at runtime, defeating the
198+ # OPENCODE_DISABLE_AUTOUPDATE =1 keeps the pinned OPENCODE_VERSION authoritative —
199+ # without it, opencode auto-replaces itself at runtime, defeating the
198200# --ignore-scripts supply-chain pinning above. Bump the image to upgrade.
199- ENV CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS =1 \
200- DISABLE_AUTOUPDATER =1 \
201+ ENV OPENCODE_EXPERIMENTAL_AGENT_TEAMS =1 \
202+ OPENCODE_DISABLE_AUTOUPDATE =1 \
201203 IS_SANDBOX=1 \
202204 LANG=C.UTF-8 \
203205 LC_ALL=C.UTF-8
204206
205207# Container starts as root so the entrypoint can chown /root to the host
206- # UID, then drops privileges via runuser. Steady-state, claude runs as the
208+ # UID, then drops privileges via runuser. Steady-state, opencode runs as the
207209# host user with no effective / permitted / ambient capabilities — the
208210# kernel clears those on the UID→non-zero transition; the bounding set
209211# retains the setup caps but is inert under `no-new-privileges`. Do not
@@ -216,4 +218,4 @@ WORKDIR /workspaces
216218COPY --chmod=0755 entrypoint.sh /usr/local/bin/entrypoint.sh
217219
218220ENTRYPOINT ["/usr/local/bin/entrypoint.sh" ]
219- CMD ["claude " ]
221+ CMD ["opencode " ]
0 commit comments