diff --git a/CLAUDE.md b/CLAUDE.md index 0b8c7f7..2cbb2b0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,7 +54,8 @@ The repo is at an early stage (no code yet). The layout below is the **intended* - **Pin versions.** Ghidra version, GhidraMCP release, JDK version, Claude Desktop build and RE MCP commit go in `.env` / build args, not `latest`. The GhidraMCP release must match the Ghidra version it was built for. - **Run as non-root** inside containers. Map the host UID/GID (via `.env`) so files in `workspace/` stay owned by the user. -- **Persist state in named volumes or `workspace/`**: Claude Desktop config/login (`~/.config/Claude`), Ghidra user settings and projects. Rebuilding an image must not lose sign-in or analysis. +- **Persist state in named volumes or `workspace/`**: Ghidra user settings and projects. Rebuilding an image must not lose analysis. +- **Claude Desktop sign-in is not persisted.** No keyring is installed and `~/.config/Claude` is not mounted, so the user logs in on every start. Don't add a keyring or config volume back without being asked. - **No secrets in the repo.** Credentials, session data and `workspace/` contents are git-ignored. Ship `.env.example`, never `.env`. - **Untrusted binaries.** Samples under analysis may be malicious. Never execute them on the host or in the Claude Desktop container. Containers that might run samples (dynamic analysis) should have no host mounts beyond `workspace/` and should allow network isolation (`network_mode: none` or an internal-only network). - **Linux only.** Target recent Docker Engine + Compose v2 (`docker compose`, not `docker-compose`). Make the setup work under both Docker and rootless Podman where practical. On SELinux hosts such as Fedora, bind mounts need `:z`/`:Z`. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..10e8d2e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,50 @@ +FROM ubuntu:24.04 + +ARG DEBIAN_FRONTEND=noninteractive +# Match the host user so files written to mounted volumes keep the right owner. +ARG USER_NAME=re +ARG USER_UID=1000 +ARG USER_GID=1000 + +# Fingerprint of Anthropic's apt signing key, from the official install docs: +# https://code.claude.com/docs/en/desktop-linux +ARG ANTHROPIC_APT_FPR=31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates curl gnupg \ + && rm -rf /var/lib/apt/lists/* + +# Add Anthropic's apt repository and refuse to continue if the key is wrong. +RUN curl -fsSLo /usr/share/keyrings/claude-desktop-archive-keyring.asc \ + https://downloads.claude.ai/claude-desktop/key.asc \ + && gpg --show-keys --with-colons /usr/share/keyrings/claude-desktop-archive-keyring.asc \ + | grep -q "^fpr:::::::::${ANTHROPIC_APT_FPR}:" \ + && echo "deb [arch=amd64,arm64 signed-by=/usr/share/keyrings/claude-desktop-archive-keyring.asc] https://downloads.claude.ai/claude-desktop/apt/stable stable main" \ + > /etc/apt/sources.list.d/claude-desktop.list + +# Recommends are skipped: they pull in QEMU/OVMF for Cowork, which needs KVM +# and isn't useful in this container. No keyring is installed: sign-in is not +# kept between runs. x11-utils provides the display check tools. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + claude-desktop \ + x11-utils \ + xauth \ + fonts-dejavu-core \ + && rm -rf /var/lib/apt/lists/* + +# Ubuntu 24.04 ships a default "ubuntu" user on UID 1000; replace it. +RUN if id ubuntu >/dev/null 2>&1; then userdel -r ubuntu; fi \ + && groupadd -g "${USER_GID}" "${USER_NAME}" \ + && useradd -m -u "${USER_UID}" -g "${USER_GID}" -s /bin/bash "${USER_NAME}" + +USER ${USER_NAME} +WORKDIR /home/${USER_NAME} + +# No keyring: tell Electron not to look for one. Session data lives only in +# the container's filesystem, which is discarded on exit (run with --rm), so +# the user signs in on every start. +# --no-sandbox: Docker's default seccomp profile blocks the namespaces +# Chromium's sandbox needs, so the zygote dies on start. The container's own +# seccomp/namespaces stay in place as the boundary to the host instead. +CMD ["claude-desktop", "--password-store=basic", "--no-sandbox"] diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..9b7aaef --- /dev/null +++ b/run.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# Start the Claude Desktop container, forwarding the host's X server. +# Before launching, verify that a window from the container can actually be +# drawn on the host display. +# +# Usage: ./run.sh [--build] [--check-only] +# --build rebuild the image even if it exists +# --check-only run the display check and exit without starting Claude + +set -euo pipefail + +IMAGE="${IMAGE:-claude-re-setup:latest}" +CONTAINER="${CONTAINER:-claude-desktop}" +RUNTIME="${RUNTIME:-docker}" + +build=0 +check_only=0 +for arg in "$@"; do + case "$arg" in + --build) build=1 ;; + --check-only) check_only=1 ;; + -h|--help) sed -n '2,9p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + *) echo "Unknown argument: $arg" >&2; exit 2 ;; + esac +done + +die() { echo "error: $*" >&2; exit 1; } +info() { echo "==> $*"; } + +command -v "$RUNTIME" >/dev/null || die "'$RUNTIME' not found (set RUNTIME=podman to use podman)" +command -v xauth >/dev/null || die "xauth not found on the host (dnf install xorg-x11-xauth)" + +# --- Display ----------------------------------------------------------------- +# On Wayland sessions this is XWayland, which GNOME/KDE start on demand. +[[ -n "${DISPLAY:-}" ]] || die "DISPLAY is not set; run this from a graphical session" + +display_num="${DISPLAY#*:}" +display_num="${display_num%%.*}" +[[ -S "/tmp/.X11-unix/X${display_num}" ]] \ + || die "X socket /tmp/.X11-unix/X${display_num} not found for DISPLAY=$DISPLAY" + +# Hand the container a copy of this display's cookie, rewritten to match any +# hostname (family ffff). This avoids opening the server up with 'xhost +'. +xauth_dir="${XDG_RUNTIME_DIR:-/tmp}/claude-re-setup" +xauth_file="$xauth_dir/Xauthority" +mkdir -p "$xauth_dir" +chmod 700 "$xauth_dir" +: > "$xauth_file" +xauth nlist "$DISPLAY" | sed -e 's/^..../ffff/' | xauth -f "$xauth_file" nmerge - \ + || die "could not export the X cookie for $DISPLAY" +[[ -s "$xauth_file" ]] || die "no X cookie found for $DISPLAY (is XAUTHORITY set?)" +# The container user has the same UID as us, so 0600 is enough. +chmod 600 "$xauth_file" + +# --- Image ------------------------------------------------------------------- +if (( build )) || ! "$RUNTIME" image inspect "$IMAGE" >/dev/null 2>&1; then + info "Building $IMAGE" + "$RUNTIME" build \ + --build-arg USER_UID="$(id -u)" \ + --build-arg USER_GID="$(id -g)" \ + -t "$IMAGE" "$(dirname "$(readlink -f "$0")")" +fi + +# label=disable: the X socket and cookie are host files that SELinux would +# otherwise block, and relabeling /tmp/.X11-unix with :z would break the host. +run_args=( + --rm + --security-opt label=disable + --shm-size 1g + -e DISPLAY=":${display_num}" + -e XAUTHORITY=/tmp/.Xauthority + -v /tmp/.X11-unix:/tmp/.X11-unix:ro + -v "$xauth_file:/tmp/.Xauthority:ro" +) + +# --- Display check ----------------------------------------------------------- +# 1. xdpyinfo proves the container can connect and authenticate to the server. +# 2. xmessage opens a real window; xwininfo confirms the server mapped it. +info "Checking that the container can draw on DISPLAY=$DISPLAY" +check_script=' +set -e +xdpyinfo >/dev/null 2>&1 || { echo "FAIL: cannot connect to X server at $DISPLAY"; exit 1; } +echo "ok: connected to X server ($(xdpyinfo | sed -n "s/^vendor string: *//p"))" + +title="claude-re-setup-x11-check" +xmessage -title "$title" -center -timeout 3 "Display check from the container: OK" & +for _ in $(seq 1 30); do + if xwininfo -name "$title" 2>/dev/null | grep -q "Map State: IsViewable"; then + echo "ok: test window is mapped and viewable" + wait || true + exit 0 + fi + sleep 0.1 +done +kill %1 2>/dev/null || true +echo "FAIL: test window was created but never became viewable" +exit 1 +' +if ! "$RUNTIME" run "${run_args[@]}" "$IMAGE" bash -c "$check_script"; then + die "display check failed; see the output above" +fi + +(( check_only )) && { info "Display check passed"; exit 0; } + +# --- Claude Desktop ---------------------------------------------------------- +if "$RUNTIME" container inspect "$CONTAINER" >/dev/null 2>&1; then + die "container '$CONTAINER' already exists; stop it with: $RUNTIME stop $CONTAINER" +fi + +# No config volume: the container is removed on stop (--rm), so the sign-in +# is discarded and Claude asks for a login on every start. +info "Starting Claude Desktop in container '$CONTAINER'" +"$RUNTIME" run -d "${run_args[@]}" \ + --name "$CONTAINER" \ + "$IMAGE" >/dev/null +info "Running. Logs: $RUNTIME logs -f $CONTAINER Stop: $RUNTIME stop $CONTAINER"