v_2-0_local-projects #1

Open
justin wants to merge 7 commits from v_2-0_local-projects into main
22 changed files with 601 additions and 214 deletions
Showing only changes of commit d2c2139d87 - Show all commits

View File

@ -37,9 +37,18 @@ RUN ARCH=$(uname -m) && \
rm -rf /tmp/gs-install
# --- the user Claude runs as ---
# We create this user so the home folder is owned by the same ID the launcher
# runs as. Without it, the container couldn't write to its own home.
RUN useradd -m -s /bin/bash -u 1001 coder
# We create 'coder' with the *host* user's UID/GID (passed in by the launcher).
# This is what makes the bind-mounted project at /code writable: the files there
# keep the host's ownership, so the container can only write them if it runs as
# that same ID. It also means files Claude creates come out owned by you on the
# host — not root or some stray ID. The launcher rebuilds this image if the host
# ID ever changes (it reads the labels below to notice), so the default here is
# just a placeholder for a from-scratch build.
ARG HOST_UID=1000
ARG HOST_GID=1000
RUN if ! getent group "${HOST_GID}" >/dev/null; then groupadd -g "${HOST_GID}" coder; fi && \
useradd -m -s /bin/bash -u "${HOST_UID}" -g "${HOST_GID}" coder
LABEL safeclaude.uid="${HOST_UID}" safeclaude.gid="${HOST_GID}"
# Claude installs itself into one of these folders, so add them to PATH.
ENV PATH="/home/coder/.local/bin:/home/coder/.claude/bin:$PATH"

View File

@ -65,10 +65,25 @@ context_hash() {
} | sha256sum | cut -c1-12
}
# Build the shared base image, baking in the host user's UID/GID so the container
# can write the bind-mounted project (see Dockerfile.base). We rebuild if the
# image is missing, or if its baked-in IDs don't match the current host user —
# e.g. a different user runs safeclaude, or the repo moved to another machine.
ensure_base() {
docker image inspect "$BASE_IMAGE" &>/dev/null && return 0
echo "[safeclaude] building base image $BASE_IMAGE ..." >&2
docker build -t "$BASE_IMAGE" -f "$SCRIPT_DIR/Dockerfile.base" "$SCRIPT_DIR" >&2
local want_uid want_gid
want_uid="$(id -u)"; want_gid="$(id -g)"
if docker image inspect "$BASE_IMAGE" &>/dev/null; then
local have_uid have_gid
have_uid="$(docker image inspect --format '{{index .Config.Labels "safeclaude.uid"}}' "$BASE_IMAGE" 2>/dev/null || true)"
have_gid="$(docker image inspect --format '{{index .Config.Labels "safeclaude.gid"}}' "$BASE_IMAGE" 2>/dev/null || true)"
[ "$have_uid" = "$want_uid" ] && [ "$have_gid" = "$want_gid" ] && return 0
echo "[safeclaude] base image user ${have_uid:-?}:${have_gid:-?} != host $want_uid:$want_gid — rebuilding ..." >&2
else
echo "[safeclaude] building base image $BASE_IMAGE ..." >&2
fi
docker build -t "$BASE_IMAGE" \
--build-arg HOST_UID="$want_uid" --build-arg HOST_GID="$want_gid" \
-f "$SCRIPT_DIR/Dockerfile.base" "$SCRIPT_DIR" >&2
}
# Build the project's container if we haven't already (force=1 rebuilds it
@ -110,8 +125,11 @@ cmd_run() {
- To add setup that runs at container startup: ask the developer to add a script to .safeclaude/hooks/.
- /home/coder persists between runs. Edits under /code are real and shared with the host. Everything else is discarded when the container exits."
# Run as the host user so writes to the mounted /code (and its cache/) land with
# the right ownership. The base image creates this exact UID/GID as 'coder', so
# HOME and the home volume resolve correctly. Still non-root, still locked down.
exec docker run --rm "${tty_args[@]}" \
--user 1001:1001 \
--user "$(id -u):$(id -g)" \
--cap-drop ALL \
--security-opt no-new-privileges:true \
--add-host host.docker.internal:host-gateway \