Skip to content

Commit 0fd4176

Browse files
committed
docs: describe the real ~/.claude mount instead of read-only protection that does not exist
The claude-code feature declares one mount, and it is a read-write bind of the whole directory: source=${localEnv:HOME}/.claude,target=/home/vscode/.claude,type=bind No `ro` flag appears anywhere in the feature, and there are no per-file mounts. The README and TROUBLESHOOTING guide described something else: five per-file read-only binds over CLAUDE.md, settings.json, agents/, commands/ and hooks/, said to prevent prompt injection and hook manipulation, with only .credentials.json and .claude.json writable. None of that is implemented, so both files now describe what the mount actually is -- one shared read-write config directory -- and state the consequence: hooks/ and settings.json are executed by Claude Code wherever it runs, so content a container writes there runs on the host the next time Claude Code starts there. The worst of it was a documented debugging step: echo "test" >> ~/.claude/CLAUDE.md # Should fail with "Read-only file system" It does not fail. It appends to the developer's real global instructions file, which is then loaded into every later Claude Code session. It is replaced with a check that CLAUDE_CONFIG_DIR points at the mount and that the directory is writable, using a throwaway file it deletes. Documentation only -- no mount, feature or script is changed. Whether the non-credential paths should actually become read-only binds is left open.
1 parent e0d3ead commit 0fd4176

2 files changed

Lines changed: 65 additions & 92 deletions

File tree

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

Lines changed: 34 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# Claude Code CLI - Local Dev Container Feature
22

3-
A local Dev Container Feature that installs the Claude Code CLI and configures it with read-only mounts to your host machine's Claude configuration.
3+
A local Dev Container Feature that installs the Claude Code CLI and bind-mounts your host machine's Claude configuration directory into the container.
44

55
## What This Feature Does
66

77
This feature combines two capabilities:
88

99
1. **CLI Installation**: Installs the `@anthropic-ai/claude-code` npm package globally
10-
2. **Configuration Mounting**: Mounts your host machine's Claude configuration files into the container as read-only binds
10+
2. **Configuration Mounting**: Bind-mounts your host machine's `~/.claude` directory into the container, read-write
1111

1212
## What Gets Installed
1313

@@ -17,36 +17,25 @@ This feature combines two capabilities:
1717

1818
## What Gets Mounted
1919

20-
The following files and directories from your **host machine** are mounted into the container:
20+
One mount, and it is the whole directory:
2121

22-
### Read-Only Mounts (Security-Protected)
23-
- `~/.claude/CLAUDE.md` → Global project instructions
24-
- `~/.claude/settings.json` → Claude CLI settings
25-
- `~/.claude/agents/` → Custom agent configurations
26-
- `~/.claude/commands/` → Command definitions
27-
- `~/.claude/hooks/` → Event-driven shell hooks
22+
```
23+
source=${localEnv:HOME}/.claude,target=/home/vscode/.claude,type=bind
24+
```
2825

29-
These are **read-only** (`ro` flag) to prevent:
30-
- Prompt injection attacks that could modify your Claude configuration
31-
- Accidental modification of shared configuration from within containers
32-
- Security issues related to hook manipulation
26+
There is no `ro` flag and no per-file mount. The host and every container of every branch share one `~/.claude`, read-write, so anything running in a container can modify any of it -- `CLAUDE.md`, `settings.json`, `agents/`, `commands/` and `hooks/` included.
3327

34-
### Read-Write Mounts (Authentication & State)
35-
- `~/.claude/.credentials.json` → OAuth access/refresh tokens
36-
- `~/.claude/.claude.json` → Account info, user ID, workspace setup tracking
28+
### What That Means
3729

38-
These files **must be writable** to enable:
39-
- OAuth authentication flow and token refresh
40-
- Workspace setup state tracking (`projectOnboardingSeenCount`)
41-
- Session continuity across container rebuilds
30+
`hooks/` and `settings.json` are executed by Claude Code wherever it runs. Content a container writes there therefore runs on the **host**, the next time Claude Code starts on the host, and a `postCreateCommand` from a repository you have not read is enough to put it there.
4231

43-
### Why These Must Be Writable
32+
It is not a confidentiality boundary either: code running in the container holds the live Claude credentials the mount carries, and under `dl` a `GH_TOKEN` with repo and workflow scopes.
4433

45-
**`.credentials.json`**: OAuth tokens need to be refreshed periodically. Claude writes updated tokens to this file.
34+
### Why It Is Still One Read-Write Directory
4635

47-
**`.claude.json`**: Claude tracks per-workspace setup state here. The `projectOnboardingSeenCount` field must be writable so Claude doesn't show the setup wizard on every launch.
36+
Sharing the directory is what makes credentials work across the host and every branch container. `.credentials.json` has to be writable because the access token is short-lived and a refresh has to persist -- a read-only or copied arrangement drifts into a re-auth. `.claude.json` has to be writable because Claude tracks per-workspace onboarding and trust state there, so a container that cannot write it re-onboards on every launch.
4837

49-
⚠️ **Security Note**: These files contain sensitive data and are mounted read-write by necessity. They are only accessible by the container user and stored with `600` permissions. Only use this feature with trusted repositories.
38+
Splitting the rest of the directory into separate read-only binds is possible and is not what this feature does today. What the container buys as it stands is reproducible dependencies and non-colliding concurrent work, not safety against hostile code. `blooop/wayfinder`'s `.devcontainer/devcontainer.json` states the same threat model in the comment above its `mounts` block.
5039

5140
## Usage
5241

@@ -193,11 +182,15 @@ Check mounted files:
193182
ls -la ~/.claude/
194183
```
195184

196-
Verify mounts are read-only:
185+
Verify the config directory is mounted, writable, and pointed at:
197186
```bash
198-
echo "test" >> ~/.claude/CLAUDE.md # Should fail with "Read-only file system"
187+
env | grep CLAUDE_CONFIG_DIR # /home/vscode/.claude
188+
mount | grep /home/vscode/.claude # one bind, rw
189+
touch ~/.claude/.mount-check && rm ~/.claude/.mount-check && echo writable
199190
```
200191

192+
The write test uses a throwaway file on purpose. Do not test the mount by appending to `CLAUDE.md`, `settings.json` or anything under `hooks/`: the mount is read-write, so the write lands on the host's real configuration and is loaded into every later Claude Code session.
193+
201194
## Authentication
202195

203196
### How It Works
@@ -253,14 +246,9 @@ devpod up . --recreate
253246

254247
## Modifying Configuration
255248

256-
Configuration files (except credentials) are read-only. You **cannot** modify Claude settings from within the container.
249+
The container writes to the same `~/.claude` as the host, so an edit made in either place is an edit to the one shared configuration.
257250

258-
To change configuration:
259-
260-
1. Edit files on your **host machine**: `~/.claude/settings.json`, `~/.claude/CLAUDE.md`, etc.
261-
2. Restart or rebuild the container to see changes
262-
263-
This is by design for security (prevents prompt injection attacks).
251+
Editing from the **host** is still the better habit: `~/.claude/settings.json`, `~/.claude/CLAUDE.md` and the rest are yours across every branch container, and a change made on the host is one you meant to make. The mount is live, so restarting `claude` picks up a change -- no rebuild needed.
264252

265253
## What Would Change Before Publishing to GHCR
266254

@@ -420,33 +408,27 @@ touch ~/.claude/CLAUDE.md ~/.claude/settings.json
420408

421409
## Security Notes
422410

423-
This implementation makes conscious security trade-offs to enable OAuth authentication and persistent setup state:
411+
The whole `~/.claude` directory is bind-mounted read-write as one mount, so nothing in it is held back from the container.
412+
413+
### What Code in the Container Can Read and Write
414+
- **`.credentials.json`**: the live OAuth access and refresh tokens
415+
- **`.claude.json`**: account info, user ID, per-workspace onboarding and trust state
416+
- **`CLAUDE.md`**, **`settings.json`**, **`agents/`**, **`commands/`**, **`hooks/`**: the host's copies, in place
424417

425-
### What's Protected (Read-Only Mounts)
426-
- **CLAUDE.md**: Prevents prompt injection attacks that could modify your global instructions
427-
- **settings.json**: Prevents config tampering
428-
- **agents/**, **commands/**, **hooks/**: Prevents malicious code execution through modified hooks
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.
429419

430-
### What's Writable (Necessary Trade-off)
431-
- **`.credentials.json`**: OAuth tokens must be writable for token refresh to work
432-
- **`.claude.json`**: Workspace state must be writable to persist `projectOnboardingSeenCount` and other setup tracking
420+
### The Consequence Worth Naming
421+
`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.
433422

434-
### Security Mitigations
435-
- Files have `600` permissions (user-only access)
436-
- Only use this feature in **trusted repositories**
437-
- Container user isolation provides some protection
438-
- Writable files are limited to authentication/state only
439-
- All configuration and code execution files remain read-only
423+
### Why It Is Accepted
424+
One shared config directory is what makes auth work across the host and every branch container without a re-auth, and it is why a container never re-onboards. That is the trade; the isolation buys reproducible dependencies and non-colliding concurrent work, not protection from hostile code. `blooop/wayfinder`'s `.devcontainer/devcontainer.json` writes out the same threat model above its `mounts` block. Treat a repository you launch this way as code you are running with your own credentials, because that is what it is.
440425

441-
### Known Risks
442-
- A malicious process in the container could exfiltrate OAuth tokens from `.credentials.json`
443-
- A malicious process could modify workspace state in `.claude.json`
444-
- **Recommendation**: Only use in repositories you trust, as you would with any dev container configuration
426+
Whether the non-credential paths should become read-only binds is an open question, not a settled one.
445427

446428
See related security discussions:
447429
- [anthropics/claude-code#4478](https://github.com/anthropics/claude-code/issues/4478)
448430
- [anthropics/claude-code#2350](https://github.com/anthropics/claude-code/issues/2350)
449-
- Original read-only approach: [PR #25](https://github.com/anthropics/devcontainer-features/pull/25)
431+
- Per-file read-only approach this feature does not implement: [PR #25](https://github.com/anthropics/devcontainer-features/pull/25)
450432

451433
## Reference
452434

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

Lines changed: 31 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,18 @@
55
### Files That Must Exist on Host
66

77
```bash
8-
~/.claude/
9-
├── .credentials.json # OAuth tokens (must be writable)
10-
├── .claude.json # Account info, setup state (must be writable)
11-
├── CLAUDE.md # Global instructions (read-only)
12-
├── settings.json # Settings (read-only)
13-
├── agents/ # Custom agents (read-only)
14-
├── commands/ # Custom commands (read-only)
15-
└── hooks/ # Event hooks (read-only)
8+
~/.claude/ # one bind mount, read-write, shared with every container
9+
├── .credentials.json # OAuth tokens
10+
├── .claude.json # Account info, setup state
11+
├── CLAUDE.md # Global instructions
12+
├── settings.json # Settings
13+
├── agents/ # Custom agents
14+
├── commands/ # Custom commands
15+
└── hooks/ # Event hooks
1616
```
1717

18+
Every one of these is writable from inside the container, and a write lands on the host. See "Security Considerations" below for what follows from that.
19+
1820
### Critical Configuration in devcontainer.json
1921

2022
```json
@@ -129,47 +131,40 @@ Two different issues:
129131
- Have to authenticate again
130132

131133
**Root Cause:**
132-
`.credentials.json` or `.claude.json` is not mounted, or is mounted read-only.
134+
`~/.claude` is not mounted, so `claude` wrote its credentials into the container's own filesystem and they went away with the container.
133135

134136
**Solution:**
135137

136-
1. **Verify mounts in container:**
138+
1. **Verify the mount in the container:**
137139
```bash
138140
devpod ssh pythontemplate
139141
mount | grep claude
140142
```
141143

142-
Should show:
144+
Should show one bind of the directory, read-write:
143145
```
144-
/dev/... on /home/vscode/.claude/.credentials.json type ext4 (rw,...)
145-
/dev/... on /home/vscode/.claude/.claude.json type ext4 (rw,...)
146+
/dev/... on /home/vscode/.claude type ext4 (rw,...)
146147
```
147148

148149
2. **Check files exist on host:**
149150
```bash
150151
ls -la ~/.claude/.credentials.json ~/.claude/.claude.json
151152
```
152153

153-
3. **Verify files are writable (not ro):**
154-
The mounts MUST be read-write for auth to persist.
154+
3. **Check the mount is read-write:**
155+
A refresh has to persist, so `rw` in the line above is load-bearing. The feature declares no `ro` flag, so a read-only mount means something outside it added one.
155156

156-
### Issue 5: "Read-only file system" Error
157+
### Issue 5: A Container Changed the Host's Claude Configuration
157158

158159
**Symptoms:**
159-
- Error when trying to write to `~/.claude/CLAUDE.md` or similar
160-
- Operations fail with "Read-only file system"
161-
162-
**Expected Behavior:**
163-
This is intentional! Security files are mounted read-only:
164-
- `CLAUDE.md`, `settings.json`, `agents/`, `commands/`, `hooks/` → Read-only
160+
- `~/.claude/CLAUDE.md`, `settings.json` or a file under `hooks/` differs from what you left on the host
161+
- A hook or setting you did not write takes effect when you start `claude` on the host
165162

166-
**Why?**
167-
Prevents prompt injection attacks that could modify your Claude configuration.
163+
**Root Cause:**
164+
Not a malfunction. `~/.claude` is one read-write bind of the whole directory, so the host and every container share it and anything in a container can write any of it. `hooks/` and `settings.json` are executed by Claude Code wherever it runs, so what a container leaves there runs on the host next time.
168165

169166
**Solution:**
170-
Edit these files on your HOST machine, then restart/rebuild the container.
171-
172-
Only `.credentials.json` and `.claude.json` are read-write (needed for auth and state).
167+
Restore the files from wherever your configuration lives -- keeping `~/.claude` under version control is what makes a change like this visible and reversible. Then look at what put it there: a `postCreateCommand`, a hook, or an agent session in the container all reach that far.
173168

174169
### Issue 6: File Permission Errors (600 vs 664)
175170

@@ -204,7 +199,7 @@ cat ~/.claude/.claude.json | jq '.oauthAccount.emailAddress'
204199
```bash
205200
# In container
206201
mount | grep claude
207-
# Should show all mounted files/directories
202+
# Should show one bind of /home/vscode/.claude, rw
208203

209204
ls -la ~/.claude/
210205
# Should show files from your host
@@ -332,20 +327,16 @@ watch -n 1 'stat ~/.claude/.claude.json | grep Modify'
332327

333328
## Security Considerations
334329

335-
### What's Protected (Read-Only)
336-
- `CLAUDE.md` - Prevents prompt injection
337-
- `settings.json` - Prevents config tampering
338-
- `agents/`, `commands/`, `hooks/` - Prevents malicious modifications
330+
### What the Mount Actually Is
331+
One read-write bind of the whole `~/.claude` directory. No `ro` flag, no per-file mounts. `.credentials.json`, `.claude.json`, `CLAUDE.md`, `settings.json`, `agents/`, `commands/` and `hooks/` are all writable from inside the container, and a write lands on the host's copy.
332+
333+
### The Consequence
334+
`hooks/` and `settings.json` are executed by Claude Code wherever it runs, so content a container writes there runs on the **host** the next time Claude Code starts there -- a `postCreateCommand` from a repository nobody read is enough. The container also holds the live Claude credentials from the mount and, under `dl`, a `GH_TOKEN` carrying repo and workflow scopes. It is not a confidentiality or integrity boundary.
339335

340-
### What's Writable (Necessary Risk)
341-
- `.credentials.json` - OAuth tokens (necessary for auth)
342-
- `.claude.json` - Setup state (necessary to skip wizard)
336+
### Why It Is Accepted
337+
Sharing one config directory is what makes credentials work across the host and every branch container: the access token is short-lived, so a read-only or copied arrangement drifts into a re-auth, and a container that cannot write `.claude.json` re-onboards on every launch. The isolation buys reproducible dependencies and non-colliding concurrent work, not safety against hostile code. `blooop/wayfinder`'s `.devcontainer/devcontainer.json` writes out the same threat model above its `mounts` block.
343338

344-
### Mitigation
345-
- Only use in trusted repositories
346-
- Files have `600` permissions (user-only access)
347-
- Container user isolation
348-
- Regular review of `.claude.json` changes
339+
Keeping `~/.claude` under version control is the one practical measure here: it makes a change from a container visible instead of silent.
349340

350341
## Known Limitations
351342

0 commit comments

Comments
 (0)