Skip to content

Commit cbff39a

Browse files
committed
docs: correct install mechanism and remove the installsAfter claim
The .devcontainer tree is about to be copied into 26 repos, so every factual statement in it had to be checked against the code beside it. Part 1, both verified against install.sh and devcontainer-feature.json: - The CLI is not the @anthropic-ai/claude-code npm package. install.sh runs `pixi global install --channel https://prefix.dev/blooop claude-shim`, downloading pixi to /usr/local/bin/pixi first if the image does not carry it. The blooop prefix.dev channel is therefore a dependency of this feature, which the README now says. install.sh only checks that the trampoline exists, so the binary arrives on the first `claude` run -- also now stated. - Removed the claim that Node.js is installed via `installsAfter`, in both places. devcontainer-feature.json declares no `installsAfter` key at all, and nothing node-related is installed because the CLI is a pixi package. The Container requirements bullet names pixi instead; nothing was invented to replace the mechanism. What the sweep found beyond that: - init-host.sh creates only ~/.claude, so "they will be created if they don't exist" was false for the subdirectories and files listed under it, and the mount being one directory bind means a missing subdirectory produces no mount warnings. Corrected in the README Requirements section, its "Mount warnings about missing files" troubleshooting entry, and the TROUBLESHOOTING quick reference. - install.sh runs at image build time, so it never touches the host. The Security Notes claim that its 600 credential files "keeps other users on the host out" was wrong; the files it creates live in the image and the bind mount covers them at runtime. Same correction to "Creates .claude/ structure in the container". - Authentication "First-Time Setup" still told you to run the OAuth flow inside the container, which the same file says three times cannot complete on the default bridge network. It now points at the host. - Two "rebuild the container" steps after editing ~/.claude on the host: the mount is live, so restarting `claude` is enough. - ci/devcontainer.json declares "../claude-code", not "./claude-code" -- fixed in three places. - The standalone `sudo ./install.sh` test is a container operation; on a host with no /home/vscode it exits with an error.
1 parent 0fd4176 commit cbff39a

2 files changed

Lines changed: 26 additions & 26 deletions

File tree

‎.devcontainer/claude-code/README.md‎

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ A local Dev Container Feature that installs the Claude Code CLI and bind-mounts
66

77
This feature combines two capabilities:
88

9-
1. **CLI Installation**: Installs the `@anthropic-ai/claude-code` npm package globally
9+
1. **CLI Installation**: `install.sh` runs `pixi global install --channel https://prefix.dev/blooop claude-shim`, and downloads pixi to `/usr/local/bin/pixi` first if the base image does not already carry it
1010
2. **Configuration Mounting**: Bind-mounts your host machine's `~/.claude` directory into the container, read-write
1111

1212
## What Gets Installed
1313

14-
- **Claude Code CLI**: The `claude` command becomes available in your container
14+
- **Claude Code CLI**: The `claude` command becomes available in your container. It comes from the `claude-shim` package on the `blooop` prefix.dev channel, so that channel is a dependency of this feature. `install.sh` checks only that the pixi trampoline exists -- the binary it points at is downloaded on the first `claude` run.
1515
- **VS Code Extension**: Automatically installs the `anthropic.claude-code` extension
16-
- **Configuration Directories**: Creates `.claude/` structure in the container
16+
- **Configuration Directories**: `install.sh` creates the `.claude/` tree, though it does so while the image is built -- at runtime the host's bind mount covers it
1717

1818
## What Gets Mounted
1919

@@ -71,8 +71,6 @@ CI publishes that image, and the `devcontainer.json` every branch launches from
7171

7272
Declaring the feature a second time there makes the devcontainer spec build a derived image on the first launch of every branch, reinstalling what the pulled image already carries.
7373

74-
**Note**: Node.js is automatically installed via the `installsAfter` dependency mechanism - you don't need to explicitly add it to your features.
75-
7674
### Authentication Uses Host Credentials, Not Host Networking
7775

7876
No OAuth flow runs inside the container. You authenticate `claude` once on the host, and the `~/.claude` bind mount plus `CLAUDE_CONFIG_DIR=/home/vscode/.claude` point the container at those same credentials, refresh tokens included. Every container of every branch reads them, and nothing has to reach the host's network to do it.
@@ -109,7 +107,7 @@ With VS Code:
109107

110108
### Host Machine
111109

112-
You should have these files/directories on your host machine (they will be created if they don't exist):
110+
`init-host.sh` runs on the host as the `initializeCommand` and creates `~/.claude` if it is missing. Nothing creates the contents below; they are optional, and the container starts without them:
113111

114112
```bash
115113
~/.claude/
@@ -120,7 +118,7 @@ You should have these files/directories on your host machine (they will be creat
120118
└── hooks/ # Optional: event hooks
121119
```
122120

123-
**Note**: If these don't exist on your host, the container will still build successfully, but you may see mount warnings. You can create them with:
121+
**Note**: The mount is the `~/.claude` directory itself, so a missing subdirectory or file costs nothing at launch. To create them anyway:
124122

125123
```bash
126124
mkdir -p ~/.claude/{agents,commands,hooks}
@@ -130,7 +128,7 @@ touch ~/.claude/settings.json
130128

131129
### Container
132130

133-
- **Node.js 18+** and **npm** are automatically installed via the `installsAfter` dependency mechanism
131+
- **pixi**, which the `Dockerfile` installs to `/usr/local/bin/pixi`; `install.sh` downloads it there itself if it is missing
134132
- No manual configuration required
135133

136134
## Assumptions
@@ -163,13 +161,15 @@ touch ~/.claude/settings.json
163161

164162
### Testing Install Script
165163

166-
You can test the install script standalone:
164+
You can test the install script standalone, from inside the container:
167165

168166
```bash
169167
cd .devcontainer/claude-code
170168
sudo ./install.sh
171169
```
172170

171+
It resolves its target from `_REMOTE_USER` and `_REMOTE_USER_HOME` and falls back to `vscode`, so on a host with no `/home/vscode` it exits with an error rather than doing anything.
172+
173173
### Debugging
174174

175175
Check if Claude is installed:
@@ -196,12 +196,12 @@ The write test uses a throwaway file on purpose. Do not test the mount by append
196196
### How It Works
197197

198198
1. **Already Authenticated on Host**: If you have Claude Code set up on your host machine, credentials are automatically shared with the container
199-
2. **First-Time Setup**: Run `claude` in the container and follow the OAuth flow:
200-
- The CLI will provide an OAuth URL
201-
- Open the URL in your browser (on your host machine)
199+
2. **First-Time Setup**: Run `claude` on the **host** and follow the OAuth flow there:
200+
- The CLI provides an OAuth URL
201+
- Open the URL in your browser
202202
- Click "Authorize"
203-
- The callback should complete automatically, or you may need to paste the code
204-
- Credentials are saved to `~/.claude/.credentials.json` on your host
203+
- The callback completes, because the CLI and the browser are both on the host
204+
- Credentials are saved to `~/.claude/.credentials.json`, and the mount carries them into every container
205205

206206
### OAuth Callback Behavior
207207

@@ -214,7 +214,7 @@ The OAuth flow opens a local callback server. In containers, this can behave dif
214214

215215
**"Paste code here" prompt hangs forever:**
216216
- Check that `~/.claude/.credentials.json` exists on your host with proper permissions (`600`)
217-
- Try authenticating on your host machine first, then rebuild the container
217+
- Try authenticating on your host machine first, then restart `claude` in the container -- the mount is live, so no rebuild is needed
218218
- If the callback fails, look for the authorization code in the URL after clicking "Authorize"
219219

220220
**Credentials not persisting:**
@@ -236,8 +236,7 @@ mv ~/.claude/.claude.json.tmp ~/.claude/.claude.json
236236
jq '. + {themeMode: "dark"}' ~/.claude/.claude.json > ~/.claude/.claude.json.tmp
237237
mv ~/.claude/.claude.json.tmp ~/.claude/.claude.json
238238

239-
# Rebuild container
240-
devpod up . --recreate
239+
# Then restart `claude` in the container -- the mount is live, so no rebuild is needed
241240
```
242241

243242
**Root cause:** Claude tracks setup wizard completion per-workspace in `.claude.json` under `.projects["/workspaces/pythontemplate"].projectOnboardingSeenCount`. When this is `0`, the setup wizard runs. Set it to `1` to mark setup as complete.
@@ -310,7 +309,7 @@ Users would then reference it as:
310309
}
311310
```
312311

313-
Instead of `"./claude-code": {}`
312+
Instead of `"../claude-code": {}`
314313

315314
## Optional: Future Composition
316315

@@ -397,9 +396,9 @@ Then use both:
397396
2. **Authenticate on host**, mount credentials, remove runArgs (no OAuth needed in container)
398397
3. **Manually install extensions** after container starts
399398

400-
### Mount warnings about missing files
399+
### `~/.claude` missing on the host
401400

402-
**Solution**: Create the directories on your host:
401+
**Solution**: The `initializeCommand` (`init-host.sh`) creates it before the container starts. To lay out the rest yourself:
403402

404403
```bash
405404
mkdir -p ~/.claude/{agents,commands,hooks}
@@ -415,7 +414,7 @@ The whole `~/.claude` directory is bind-mounted read-write as one mount, so noth
415414
- **`.claude.json`**: account info, user ID, per-workspace onboarding and trust state
416415
- **`CLAUDE.md`**, **`settings.json`**, **`agents/`**, **`commands/`**, **`hooks/`**: the host's copies, in place
417416

418-
`install.sh` creates the two credential files with `600` permissions when they do not already exist, which keeps other users on the host out. It does not restrict code running inside the container, which runs as the user those files belong to.
417+
`install.sh` runs when the image is built, so the two `600` credential files it creates live in the image and the bind mount covers them at runtime. Permissions on the host's real files are whatever the host set -- see Issue 6 in TROUBLESHOOTING.md. Nothing here restricts code running inside the container, which runs as the user those files belong to.
419418

420419
### The Consequence Worth Naming
421420
`hooks/` and `settings.json` are executed by Claude Code wherever it runs. Code in the container that writes there gets its content executed on the **host**, the next time Claude Code starts there -- a `postCreateCommand` from a repository you have not read reaches that far. The container also carries the live Claude credentials and, under `dl`, a `GH_TOKEN` with repo and workflow scopes, so it is not a confidentiality boundary either.

‎.devcontainer/claude-code/TROUBLESHOOTING.md‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
## Quick Reference
44

5-
### Files That Must Exist on Host
5+
### Files on the Host
6+
7+
`init-host.sh` creates `~/.claude` as the `initializeCommand`. Everything inside it is optional, and only `.credentials.json` is load-bearing for an authenticated `claude`.
68

79
```bash
810
~/.claude/ # one bind mount, read-write, shared with every container
@@ -34,7 +36,7 @@ Every one of these is writable from inside the container, and a write lands on t
3436
}
3537
```
3638

37-
The `./claude-code` feature is declared in `.devcontainer/ci/devcontainer.json`, the config CI builds the image from, so there is no `features` block here -- and no `runArgs` either.
39+
The `../claude-code` feature is declared in `.devcontainer/ci/devcontainer.json`, the config CI builds the image from, so there is no `features` block here -- and no `runArgs` either.
3840

3941
## Common Issues and Solutions
4042

@@ -68,8 +70,7 @@ mv ~/.claude/.claude.json.tmp ~/.claude/.claude.json
6870
jq '. + {themeMode: "dark"}' ~/.claude/.claude.json > ~/.claude/.claude.json.tmp
6971
mv ~/.claude/.claude.json.tmp ~/.claude/.claude.json
7072

71-
# Rebuild container
72-
devpod up . --recreate
73+
# Then restart `claude` in the container -- the mount is live, so no rebuild is needed
7374
```
7475

7576
**Why 999?** The field is `projectOnboardingSeenCount` - it increments each time you see the wizard. Setting it high tells Claude "this workspace has been onboarded many times, skip the wizard."
@@ -250,7 +251,7 @@ docker inspect <container-id> | jq '.[0].HostConfig.NetworkMode'
250251

251252
When setting up a new workspace:
252253

253-
- [ ] `./claude-code` feature declared in `.devcontainer/ci/devcontainer.json`, so the published image carries it
254+
- [ ] `../claude-code` feature declared in `.devcontainer/ci/devcontainer.json`, so the published image carries it
254255
- [ ] `claude` authenticated on the host, so no OAuth flow runs in the container
255256
- [ ] Environment variables added (CLAUDE_CONFIG_DIR, XDG_*)
256257
- [ ] Files exist on host: `.credentials.json`, `.claude.json`

0 commit comments

Comments
 (0)