feat(devbase): stop installing the Claude Code CLI - #13
Merged
Conversation
devbase declared ghcr.io/anthropics/devcontainer-features/claude-code:1 in
dependsOn, so every consuming container installed the Claude Code CLI globally
via npm. That copy was redundant, and it was broken in a way that needed a
manual fix.
Redundant, because the VS Code extension ships and runs its own runtime.
Verified by process list in a running container rather than inferred:
/home/vscode/.vscode-server/extensions/anthropic.claude-code-2.1.263-linux-x64/
resources/native-binary/claude --output-format stream...
The extension never invokes the npm global. It is what we actually use, so the
npm install was a second copy of the same thing.
Broken, because the claude-code feature installs as root into a global npm tree
that the node feature makes vscode:nvm. The package directory therefore lands
root-owned at mode 755 — no group write — and `claude update` fails for the
remote user until someone runs, by hand:
sudo chown -R vscode:nvm "$(npm root -g)/@Anthropic-AI"
Fixing the ownership in install.sh was the alternative, and it would have
shipped as a patch and so reached consumers immediately. Rejected: it keeps a
redundant second runtime in every image purely to make it self-updating.
Removing it is the smaller end state, and this Feature has exactly one consumer
group, so a major costs less here than it would for a public Feature.
Checked before removing: no repository's scripts, workflows or package manifests
call the claude CLI — zero hits across the fleet.
Three things deliberately stay.
RTK stays. Its PreToolUse hook lives in the bind-mounted, host-shared
~/.claude/settings.json and fires under the extension's runtime exactly as it
did under the CLI's — confirmed by RTK-filtered command output in a container
running only the extension. Without the binary that hook still exits 127 on
every Bash call, which is the whole reason RTK is in this Feature.
node stays in dependsOn. One of its two documented reasons retires here —
claude-code installed EOL Node 18 from nodesource when it found no Node — but
the other stands unchanged: devbase's own pnpm, commitizen and npm-floor steps
need npm.
The anthropic.claude-code and anthropic.claude-vscode VS Code extensions stay.
They are now the only Claude Code in a container.
Tests move with the behaviour. The "dependsOn installed the claude CLI" check
inverts to assert the CLI is absent: a removal with no assertion cannot
distinguish "removed" from "quietly came back through some other feature's own
dependsOn", which is exactly what dependsOn does. node_pinned's nodesource
guard is dropped rather than kept — its subject is gone, so it could no longer
fail, and a check that cannot fail is worse than no check.
BREAKING CHANGE: devbase no longer installs the Claude Code CLI. A container
built from 2.x has no `claude` on PATH unless the repository lists
ghcr.io/anthropics/devcontainer-features/claude-code:1 itself. The Claude Code
VS Code extension is unaffected — it runs its own bundled runtime. Consumers
pin devbase:1, so this reaches nobody until they change that reference to
devbase:2 and re-resolve any committed devcontainer-lock.json; see "Migrating
to 2.0.0" in README.md.
Refs RSRMID-3053.
Both frames said node, github-cli and claude-code all come from devbase's dependsOn. The preceding commit removed claude-code, so that is now wrong in three places. The default frame keeps referencing devbase:1, per the dogfooding rule — the frame only stops expecting something after the release that stops providing it — so its comment says so explicitly: the CLI is still there until that reference moves to :2, and deliberately gone afterwards. The local frame has no published :1 to fall back on: it builds the working-tree copy, so the removal takes effect there immediately. Noted, because that asymmetry is the whole point of having the alternate config and is the first place anyone will notice `claude` missing. Also corrects the ~/.claude mkdir rationale, which named claude-code as the thing that could not write its config into a root-owned directory. The directory is still needed for exactly that reason — the VS Code extension writes there, and the RTK hook is read from the settings.json inside it. Refs RSRMID-3053.
Contributor
|
🎉 This PR is included in version 2.0.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
RSRMID-3053 — drops
ghcr.io/anthropics/devcontainer-features/claude-code:1fromdevbase'sdependsOn. This is a major, released as2.0.0.Why
The npm CLI was redundant. The VS Code extension ships and runs its own runtime — verified by process list in a running container, not inferred:
The extension never invokes the npm global at
/usr/local/share/nvm/versions/node/v24.20.0/bin/claude. The extension is what we use, so the npm install was a second copy of the same thing.And it could not update itself. The
claude-codefeature installs as root into a global npm tree the node feature makesvscode:nvm, so the package directory lands root-owned at mode 755 — no group write — andclaude updatefails for the remote user until someone runs by hand:sudo chown -R vscode:nvm "$(npm root -g)/@anthropic-ai"The alternative was considered and rejected. Chowning that directory in
install.shwould have been afix(devbase)— a patch, reaching consumers on their next rebuild with no migration. It keeps a redundant second runtime in every image purely to make it self-updating. Removal is the smaller end state, and this Feature has exactly one consumer group, so a major costs less here than it would for a public Feature.Checked before removing: no repository's scripts, workflows or package manifests call the
claudeCLI — zero hits across the fleet.What deliberately stays
PreToolUsehook lives in the bind-mounted, host-shared~/.claude/settings.jsonand fires under the extension's runtime exactly as it did under the CLI's — confirmed by RTK-filtered command output in a container running only the extension. Without the binary that hook still exits 127 on every Bash call, which is the whole reason RTK is in this Feature.nodeindependsOn. One of its two documented reasons retires here (claude-codeinstalled EOL Node 18 from nodesource when it found no Node); the other stands unchanged — devbase's own pnpm, commitizen and npm-floor steps need npm.anthropic.claude-codeandanthropic.claude-vscodeVS Code extensions, now the only Claude Code in a container.Tests move with the behaviour
dependsOn installed the claude CLIinverts to assert the CLI is absent. A removal with no assertion cannot distinguish "removed" from "quietly came back through some other feature's owndependsOn" — which is exactly whatdependsOndoes.node_pinned'sclaude-code added no Node of its ownnodesource guard is dropped, not kept. Its subject is gone, so it could no longer fail, and per this repo's own doctrine a check that cannot fail is worse than no check. The comment explaining why is left in its place.Ordering, and the follow-up
The default
.devcontainer/devcontainer.jsonstill referencesdevbase:1, per the dogfooding rule — a frame stops expecting something only after the release that stops providing it. Its comment now says so explicitly. Thelocal/frame builds the working-tree copy, so the removal takes effect there immediately; that asymmetry is noted, since it is the first place anyone will seeclaudemissing.Consumers pin
:1, so2.0.0reaches nobody on a rebuild. The follow-up sweep has to move each frame fromdevbase:1todevbase:2and re-resolve the eight committeddevcontainer-lock.jsonfiles, which neitherdevcontainer upgradenor Dependabot will move.README.mdgains a Migrating to 2.0.0 section covering exactly that, plus how to opt back into the CLI per repository if a container ever genuinely needs it.Note
node-sdkkeeps the CLI regardless — it listsclaude-codein its own frame, because its devbase migration was never completed (a separate finding from RSRMID-3052, still unticketed).Verification
pnpm features:lintandpnpm prettierclean. The container suite runs here in CI, sincefeatures/**is touched — that is what exercises the inverted assertion, as this workspace container has no Docker daemon.