Skip to content

Commit e0d3ead

Browse files
committed
docs: correct auth and network guidance after devcontainer change
The previous commit dropped the features block, --network=host and the ~/.ssh and ~/.config/gh mounts from .devcontainer/devcontainer.json, which left four places in the repo describing a container that no longer exists. .devcontainer/claude-code/README.md led with "Why --network=host is Required" and an example devcontainer.json carrying runArgs and a features block. The default path is now the one the template ships: authenticate claude on the host, and the ~/.claude bind mount plus CLAUDE_CONFIG_DIR hand those credentials to every container, so no OAuth flow runs inside one. The OAuth callback explanation stays, reframed as the reason to opt into host networking, along with what that costs -- VS Code extensions stop installing (vscode-remote-release#9212), and every port the container binds becomes a host port, so two branch containers collide. The example now shows the feature declared in .devcontainer/ci/devcontainer.json, where the image is built, and a launch config with neither features nor runArgs. .devcontainer/claude-code/TROUBLESHOOTING.md repeated "add --network=host" as the fix in its quick-reference config, in Issue 2 and in the setup checklist, and told readers NetworkMode should read "host". Each now points at host authentication first and names host networking as the opt-in with its costs. AGENTS.md claimed gh authentication is shared through a mounted ~/.config/gh. That mount is gone, and it never worked anyway: gh keeps its token in the system keyring, so the mounted hosts.yml carried no oauth_token. GitHub auth arrives as GH_TOKEN, which dl forwards into every workspace it starts. A container opened by anything else -- devpod up, or VS Code's Reopen in Container -- has no gh login until you export it. .devcontainer/Dockerfile created /home/vscode/.ssh and /home/vscode/.config/gh solely to receive the two deleted mounts. Both blocks are removed.
1 parent fcf50ae commit e0d3ead

4 files changed

Lines changed: 55 additions & 56 deletions

File tree

‎.devcontainer/Dockerfile‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,3 @@ RUN echo 'eval "$(pixi completion -s bash)"' >> /home/vscode/.bashrc \
1414
&& echo 'export PATH="$HOME/.pixi/bin:$PATH"' >> /home/vscode/.profile \
1515
&& echo '# Workaround: pixi trampoline fails for bash scripts, so add env bin directly' >> /home/vscode/.profile \
1616
&& echo '[ -d "$HOME/.pixi/envs/claude-shim/bin" ] && export PATH="$HOME/.pixi/envs/claude-shim/bin:$PATH"' >> /home/vscode/.profile
17-
18-
# Create .ssh directory with proper permissions for SSH config mounts
19-
RUN mkdir -p /home/vscode/.ssh && chmod 700 /home/vscode/.ssh
20-
21-
# Create .config/gh directory for GitHub CLI config mounts
22-
RUN mkdir -p /home/vscode/.config/gh

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

Lines changed: 39 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -52,45 +52,58 @@ These files **must be writable** to enable:
5252

5353
### Setup
5454

55-
Add this feature to your `devcontainer.json`:
55+
The feature is declared where the image is built, in `.devcontainer/ci/devcontainer.json`:
5656

5757
```json
5858
{
59+
"build": {
60+
"dockerfile": "../Dockerfile",
61+
"context": "."
62+
},
5963
"features": {
60-
"./claude-code": {}
64+
"../claude-code": {}
65+
}
66+
}
67+
```
68+
69+
CI publishes that image, and the `devcontainer.json` every branch launches from pulls it and declares neither `features` nor `runArgs`:
70+
71+
```json
72+
{
73+
"image": "ghcr.io/blooop/python_template/devcontainer:latest",
74+
"containerEnv": {
75+
"CLAUDE_CONFIG_DIR": "/home/vscode/.claude"
6176
},
62-
"runArgs": ["--network=host"]
77+
"mounts": [
78+
"source=${localEnv:HOME}/.claude,target=/home/vscode/.claude,type=bind"
79+
]
6380
}
6481
```
6582

83+
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.
84+
6685
**Note**: Node.js is automatically installed via the `installsAfter` dependency mechanism - you don't need to explicitly add it to your features.
6786

68-
### Why `--network=host` is Required
87+
### Authentication Uses Host Credentials, Not Host Networking
6988

70-
The `runArgs: ["--network=host"]` is **critical for OAuth authentication** to work in containers.
89+
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.
7190

72-
**How OAuth works:**
73-
1. You run `claude` → starts OAuth flow
74-
2. Opens browser → you click "Authorize"
75-
3. Browser redirects to `http://localhost:<random-port>/callback`
76-
4. OAuth server running in container receives the callback
91+
### Why You Might Opt Into `--network=host`
7792

78-
**The problem without host networking:**
79-
- OAuth server runs on port X **inside container**
80-
- Browser callback goes to port X on **host's localhost**
81-
- ❌ Container's port is not accessible from host → **callback fails**
93+
The one thing the bind mount does not give you is the interactive OAuth login *from inside* the container, which needs host networking to complete:
8294

83-
**The solution:**
84-
- With `--network=host`, container shares host's network namespace
85-
- OAuth server on port X in container = port X on host
86-
- ✅ Browser callback reaches the container → **authentication succeeds**
95+
1. You run `claude` → it starts a callback server on a random port in the container
96+
2. Your browser opens the authorize page, you click "Authorize", and it redirects to `http://localhost:<that-port>/callback`
97+
3. On the default bridge network that port belongs to the container, not the host, so the browser cannot reach it and the CLI sits at "Paste code here"
8798

88-
**Security note:** Host networking gives the container full network access. Only use in trusted environments.
99+
With `--network=host` the container shares the host's network namespace, port X in the container *is* port X on the host, and the callback lands.
89100

90-
**Alternative (if host networking is not acceptable):**
91-
- Authenticate Claude on your host machine first
92-
- Credentials in `~/.claude/.credentials.json` are automatically shared with container
93-
- No OAuth flow needed in container
101+
Two costs come with it, and they are why this template does not set it:
102+
103+
- **VS Code extensions stop installing**: [vscode-remote-release#9212](https://github.com/microsoft/vscode-remote-release/issues/9212), covered again under Troubleshooting below.
104+
- **Every port the container binds becomes a host port.** A container per branch is the reason these repos are launched with `dl`, and two branch containers on the host's network namespace collide on the first port they share.
105+
106+
Host networking also gives the container full access to the host's network, so only use it in environments you trust.
94107

95108
### Build the Container
96109

@@ -377,23 +390,15 @@ Then use both:
377390

378391
**Problem**: Browser clicks "Authorize" but container never receives the callback.
379392

380-
**Solution**: Add `--network=host` to your `devcontainer.json`:
381-
382-
```json
383-
{
384-
"runArgs": ["--network=host"]
385-
}
386-
```
387-
388-
See "Why `--network=host` is Required" section above for details.
393+
**Solution**: Run `claude` on the host instead and let the container read the credentials it writes to `~/.claude`. If you need the login to happen inside the container, add `--network=host` and accept its costs -- see "Why You Might Opt Into `--network=host`" above.
389394

390395
### Interactive `claude` asks for authentication but `claude --print` works
391396

392397
**Problem**: You're authenticated (credentials mounted) but interactive mode prompts for login.
393398

394-
**Root cause**: Without `--network=host`, OAuth callbacks can't reach the container.
399+
**Root cause**: Interactive mode tried to start an OAuth flow, which means it found no usable credentials under `CLAUDE_CONFIG_DIR`.
395400

396-
**Solution**: Add `"runArgs": ["--network=host"]` to devcontainer.json.
401+
**Solution**: Authenticate on the host so `~/.claude/.credentials.json` holds a live token, and check that `~/.claude` is actually mounted and `CLAUDE_CONFIG_DIR` points at it.
397402

398403
### VS Code extensions don't install with `--network=host`
399404

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

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,21 @@
1919

2020
```json
2121
{
22-
"features": {
23-
"ghcr.io/devcontainers/features/node:1": {},
24-
"./claude-code": {}
25-
},
26-
"runArgs": ["--network=host"],
22+
"image": "ghcr.io/blooop/python_template/devcontainer:latest",
2723
"containerEnv": {
2824
"CLAUDE_CONFIG_DIR": "/home/vscode/.claude",
2925
"XDG_CONFIG_HOME": "/home/vscode/.config",
3026
"XDG_CACHE_HOME": "/home/vscode/.cache",
3127
"XDG_DATA_HOME": "/home/vscode/.local/share"
32-
}
28+
},
29+
"mounts": [
30+
"source=${localEnv:HOME}/.claude,target=/home/vscode/.claude,type=bind"
31+
]
3332
}
3433
```
3534

35+
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.
36+
3637
## Common Issues and Solutions
3738

3839
### Issue 1: Setup Wizard Runs on Every Container Rebuild
@@ -89,6 +90,9 @@ claude # Should go straight to interactive mode without wizard
8990
OAuth callback server runs inside container on a random port (e.g., `localhost:35673`). Your browser tries to connect to that port on the HOST, but the container's port isn't accessible.
9091

9192
**Solution:**
93+
Authenticate on the host, where the browser can reach the callback port, and let the container read the resulting credentials through the `~/.claude` bind mount. No OAuth flow then runs in the container at all.
94+
95+
**Alternative, if you want the login to happen inside the container:**
9296
Add `--network=host` to devcontainer.json:
9397

9498
```json
@@ -99,11 +103,8 @@ Add `--network=host` to devcontainer.json:
99103

100104
This makes the container share the host's network namespace, so ports inside the container are accessible from the host browser.
101105

102-
**Trade-off:**
103-
Using `--network=host` gives the container full network access and may prevent VS Code extensions from installing (known issue: [#9212](https://github.com/microsoft/vscode-remote-release/issues/9212)).
104-
105-
**Workaround if you can't use --network=host:**
106-
Authenticate on your host machine first, then credentials are shared via mounts.
106+
**What that costs:**
107+
VS Code extensions stop installing (known issue: [#9212](https://github.com/microsoft/vscode-remote-release/issues/9212)), the container gets full access to the host's network, and every port the container binds becomes a host port -- so two branch containers of the same repo collide on the first port they share.
107108

108109
### Issue 3: `claude --print` Works But Interactive `claude` Asks for Login
109110

@@ -247,16 +248,15 @@ Look for:
247248
```bash
248249
# On HOST
249250
docker inspect <container-id> | jq '.[0].HostConfig.NetworkMode'
250-
# Should show: "host"
251+
# "host" only if you opted into --network=host; otherwise the default bridge network
251252
```
252253

253254
## Complete Setup Checklist
254255

255256
When setting up a new workspace:
256257

257-
- [ ] Node.js feature added to devcontainer.json
258-
- [ ] `./claude-code` feature added
259-
- [ ] `runArgs: ["--network=host"]` added
258+
- [ ] `./claude-code` feature declared in `.devcontainer/ci/devcontainer.json`, so the published image carries it
259+
- [ ] `claude` authenticated on the host, so no OAuth flow runs in the container
260260
- [ ] Environment variables added (CLAUDE_CONFIG_DIR, XDG_*)
261261
- [ ] Files exist on host: `.credentials.json`, `.claude.json`
262262
- [ ] File permissions: `chmod 600` on sensitive files

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This project uses a devcontainer with pixi for environment management.
66

77
### Available Tools
88

9-
- **GitHub CLI (`gh`)**: Available via `pixi run gh` or directly if using a login shell. The container mounts the host's `~/.config/gh` directory, so if the user is authenticated on the host, authentication is shared automatically.
9+
- **GitHub CLI (`gh`)**: Available via `pixi run gh` or directly if using a login shell. Authentication arrives as `GH_TOKEN`, which `dl` forwards into every workspace it starts, taking it from `GH_TOKEN`, `GITHUB_TOKEN` or `gh auth token` -- whichever answers first. The container used to mount the host's `~/.config/gh` instead, which never worked: `gh` keeps its token in the system keyring, so the mounted `hosts.yml` carried no `oauth_token`. If the container was opened by something other than `dl` -- a plain `devpod up`, or VS Code's Reopen in Container -- it has no `gh` login and you have to export `GH_TOKEN` yourself.
1010

1111
### Running Commands
1212

0 commit comments

Comments
 (0)