diff --git a/CLAUDE.md b/CLAUDE.md index 65a4562..63d9fe7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -408,7 +408,7 @@ Inside the debug shell, you can run diagnostics manually: - `.env.example` - Example environment variables for MCP servers - `.env` - Your local environment variables (create from .env.example) - `.dockerignore` - Files excluded from Docker build context -- `install.sh` - One-line installer (`curl … | bash`): pulls the GHCR image, stores the OAuth token in `~/.config/claude-standalone/claude.env` (chmod 600), and installs a `claude-box` launcher into `~/.local/bin` (the hardened `docker run` wrapped as an executable; supports `--uninstall` and a non-interactive path via `CLAUDE_CODE_OAUTH_TOKEN`) +- `install.sh` - One-line installer (`curl … | bash`): pulls the GHCR image, stores the OAuth token in `~/.config/claude-standalone/claude.env` (chmod 600), and installs a `claude-box` launcher into `~/.local/bin` (the hardened `docker run` wrapped as an executable; supports `--uninstall` and a non-interactive path via `CLAUDE_CODE_OAUTH_TOKEN`). Also detects host `~/.claude/{agents,commands,skills}` and offers to pass them through — **mount** the live path (default), **copy** a snapshot to `~/.config/claude-standalone/resources/`, or **skip** (override non-interactively with `CLAUDE_RESOURCES_MODE`); the choice is written to `resources.conf`, `claude-box` mounts the paths read-only at `/host-claude/*`, and the entrypoint merges them OVER the baked state (host wins on collision; baked `opsx`/openspec skills survive) - `run_claude.sh` - Main entry point for running Claude Code (autonomous agent) - `run_acp.sh` - ACP entry point for IDE use (Zed); launched BY the editor over stdio - `.devcontainer/devcontainer.json` - Dev Container definition (interactive dev inside the image) diff --git a/Dockerfile b/Dockerfile index ab1b738..221383a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -273,6 +273,16 @@ if [ "$HOME" != "/home/claude" ] && [ ! -e "$HOME/.claude.json" ]; then\n\ mkdir -p "$HOME"\n\ cp -a /home/claude/. "$HOME/" 2>/dev/null || true\n\ fi\n\ +# Merge host-provided resources mounted by the launcher at /host-claude/\n\ +# OVER the baked state. Unconditional and AFTER the baked copy (so a resumed HOME\n\ +# still receives them). Host files win on name collision; baked-only files such as\n\ +# commands/opsx and the openspec skills survive because cp merges, not replaces.\n\ +for d in agents commands skills; do\n\ + if [ -d "/host-claude/$d" ]; then\n\ + mkdir -p "$HOME/.claude/$d"\n\ + cp -a "/host-claude/$d/." "$HOME/.claude/$d/" 2>/dev/null || true\n\ + fi\n\ +done\n\ cd /workspace 2>/dev/null || cd "$HOME"\n\ EXTRA_ARGS=""\n\ mode_msg="permission mode: auto (default; falls back to default mode if auto is unavailable)"\n\ diff --git a/README.md b/README.md index 58caf0b..3dddb60 100644 --- a/README.md +++ b/README.md @@ -34,9 +34,17 @@ curl -fsSL https://raw.githubusercontent.com/highload-zone/claude-code-standalon less install.sh && bash install.sh CLAUDE_CODE_OAUTH_TOKEN=... bash install.sh # non-interactive (skips the token prompt) +CLAUDE_RESOURCES_MODE=mount bash install.sh # non-interactive resource choice: mount | copy | skip bash install.sh --uninstall # remove the launcher (config is left in place) ``` +**Your local agents, commands, and skills.** If the installer finds `~/.claude/agents`, +`~/.claude/commands`, or `~/.claude/skills` on the host, it offers to pass them through to the +container: **mount** the live path (default — edits on the host show up next run), **copy** a +snapshot into `~/.config/claude-standalone/resources/`, or **skip**. `claude-box` mounts the chosen +paths read-only and the container merges them **over** its baked state, so your resources win on a +name clash while the image's own commands/skills (e.g. `opsx`) still work. + `claude-box` forwards your host git identity (so commits are attributed to you) and, if you set `DEPLOY_KEY=/path/to/scoped_key`, mounts it read-only to enable `git push` (see [SECURITY.md](./SECURITY.md)). diff --git a/install.sh b/install.sh index b82114a..baa25fd 100755 --- a/install.sh +++ b/install.sh @@ -86,6 +86,58 @@ chmod 600 "$ENV_FILE" say "Token saved to $ENV_FILE (chmod 600)." say "Optional MCP keys: add 'CONTEXT7_API_KEY=...' / 'PERPLEXITY_API_KEY=...' lines to that file." +# ---------------------------------------------------------------------------- +# Host resources: detect ~/.claude/{agents,commands,skills} and pass them through +# (the launcher mounts them at /host-claude/*; the container entrypoint merges +# them OVER the baked state — host wins, baked-only files like opsx survive). +# Default is to mount the live path; copy takes a snapshot; skip opts out. +# ---------------------------------------------------------------------------- +RES_CONF="$CONFIG_DIR/resources.conf" +RES_SNAPSHOT="$CONFIG_DIR/resources" +HOST_CLAUDE="${CLAUDE_HOME:-$HOME/.claude}" +: > "$RES_CONF" + +detected="" +for d in agents commands skills; do + if [ -d "$HOST_CLAUDE/$d" ] && [ -n "$(ls -A "$HOST_CLAUDE/$d" 2>/dev/null)" ]; then + detected="$detected $d" + fi +done +detected="${detected# }" + +if [ -n "$detected" ]; then + say "" + say "Found local Claude resources in $HOST_CLAUDE: $detected" + mode="${CLAUDE_RESOURCES_MODE:-}" + if [ -z "$mode" ]; then + if [ -r /dev/tty ]; then + printf 'Pass them to the container? [M]ount live path (default) / [C]opy snapshot / [S]kip: ' >&2 + read -r ans < /dev/tty + case "$ans" in [Cc]*) mode="copy";; [Ss]*) mode="skip";; *) mode="mount";; esac + else + mode="mount" # non-interactive default + fi + fi + if [ "$mode" = "skip" ]; then + say "Skipping host resource passthrough." + else + rm -rf "$RES_SNAPSHOT" + for d in $detected; do + if [ "$mode" = "copy" ]; then + mkdir -p "$RES_SNAPSHOT/$d" + cp -a "$HOST_CLAUDE/$d/." "$RES_SNAPSHOT/$d/" 2>/dev/null || true + src="$RES_SNAPSHOT/$d" + else + src="$HOST_CLAUDE/$d" + fi + printf 'CLAUDE_RES_%s="%s"\n' "$(printf '%s' "$d" | tr 'a-z' 'A-Z')" "$src" >> "$RES_CONF" + done + say "Host resources ($mode): $detected — will be merged into the container's ~/.claude/ on launch." + fi +else + say "No local ~/.claude/{agents,commands,skills} detected — skipping resource passthrough." +fi + # ---------------------------------------------------------------------------- # Install the launcher (regenerated every run = upgrade path) # ---------------------------------------------------------------------------- @@ -105,6 +157,7 @@ set -euo pipefail IMAGE="${CLAUDE_IMAGE:-ghcr.io/highload-zone/claude-code-standalone:latest}" ENV_FILE="${CLAUDE_ENV_FILE:-${XDG_CONFIG_HOME:-$HOME/.config}/claude-standalone/claude.env}" +RES_CONF="${XDG_CONFIG_HOME:-$HOME/.config}/claude-standalone/resources.conf" # Footgun guards (not a defense against a hostile operator — see SECURITY.md). for a in "$@"; do @@ -135,6 +188,20 @@ else echo "claude-box: no env-file at $ENV_FILE — set CLAUDE_CODE_OAUTH_TOKEN or re-run install.sh." >&2 fi +# Host resources (agents/commands/skills), configured by install.sh. Mounted +# read-only at /host-claude/; the container entrypoint merges them over the +# baked state. Paths come from resources.conf (live path, or a copied snapshot). +if [ -f "$RES_CONF" ]; then + . "$RES_CONF" + for d in agents commands skills; do + var="CLAUDE_RES_$(printf '%s' "$d" | tr 'a-z' 'A-Z')" + eval "p=\${$var:-}" + if [ -n "$p" ] && [ -d "$p" ]; then + args+=( -v "$p:/host-claude/$d:ro" ) + fi + done +fi + # git commit identity from the host (so commits are attributed to you). gn="$(git config --get user.name 2>/dev/null || true)" ge="$(git config --get user.email 2>/dev/null || true)"