diff --git a/.claudio b/.claudio index 9c52d82..11a7d06 100644 --- a/.claudio +++ b/.claudio @@ -12,6 +12,13 @@ MOUNTS=( # "$PWD/../sibling-project" ) +# GUI=1: graphical apps show up on your desktop (host Wayland socket + GPU). +# On KDE Plasma this also lets them control input, the screen and clipboard. +# AUDIO=1: apps can play sound through the host's PulseAudio/PipeWire socket. +# This also lets them record from your microphone. +GUI=0 +AUDIO=0 + # Extra `podman run` options (env vars, ports, resources, ...). PODMAN_ARGS=( # -e GITHUB_TOKEN diff --git a/Containerfile b/Containerfile index ce7de2e..70f7aa0 100644 --- a/Containerfile +++ b/Containerfile @@ -3,11 +3,15 @@ FROM docker.io/library/debian:stable-slim # Base tools Claude Code relies on. Add project-wide tools here (python3, nodejs, ...). +# The second line is for GUI=1 / AUDIO=1: OpenGL drivers, a font, PulseAudio client. RUN apt-get update \ && apt-get install -y --no-install-recommends \ ca-certificates curl git ripgrep less procps openssh-client \ + libgl1-mesa-dri fonts-dejavu-core libpulse0 libasound2-plugins \ && rm -rf /var/lib/apt/lists/* \ - && useradd -m -u 1000 -s /bin/bash claude + && useradd -m -u 1000 -s /bin/bash claude \ + && install -d -m 700 -o claude -g claude /run/user/1000 \ + && printf 'pcm.!default pulse\nctl.!default pulse\n' > /etc/asound.conf # Native Claude Code binary, installed system-wide (not in $HOME) so that image # rebuilds update it even though /home/claude is a persistent volume. @@ -17,7 +21,8 @@ RUN curl -fsSL https://claude.ai/install.sh | HOME=/tmp/cc bash \ # Updates come from rebuilding the image, not from in-container self-updates. ENV DISABLE_AUTOUPDATER=1 \ - PATH=/home/claude/.local/bin:$PATH + PATH=/home/claude/.local/bin:$PATH \ + XDG_RUNTIME_DIR=/run/user/1000 USER claude WORKDIR /home/claude diff --git a/README.md b/README.md index 6e974b0..4051044 100644 --- a/README.md +++ b/README.md @@ -41,17 +41,38 @@ cp ~/claudio/.claudio ~/projects/myapp/ ``` ```bash +GUI=1 +AUDIO=1 MOUNTS=("$HOME/.gitconfig:/home/claude/.gitconfig:ro" "/data/shared") PODMAN_ARGS=(-e GITHUB_TOKEN -p 3000:3000) CLAUDE_ARGS=(--model opus) ``` +- `GUI=1`: graphical apps open on your desktop (see below). +- `AUDIO=1`: apps can play sound (see below). - `MOUNTS`: extra `-v` mounts. A bare path is mounted at the same path. Add `:ro` for read-only. - `PODMAN_ARGS`: extra `podman run` options. - `CLAUDE_ARGS`: default arguments for `claude`. `.claudio` is sourced as bash, so only use ones you trust. +## Graphical apps and sound + +Both are off by default and work by sharing a single socket from your session. + +- `GUI=1` shares your Wayland socket and the GPU (`/dev/dri`), and makes + GTK, Qt, SDL and Electron apps use Wayland. It requires a Wayland session + (there is no X11 support). How isolated the container is depends on your + compositor: on KDE Plasma it **can type and click into other apps, record + the screen, read the clipboard and control all windows**, because KWin + offers those interfaces to every app. +- `AUDIO=1` shares the PulseAudio socket (`pulse/native`, also provided by + PipeWire). It works both ways: anything in the container can also **record + from your microphone**. + +The image only ships OpenGL drivers, a font and the PulseAudio client. Add the +apps you want to run (and their libraries) to the `apt-get install` line. + ## State The container's home (`/home/claude`) lives in the podman volume `claudio-home`. diff --git a/claudio.sh b/claudio.sh index 61e4edb..0a2b2df 100755 --- a/claudio.sh +++ b/claudio.sh @@ -10,7 +10,8 @@ # claudio update rebuild from scratch (latest Debian + Claude Code) # # The current directory is mounted at its own path. If it contains a .claudio -# file, that file is sourced (bash) for extra mounts and options. +# file, that file is sourced (bash) for extra mounts and options, and can set +# GUI=1 / AUDIO=1 to share the host's Wayland / PulseAudio sockets. # The container's home (/home/claude: login, settings, history) lives in the # global podman volume "claudio-home". set -euo pipefail @@ -34,7 +35,7 @@ fi podman image exists "$IMAGE" || build # Project settings (see the .claudio template). -MOUNTS=() PODMAN_ARGS=() CLAUDE_ARGS=() +MOUNTS=() PODMAN_ARGS=() CLAUDE_ARGS=() GUI=0 AUDIO=0 if [[ -f .claudio ]]; then source ./.claudio; fi args=(-v "$PWD:$PWD" -w "$PWD" -v "$VOLUME:/home/claude") @@ -43,6 +44,26 @@ for m in "${MOUNTS[@]}"; do args+=(-v "$m") done +# GUI/AUDIO: share a socket from the host's runtime dir into the container's. +host_run="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" +share() { + [[ -S "$host_run/$1" ]] || { echo "claudio: no socket at $host_run/$1" >&2; exit 1; } + args+=(-v "$host_run/$1:/run/user/1000/$1") +} + +if [[ "$GUI" == 1 ]]; then + wl="${WAYLAND_DISPLAY:-wayland-0}" + share "$wl" + args+=(-e WAYLAND_DISPLAY="$wl" -e GDK_BACKEND=wayland -e QT_QPA_PLATFORM=wayland + -e SDL_VIDEODRIVER=wayland -e ELECTRON_OZONE_PLATFORM_HINT=wayland) + if [[ -e /dev/dri ]]; then args+=(--device /dev/dri); fi +fi + +if [[ "$AUDIO" == 1 ]]; then + share pulse/native + args+=(-e PULSE_SERVER=unix:/run/user/1000/pulse/native) +fi + if [[ "${1:-}" == shell ]]; then shift args+=(--entrypoint bash)