@@ -8,38 +8,68 @@ set -euo pipefail
88# We remap dev to that UID -- fast and seamless.
99#
1010# Rootless Docker: Workspace appears as root-owned (UID 0) inside the
11- # container due to user-namespace mapping. We can't remap
12- # dev to UID 0 (that's root), so we grant access with
13- # POSIX ACLs instead.
11+ # container due to user-namespace mapping. Requires
12+ # DEVCONTAINER_REMOTE_USER=root (set automatically by
13+ # ods dev up). Container root IS the host user, so
14+ # bind-mounts and named volumes are symlinked into /root.
1415
1516WORKSPACE=/workspace
1617TARGET_USER=dev
18+ REMOTE_USER=" ${SUDO_USER:- $TARGET_USER } "
1719
1820WS_UID=$( stat -c ' %u' " $WORKSPACE " )
1921WS_GID=$( stat -c ' %g' " $WORKSPACE " )
2022DEV_UID=$( id -u " $TARGET_USER " )
2123DEV_GID=$( id -g " $TARGET_USER " )
2224
23- DEV_HOME=/home/" $TARGET_USER "
25+ # devcontainer.json bind-mounts and named volumes target /home/dev regardless
26+ # of remoteUser. When running as root ($HOME=/root), Phase 1 bridges the gap
27+ # with symlinks from ACTIVE_HOME → MOUNT_HOME.
28+ MOUNT_HOME=/home/" $TARGET_USER "
2429
25- # Ensure directories that tools expect exist under ~dev.
26- # ~/.local and ~/.cache are named Docker volumes -- ensure they are owned by dev.
27- mkdir -p " $DEV_HOME " /.local/state " $DEV_HOME " /.local/share
28- chown -R " $TARGET_USER " :" $TARGET_USER " " $DEV_HOME " /.local
29- chown -R " $TARGET_USER " :" $TARGET_USER " " $DEV_HOME " /.cache
30+ if [ " $REMOTE_USER " = " root" ]; then
31+ ACTIVE_HOME=" /root"
32+ else
33+ ACTIVE_HOME=" $MOUNT_HOME "
34+ fi
35+
36+ # ── Phase 1: home directory setup ───────────────────────────────────
37+
38+ # ~/.local and ~/.cache are named Docker volumes mounted under MOUNT_HOME.
39+ mkdir -p " $MOUNT_HOME " /.local/state " $MOUNT_HOME " /.local/share
3040
31- # Copy host configs mounted as *.host into their real locations.
32- # This gives the dev user owned copies without touching host originals.
33- if [ -d " $DEV_HOME /.ssh.host" ]; then
34- cp -a " $DEV_HOME /.ssh.host" " $DEV_HOME /.ssh"
35- chmod 700 " $DEV_HOME /.ssh"
36- chmod 600 " $DEV_HOME " /.ssh/id_* 2> /dev/null || true
37- chown -R " $TARGET_USER " :" $TARGET_USER " " $DEV_HOME /.ssh"
41+ # When running as root, symlink bind-mounts and named volumes into /root
42+ # so that $HOME-relative tools (Claude Code, git, etc.) find them.
43+ if [ " $ACTIVE_HOME " != " $MOUNT_HOME " ]; then
44+ for item in .claude .cache .local; do
45+ [ -d " $MOUNT_HOME /$item " ] || continue
46+ if [ -e " $ACTIVE_HOME /$item " ] && [ ! -L " $ACTIVE_HOME /$item " ]; then
47+ echo " warning: replacing $ACTIVE_HOME /$item with symlink to $MOUNT_HOME /$item " >&2
48+ rm -rf " $ACTIVE_HOME /$item "
49+ fi
50+ ln -sfn " $MOUNT_HOME /$item " " $ACTIVE_HOME /$item "
51+ done
52+ # Symlink files (not directories).
53+ for file in .claude.json .gitconfig .zshrc.host; do
54+ [ -f " $MOUNT_HOME /$file " ] && ln -sf " $MOUNT_HOME /$file " " $ACTIVE_HOME /$file "
55+ done
56+
57+ # Nested mount: .config/nvim
58+ if [ -d " $MOUNT_HOME /.config/nvim" ]; then
59+ mkdir -p " $ACTIVE_HOME /.config"
60+ if [ -e " $ACTIVE_HOME /.config/nvim" ] && [ ! -L " $ACTIVE_HOME /.config/nvim" ]; then
61+ echo " warning: replacing $ACTIVE_HOME /.config/nvim with symlink" >&2
62+ rm -rf " $ACTIVE_HOME /.config/nvim"
63+ fi
64+ ln -sfn " $MOUNT_HOME /.config/nvim" " $ACTIVE_HOME /.config/nvim"
65+ fi
3866fi
39- if [ -d " $DEV_HOME /.config/nvim.host" ]; then
40- mkdir -p " $DEV_HOME /.config"
41- cp -a " $DEV_HOME /.config/nvim.host" " $DEV_HOME /.config/nvim"
42- chown -R " $TARGET_USER " :" $TARGET_USER " " $DEV_HOME /.config/nvim"
67+
68+ # ── Phase 2: workspace access ───────────────────────────────────────
69+
70+ # Root always has workspace access; Phase 1 handled home setup.
71+ if [ " $REMOTE_USER " = " root" ]; then
72+ exit 0
4373fi
4474
4575# Already matching -- nothing to do.
@@ -61,45 +91,17 @@ if [ "$WS_UID" != "0" ]; then
6191 echo " warning: failed to remap $TARGET_USER UID to $WS_UID " >&2
6292 fi
6393 fi
64- if ! chown -R " $TARGET_USER " :" $TARGET_USER " /home/ " $TARGET_USER " 2>&1 ; then
65- echo " warning: failed to chown /home/ $TARGET_USER " >&2
94+ if ! chown -R " $TARGET_USER " :" $TARGET_USER " " $MOUNT_HOME " 2>&1 ; then
95+ echo " warning: failed to chown $MOUNT_HOME " >&2
6696 fi
6797else
6898 # ── Rootless Docker ──────────────────────────────────────────────
69- # Workspace is root-owned inside the container. Grant dev access
70- # via POSIX ACLs (preserves ownership, works across the namespace
71- # boundary).
72- if command -v setfacl & > /dev/null; then
73- setfacl -Rm " u:${TARGET_USER} :rwX" " $WORKSPACE "
74- setfacl -Rdm " u:${TARGET_USER} :rwX" " $WORKSPACE " # default ACL for new files
75-
76- # Git refuses to operate in repos owned by a different UID.
77- # Host gitconfig is mounted readonly as ~/.gitconfig.host.
78- # Create a real ~/.gitconfig that includes it plus container overrides.
79- printf ' [include]\n\tpath = %s/.gitconfig.host\n[safe]\n\tdirectory = %s\n' \
80- " $DEV_HOME " " $WORKSPACE " > " $DEV_HOME /.gitconfig"
81- chown " $TARGET_USER " :" $TARGET_USER " " $DEV_HOME /.gitconfig"
82-
83- # If this is a worktree, the main .git dir is bind-mounted at its
84- # host absolute path. Grant dev access so git operations work.
85- GIT_COMMON_DIR=$( git -C " $WORKSPACE " rev-parse --git-common-dir 2> /dev/null || true)
86- if [ -n " $GIT_COMMON_DIR " ] && [ " $GIT_COMMON_DIR " != " $WORKSPACE /.git" ]; then
87- [ ! -d " $GIT_COMMON_DIR " ] && GIT_COMMON_DIR=" $WORKSPACE /$GIT_COMMON_DIR "
88- if [ -d " $GIT_COMMON_DIR " ]; then
89- setfacl -Rm " u:${TARGET_USER} :rwX" " $GIT_COMMON_DIR "
90- setfacl -Rdm " u:${TARGET_USER} :rwX" " $GIT_COMMON_DIR "
91- git config -f " $DEV_HOME /.gitconfig" --add safe.directory " $( dirname " $GIT_COMMON_DIR " ) "
92- fi
93- fi
94-
95- # Also fix bind-mounted dirs under ~dev that appear root-owned.
96- for dir in /home/" $TARGET_USER " /.claude; do
97- [ -d " $dir " ] && setfacl -Rm " u:${TARGET_USER} :rwX" " $dir " && setfacl -Rdm " u:${TARGET_USER} :rwX" " $dir "
98- done
99- [ -f /home/" $TARGET_USER " /.claude.json ] && \
100- setfacl -m " u:${TARGET_USER} :rw" /home/" $TARGET_USER " /.claude.json
101- else
102- echo " warning: setfacl not found; dev user may not have write access to workspace" >&2
103- echo " install the 'acl' package or set remoteUser to root" >&2
104- fi
99+ # Workspace is root-owned (UID 0) due to user-namespace mapping.
100+ # The supported path is remoteUser=root (set DEVCONTAINER_REMOTE_USER=root),
101+ # which is handled above. If we reach here, the user is running as dev
102+ # under rootless Docker without the override.
103+ echo " error: rootless Docker detected but remoteUser is not root." >&2
104+ echo " Set DEVCONTAINER_REMOTE_USER=root before starting the container," >&2
105+ echo " or use 'ods dev up' which sets it automatically." >&2
106+ exit 1
105107fi
0 commit comments