Skip to content

Commit b7d810c

Browse files
authored
Merge pull request #507 from thevibeworks/fix/home-chown-506
fix(entrypoint): chown $DEVA_HOME itself after UID remap
2 parents c1d482a + 0a8365b commit b7d810c

4 files changed

Lines changed: 100 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
- Intermittent `env: 'claude': Permission denied` at container start
12+
(#506): `usermod -u` chowns the home tree with the top-level dir last,
13+
so a transient error while walking live host mounts inside home left
14+
`/home/deva` at the build UID with mode 750 — untraversable for the
15+
remapped user. The entrypoint now chowns `$DEVA_HOME` itself explicitly
16+
(non-recursive) after the UID remap. Latent since 5807889; the 7511464
17+
whitelist fixed subdirs but never the home dir itself.
18+
1019
## [0.18.0] - 2026-07-27
1120

1221
### Added

DEV-LOGS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
- Minimal markdown markers, no unnecessary formatting, minimal emojis.
1414
- Reference issue numbers in the format `#<issue-number>` for easy linking.
1515

16+
# [2026-07-28] Dev Log: home dir chown race bricks containers #506
17+
- Why: intermittent `env: 'claude': Permission denied` on fresh containers. /home/deva stuck at build UID 1001 mode 750 (noble HOME_MODE) after remap to host UID — user can't traverse its own home. usermod's implicit home-tree chown walks live host mounts (~/.claude churning under concurrent sessions), aborts mid-walk with rc=12 AFTER updating passwd; shadow chowns the top dir last, so it never gets fixed. The 7511464 whitelist chowns subdirs, never $DEVA_HOME itself. Latent since 5807889 dropped the recursive home chown; only bites when the walk races live mounts, which is why sibling containers were fine.
18+
- What: explicit non-recursive `chown "$DEVA_UID:$DEVA_GID" "$DEVA_HOME"` in setup_nonroot_user, after the usermod block, using the adapted DEVA_UID so the usermod-failed-entirely variant stays consistent. Devlog with full forensics in docs/devlog/20260728-home-dir-chown-race.org. Verified by fault injection: stub usermod (passwd updated, chown skipped, exit 12) reproduces the brick unpatched, comes out clean patched.
19+
- Result: half-succeeded UID remaps no longer brick containers. Bricked-container first aid: `docker exec <c> chown <uid>:<gid> /home/deva`. Open: entrypoint has zero test coverage — two "Permission denied" regressions shipped from the same function; the usermod stub is a ready-made regression test.
20+
1621
# [2026-07-27] Dev Log: DEVA_AUTH_TAG for in-container account identity #496
1722
- Why: two claude containers on `--auth-with a.credentials.json` / `b.credentials.json` are indistinguishable from inside — /status shows the shared `.claude.json`'s oauthAccount (whoever logged in last), and claude-code-statusline's "account-scoped" caches under the shared `~/.claude/statusline` actively bleed across accounts (B renders A's 5h/7d bars whenever A fetched last; profile.cache sticks to the first account for 24h). Credentials files carry no identity — tokens rotate, no email/uuid — so the runner is the only party that knows which account a container talks as.
1823
- What: hoist generate_auth_tag() out of the container-name rewrite branch (it already produced `auth-default | auth-file-<stem> | api-key-<last4> | env` for naming and the `deva.auth_tag` label) and export it as `DEVA_AUTH_TAG` on every run. No naming/label change. Verified all three modes via scratch-HOME `--dry-run`.

docker-entrypoint.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,13 @@ setup_nonroot_user() {
258258
DEVA_UID="$actual_uid"
259259
fi
260260
fi
261+
# usermod's implicit home chown walks the whole tree (including live
262+
# host mounts) and shadow chowns $DEVA_HOME itself LAST -- any
263+
# transient error mid-walk (rc=12) leaves the home dir at the
264+
# build-time UID with mode 750, so the remapped user cannot even
265+
# traverse into its own home ("env: 'claude': Permission denied").
266+
# Chown the home dir itself explicitly, non-recursively.
267+
chown "$DEVA_UID:$DEVA_GID" "$DEVA_HOME" 2>/dev/null || true
261268
# Fix container-managed directories (whitelist approach - safe for mounted volumes)
262269
# These directories are created at image build time and must be chowned to match host UID
263270
for dir in .npm-global .local .oh-my-zsh .skills .config .cache go; do
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
* [2026-07-28] Home dir chown race: usermod aborts, $DEVA_HOME stays untraversable :FIX:ENTRYPOINT:RELIABILITY:
2+
3+
** Tension
4+
Fresh persistent container, credentials-file auth, latest image:
5+
6+
#+BEGIN_EXAMPLE
7+
[deva] Starting Claude Code v2.1.220
8+
env: 'claude': Permission denied
9+
#+END_EXAMPLE
10+
11+
Same launcher, same image family, eleven sibling containers running fine.
12+
The 2026-01-08 devlog claims this exact symptom was fixed by the whitelist
13+
chown (=7511464=). It wasn't — the whitelist fixed the subdirs and left a
14+
hole one level up.
15+
16+
** Observation
17+
Inside the broken container:
18+
19+
- =claude.exe= is =-rwxr-xr-x= and every dir on the way down is 755. Not the binary.
20+
- =namei -l= shows the one bad component: =/home/deva= itself is
21+
=drwxr-x--- 1001:1001= while the remapped user is 501:20. Mode 750 comes
22+
from Ubuntu noble's =useradd= default (=HOME_MODE 0750=); owner 1001 is the
23+
build-time UID. The user cannot traverse into its own home, so =env=
24+
reports EACCES on everything under it.
25+
- Whitelist subdirs (=.npm-global=, =.local=, ...) ARE 501:20 — so
26+
=setup_nonroot_user= ran and its chowns worked. Only the home dir itself
27+
was left behind.
28+
- Working siblings show =/home/deva= at 501:20. Same entrypoint bytes
29+
(diffed baked scripts), same shadow-utils version, same home mounts.
30+
31+
Mechanism: =usermod -u= implicitly chowns the home tree, and shadow's
32+
=chown_tree= recurses first, chowning the top-level dir LAST. The walk
33+
crosses live host mounts inside home (=~/.claude= and friends, actively
34+
written by concurrent sessions). Any transient mid-walk error aborts the
35+
tree chown — usermod exits 12, passwd IS already updated — and the top
36+
dir never gets chowned. Our code suppresses stderr and only reacts when
37+
the UID itself failed to change. Reproduced the exact broken state by
38+
recreating a container with the same home-mount set; one-shot runs with
39+
idle mounts come out clean, which is why this stayed latent for months.
40+
41+
There is a second, benign variant: usermod fails outright (rc != 0, UID
42+
unchanged). The entrypoint adapts =DEVA_UID= to reality and everything
43+
stays self-consistent at 1001 — launches work. Only the half-success
44+
(passwd updated, chown aborted) bricks the container.
45+
46+
** Decision
47+
Add one explicit non-recursive chown of =$DEVA_HOME= itself after the
48+
usermod block, before the whitelist loop. It runs with the (possibly
49+
adapted) =DEVA_UID=, so both usermod outcomes stay consistent.
50+
51+
Rejected:
52+
- Restoring =chown -R "$DEVA_HOME"= (pre-=5807889= behavior): recursive
53+
chown across host mounts is the thing we deliberately stopped doing.
54+
- Adding =$DEVA_HOME= to the whitelist loop: the loop is =chown -R= per
55+
entry; home needs exactly depth-0.
56+
- Replacing usermod with passwd-file editing + targeted chowns: right
57+
long-term answer (usermod's implicit tree walk over host mounts is both
58+
the race and a hazard), but a bigger change than this fix needs.
59+
60+
** Tradeoff
61+
usermod still walks the whole home tree on every UID remap — wasted IO
62+
and a lingering hazard of chowning through bind mounts. The fix makes the
63+
outcome correct, not the mechanism. If launches ever get slow on big
64+
mounted homes, kill the implicit walk (=usermod= → passwd edit) instead
65+
of tuning around it.
66+
67+
** Next
68+
The fault-injection trick (stub =usermod= that updates passwd, skips the
69+
chown, exits 12) is exactly a regression test waiting for a harness —
70+
tests/ has no entrypoint coverage at all, which is why two "Permission
71+
denied" regressions in this same function (=5807889=, now this) shipped.
72+
73+
** Artifacts
74+
- =docker-entrypoint.sh=: explicit =chown "$DEVA_UID:$DEVA_GID" "$DEVA_HOME"=
75+
in =setup_nonroot_user=.
76+
- Verified: patched entrypoint + stubbed usermod (passwd updated, chown
77+
skipped, rc=12) → home 501:20, claude launches. Unpatched + same stub →
78+
reproduces the failure.
79+
- Bricked-container first aid: =docker exec <c> chown <uid>:<gid> /home/deva=.

0 commit comments

Comments
 (0)