From ea23cf1a504c3bffa839d7142e0b69cf7e4e245e Mon Sep 17 00:00:00 2001 From: Cristian Gora Date: Tue, 27 Oct 2026 01:02:14 +0100 Subject: [PATCH] Initial commit --- .claudio | 26 ++++++++++++++++++++ Containerfile | 24 ++++++++++++++++++ LICENSE | 24 ++++++++++++++++++ README.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++ claudio.sh | 61 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 203 insertions(+) create mode 100644 .claudio create mode 100644 Containerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100755 claudio.sh diff --git a/.claudio b/.claudio new file mode 100644 index 0000000..9c52d82 --- /dev/null +++ b/.claudio @@ -0,0 +1,26 @@ +# .claudio — project settings for claudio. +# Sourced as bash from the project directory. Everything here is optional; +# the project directory itself is always mounted at its own path. + +# Extra mounts, passed to `podman run -v`: +# "/host/path" same path inside the container, read-write +# "/host/path:/container/path" different path +# "/host/path:/host/path:ro" same path, read-only +MOUNTS=( + # "$HOME/.gitconfig:/home/claude/.gitconfig:ro" + # "$HOME/shared-libs:$HOME/shared-libs:ro" + # "$PWD/../sibling-project" +) + +# Extra `podman run` options (env vars, ports, resources, ...). +PODMAN_ARGS=( + # -e GITHUB_TOKEN + # -p 3000:3000 + # --memory 4g +) + +# Default arguments for claude itself. +CLAUDE_ARGS=( + # --dangerously-skip-permissions + # --model opus +) diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..ce7de2e --- /dev/null +++ b/Containerfile @@ -0,0 +1,24 @@ +# claudio — minimal image for running Claude Code in isolation. +# Rebuild with `claudio update` to get the latest base packages and Claude Code. +FROM docker.io/library/debian:stable-slim + +# Base tools Claude Code relies on. Add project-wide tools here (python3, nodejs, ...). +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates curl git ripgrep less procps openssh-client \ + && rm -rf /var/lib/apt/lists/* \ + && useradd -m -u 1000 -s /bin/bash claude + +# Native Claude Code binary, installed system-wide (not in $HOME) so that image +# rebuilds update it even though /home/claude is a persistent volume. +RUN curl -fsSL https://claude.ai/install.sh | HOME=/tmp/cc bash \ + && cp -L /tmp/cc/.local/bin/claude /usr/local/bin/claude \ + && rm -rf /tmp/cc + +# Updates come from rebuilding the image, not from in-container self-updates. +ENV DISABLE_AUTOUPDATER=1 \ + PATH=/home/claude/.local/bin:$PATH + +USER claude +WORKDIR /home/claude +ENTRYPOINT ["claude"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..efb9808 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/README.md b/README.md new file mode 100644 index 0000000..6e974b0 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# claudio + +Run Claude Code isolated in a podman container, one command per project. + +## Files + +- `Containerfile`: the image (Debian slim + Claude Code). +- `claudio.sh`: builds, updates and runs the container. +- `.claudio`: template for per-project settings (plain bash). + +## Install + +```sh +mkdir -p ~/claudio && cp Containerfile claudio.sh ~/claudio/ +chmod +x ~/claudio/claudio.sh +echo 'alias claudio=~/claudio/claudio.sh' >> ~/.bashrc +``` + +Requires `podman` (rootless is fine). + +## Usage + +```sh +cd ~/projects/myapp +claudio # start Claude Code here (builds the image on first run) +claudio --resume # any extra args are passed to claude +claudio shell # bash inside the container instead of claude +claudio build # build the image +claudio update # rebuild from scratch: latest base image + latest Claude Code +``` + +The current directory is mounted at the same path inside the container. +Nothing else from the host is visible unless you add it to a `.claudio` file. + +## Per-project settings + +Copy the template into a project and uncomment what you need: + +```sh +cp ~/claudio/.claudio ~/projects/myapp/ +``` + +```bash +MOUNTS=("$HOME/.gitconfig:/home/claude/.gitconfig:ro" "/data/shared") +PODMAN_ARGS=(-e GITHUB_TOKEN -p 3000:3000) +CLAUDE_ARGS=(--model opus) +``` + +- `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. + +## State + +The container's home (`/home/claude`) lives in the podman volume `claudio-home`. +This volume is shared by all projects and holds your login, settings and history. +The container itself is removed after every run. + +- Reset everything: `podman volume rm claudio-home` +- Need more tools in the image? Add them to the `apt-get install` line, then run `claudio update`. + +## Notes + +- Files written in mounts are owned by your host user (`--userns=keep-id`). +- SELinux labeling is disabled for the container, so mounts work without relabeling. +- claudio refuses to run from `$HOME` or `/`. diff --git a/claudio.sh b/claudio.sh new file mode 100755 index 0000000..61e4edb --- /dev/null +++ b/claudio.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# claudio — run Claude Code isolated in a podman container. +# +# Install: keep this file next to the Containerfile, then +# alias claudio=/path/to/claudio.sh (e.g. in ~/.bashrc) +# +# Usage: claudio [claude args...] run Claude Code in the current directory +# claudio shell [bash args] open a bash shell in the container instead +# claudio build build the image +# 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. +# The container's home (/home/claude: login, settings, history) lives in the +# global podman volume "claudio-home". +set -euo pipefail + +IMAGE=localhost/claudio:latest +VOLUME=claudio-home +DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" + +build() { podman build -t "$IMAGE" "$@" "$DIR"; } + +case "${1:-}" in + build) build; exit ;; + update) build --pull=always --no-cache && podman image prune -f; exit ;; +esac + +if [[ "$PWD" == "$HOME" || "$PWD" == / ]]; then + echo "claudio: refusing to mount $PWD — run it from a project directory." >&2 + exit 1 +fi + +podman image exists "$IMAGE" || build + +# Project settings (see the .claudio template). +MOUNTS=() PODMAN_ARGS=() CLAUDE_ARGS=() +if [[ -f .claudio ]]; then source ./.claudio; fi + +args=(-v "$PWD:$PWD" -w "$PWD" -v "$VOLUME:/home/claude") +for m in "${MOUNTS[@]}"; do + [[ "$m" == *:* ]] || m="$m:$m" # bare path -> same path inside the container + args+=(-v "$m") +done + +if [[ "${1:-}" == shell ]]; then + shift + args+=(--entrypoint bash) + CLAUDE_ARGS=() +fi + +if [[ -t 0 && -t 1 ]]; then args+=(-it); else args+=(-i); fi + +# keep-id maps your host user to the container's uid 1000, so files written +# in mounts stay owned by you. label=disable avoids SELinux relabeling of mounts. +exec podman run --rm "${args[@]}" \ + --userns=keep-id:uid=1000,gid=1000 \ + --security-opt label=disable \ + --hostname claudio \ + "${PODMAN_ARGS[@]}" \ + "$IMAGE" "${CLAUDE_ARGS[@]}" "$@"