diff --git a/app/src-tauri/src/docker/container.rs b/app/src-tauri/src/docker/container.rs index 2b15de6..b8d47bb 100644 --- a/app/src-tauri/src/docker/container.rs +++ b/app/src-tauri/src/docker/container.rs @@ -49,6 +49,21 @@ pub async fn create_container( let mut env_vars: Vec = Vec::new(); + // Pass host UID/GID so the entrypoint can remap the container user + #[cfg(unix)] + { + let uid = std::process::Command::new("id").arg("-u").output(); + let gid = std::process::Command::new("id").arg("-g").output(); + if let Ok(out) = uid { + let val = String::from_utf8_lossy(&out.stdout).trim().to_string(); + env_vars.push(format!("HOST_UID={}", val)); + } + if let Ok(out) = gid { + let val = String::from_utf8_lossy(&out.stdout).trim().to_string(); + env_vars.push(format!("HOST_GID={}", val)); + } + } + if let Some(key) = api_key { env_vars.push(format!("ANTHROPIC_API_KEY={}", key)); } @@ -82,10 +97,10 @@ pub async fn create_container( }, ]; - // SSH keys mount (read-only) + // SSH keys mount (read-only staging; entrypoint copies to ~/.ssh with correct perms) if let Some(ref ssh_path) = project.ssh_key_path { mounts.push(Mount { - target: Some("/home/claude/.ssh".to_string()), + target: Some("/tmp/.host-ssh".to_string()), source: Some(ssh_path.clone()), typ: Some(MountTypeEnum::BIND), read_only: Some(true), diff --git a/app/src-tauri/src/docker/exec.rs b/app/src-tauri/src/docker/exec.rs index fa35f91..cf34af4 100644 --- a/app/src-tauri/src/docker/exec.rs +++ b/app/src-tauri/src/docker/exec.rs @@ -72,6 +72,7 @@ impl ExecSessionManager { attach_stderr: Some(true), tty: Some(true), cmd: Some(cmd), + user: Some("claude".to_string()), working_dir: Some("/workspace".to_string()), ..Default::default() }, diff --git a/container/Dockerfile b/container/Dockerfile index e0431ec..ee0706d 100644 --- a/container/Dockerfile +++ b/container/Dockerfile @@ -88,7 +88,9 @@ RUN mkdir -p /home/claude/.claude /home/claude/.ssh WORKDIR /workspace -COPY --chown=claude:claude entrypoint.sh /home/claude/entrypoint.sh -RUN chmod +x /home/claude/entrypoint.sh +# ── Switch back to root for entrypoint (handles UID/GID remapping) ───────── +USER root +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh -ENTRYPOINT ["/home/claude/entrypoint.sh"] +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/container/entrypoint.sh b/container/entrypoint.sh index fcf652a..de2321c 100644 --- a/container/entrypoint.sh +++ b/container/entrypoint.sh @@ -1,49 +1,75 @@ #!/bin/bash set -e -# ── SSH key permissions ────────────────────────────────────────────────────── -# If SSH keys were mounted, fix permissions (bind mounts may have wrong perms) -if [ -d /home/claude/.ssh ]; then +# ── UID/GID remapping ────────────────────────────────────────────────────── +# Match the container's claude user to the host user's UID/GID so that +# bind-mounted files (project dir, docker socket) have correct ownership. +if [ -n "$HOST_UID" ] && [ "$HOST_UID" != "$(id -u claude)" ]; then + usermod -u "$HOST_UID" claude +fi +if [ -n "$HOST_GID" ] && [ "$HOST_GID" != "$(id -g claude)" ]; then + groupmod -g "$HOST_GID" claude +fi + +# Fix ownership of home directory after UID/GID change +chown -R claude:claude /home/claude + +# ── SSH key setup ────────────────────────────────────────────────────────── +# Host SSH dir is mounted read-only at /tmp/.host-ssh. +# Copy to /home/claude/.ssh so we can fix permissions. +if [ -d /tmp/.host-ssh ]; then + rm -rf /home/claude/.ssh + cp -a /tmp/.host-ssh /home/claude/.ssh + chown -R claude:claude /home/claude/.ssh chmod 700 /home/claude/.ssh find /home/claude/.ssh -type f -name "id_*" ! -name "*.pub" -exec chmod 600 {} \; find /home/claude/.ssh -type f -name "*.pub" -exec chmod 644 {} \; - # Write known_hosts fresh (not append) to avoid duplicates across restarts - ssh-keyscan -t ed25519,rsa github.com gitlab.com bitbucket.org > /home/claude/.ssh/known_hosts 2>/dev/null || true - chmod 644 /home/claude/.ssh/known_hosts + if [ -f /home/claude/.ssh/known_hosts ]; then + chmod 644 /home/claude/.ssh/known_hosts + fi + if [ -f /home/claude/.ssh/config ]; then + chmod 600 /home/claude/.ssh/config + fi fi +# Append common host keys (avoid duplicates) +su -s /bin/bash claude -c ' + mkdir -p /home/claude/.ssh + ssh-keyscan -t ed25519,rsa github.com gitlab.com bitbucket.org >> /home/claude/.ssh/known_hosts 2>/dev/null || true + sort -u -o /home/claude/.ssh/known_hosts /home/claude/.ssh/known_hosts +' + # ── Git credential helper (for HTTPS token) ───────────────────────────────── if [ -n "$GIT_TOKEN" ]; then - # Use git credential-store with a protected file instead of embedding in config CRED_FILE="/home/claude/.git-credentials" : > "$CRED_FILE" chmod 600 "$CRED_FILE" + chown claude:claude "$CRED_FILE" echo "https://oauth2:${GIT_TOKEN}@github.com" >> "$CRED_FILE" echo "https://oauth2:${GIT_TOKEN}@gitlab.com" >> "$CRED_FILE" echo "https://oauth2:${GIT_TOKEN}@bitbucket.org" >> "$CRED_FILE" - git config --global credential.helper "store --file=$CRED_FILE" - # Clear the env var so it's not visible in /proc/*/environ + su -s /bin/bash claude -c "git config --global credential.helper 'store --file=$CRED_FILE'" unset GIT_TOKEN fi # ── Git user config ────────────────────────────────────────────────────────── if [ -n "$GIT_USER_NAME" ]; then - git config --global user.name "$GIT_USER_NAME" + su -s /bin/bash claude -c "git config --global user.name '$GIT_USER_NAME'" fi if [ -n "$GIT_USER_EMAIL" ]; then - git config --global user.email "$GIT_USER_EMAIL" + su -s /bin/bash claude -c "git config --global user.email '$GIT_USER_EMAIL'" fi # ── Docker socket permissions ──────────────────────────────────────────────── if [ -S /var/run/docker.sock ]; then DOCKER_GID=$(stat -c '%g' /var/run/docker.sock) if ! getent group "$DOCKER_GID" > /dev/null 2>&1; then - sudo groupadd -g "$DOCKER_GID" docker-host + groupadd -g "$DOCKER_GID" docker-host fi DOCKER_GROUP=$(getent group "$DOCKER_GID" | cut -d: -f1) - sudo usermod -aG "$DOCKER_GROUP" claude + usermod -aG "$DOCKER_GROUP" claude fi -# ── Stay alive ─────────────────────────────────────────────────────────────── +# ── Stay alive as claude ───────────────────────────────────────────────────── echo "Triple-C container ready." -exec sleep infinity +exec su -s /bin/bash claude -c "exec sleep infinity"