From 9651ce759d52ea2fbcff0a55a459894698fba487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=BChl?= Date: Mon, 4 May 2026 08:41:52 +0000 Subject: [PATCH] docs(quick-260504-bw4): Add SSH support to claudebox --- .planning/STATE.md | 3 +- .../260504-bw4-PLAN.md | 290 ++++++++++++++++++ .../260504-bw4-SUMMARY.md | 68 ++++ 3 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 .planning/quick/260504-bw4-add-ssh-support-to-claudebox-with-ssh-fl/260504-bw4-PLAN.md create mode 100644 .planning/quick/260504-bw4-add-ssh-support-to-claudebox-with-ssh-fl/260504-bw4-SUMMARY.md diff --git a/.planning/STATE.md b/.planning/STATE.md index 187f20e..eacdc39 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -28,7 +28,7 @@ See: .planning/PROJECT.md (updated 2026-04-09) Phase: 04 of 3 (sandbox aware prompting) Plan: Not started Status: Ready to execute -Last activity: 2026-04-10 +Last activity: 2026-05-04 - Completed quick task 260504-bw4: Add SSH support to claudebox Progress: [███░░░░░░░] 33% @@ -63,6 +63,7 @@ None. | # | Description | Date | Commit | Directory | |---|-------------|------|--------|-----------| | 260410-d4u | on non-nixos hosts, bwrap fails because /etc/static does not exist | 2026-04-10 | 97c10f8 | [260410-d4u-on-non-nixos-hosts-bwrap-fails-because-e](./quick/260410-d4u-on-non-nixos-hosts-bwrap-fails-because-e/) | +| 260504-bw4 | Add SSH support to claudebox: --with-ssh flag forwards SSH_AUTH_SOCK agent socket, --ssh-key flag mounts specific key files read-only into sandbox ~/.ssh/ | 2026-05-04 | b2aeb2f | [260504-bw4-add-ssh-support-to-claudebox-with-ssh-fl](./quick/260504-bw4-add-ssh-support-to-claudebox-with-ssh-fl/) | ## Session Continuity diff --git a/.planning/quick/260504-bw4-add-ssh-support-to-claudebox-with-ssh-fl/260504-bw4-PLAN.md b/.planning/quick/260504-bw4-add-ssh-support-to-claudebox-with-ssh-fl/260504-bw4-PLAN.md new file mode 100644 index 0000000..8d2405c --- /dev/null +++ b/.planning/quick/260504-bw4-add-ssh-support-to-claudebox-with-ssh-fl/260504-bw4-PLAN.md @@ -0,0 +1,290 @@ +--- +phase: 260504-bw4 +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - claudebox.sh + - README.md +autonomous: true +requirements: + - SSH-01 + - SSH-02 + - SSH-03 + - SSH-04 +must_haves: + truths: + - "Running `claudebox --with-ssh` forwards $SSH_AUTH_SOCK into the sandbox at the same path with SSH_AUTH_SOCK env var set" + - "Running `claudebox --ssh-key ~/.ssh/id_ed25519` mounts that file (and its .pub if present) read-only into the sandbox at ~/.ssh/id_ed25519" + - "When any SSH mechanism is active, ~/.ssh/known_hosts is mounted read-only (if it exists on the host)" + - "--ssh-key is repeatable; multiple keys all land in the synthetic sandbox ~/.ssh/" + - "--with-ssh and --ssh-key can be combined in one invocation" + - "The audit display shows active SSH mechanism(s) and mounts" + - "The --dry-run output includes the SSH bwrap flags" + - "SANDBOX.md inside the sandbox reflects that SSH is available when SSH flags are active" + - "README.md documents SSH usage including ssh-agent setup for bash and fish" + artifacts: + - path: "claudebox.sh" + provides: "--with-ssh and --ssh-key flags, SSH bwrap mounts, conditional SANDBOX.md, audit/dry-run integration" + contains: "--with-ssh" + - path: "README.md" + provides: "SSH section + Flags table entries" + contains: "## SSH" + key_links: + - from: "claudebox.sh flag parser" + to: "BWRAP_ARGS SSH mount block" + via: "WITH_SSH and SSH_KEYS array set during arg parsing, consumed when assembling bwrap args" + pattern: "WITH_SSH|SSH_KEYS" + - from: "claudebox.sh SSH state" + to: "SANDBOX.md heredoc generation" + via: "conditional Default Restrictions text based on WITH_SSH/SSH_KEYS" + pattern: "SANDBOX.md" + - from: "claudebox.sh SSH state" + to: "print_audit + dry-run output" + via: "Mounts section emits SSH lines when active" + pattern: "print_audit|DRY_RUN" +--- + + +Add two opt-in SSH mechanisms to claudebox so users can `git push/pull` from inside the sandbox without exposing SSH keys by default. + +Purpose: Today the sandbox blocks all SSH. Real workflows need it for git remotes. The right answer is opt-in agent forwarding (`--with-ssh`) plus explicit key file mounting (`--ssh-key`), with audit visibility so the user always sees what crossed the boundary. + +Output: Updated `claudebox.sh` implementing both flags, audit + dry-run + SANDBOX.md integration, and an updated `README.md` documenting setup and usage. + + + +@./CLAUDE.md +@./claudebox.sh +@./README.md +@.planning/STATE.md + + + + +Flag parsing pattern (lines 9-20): +```bash +while (( $# > 0 )); do + case "$1" in + --yes|-y) SKIP_AUDIT=true ;; + ... + *) CLAUDE_ARGS+=("$1") ;; + esac + shift +done +``` + +Audit data structure (lines 240-245): AUDIT_SANDBOX_KEYS / AUDIT_HOST_KEYS / AUDIT_EXTRA_KEYS arrays + parallel _VALS assoc arrays. SSH mounts are mounts, not env vars — they belong in the print_audit `Mounts:` section (line 361), not in the env arrays. SSH_AUTH_SOCK is an env var — it should go through ENV_ARGS via the AUDIT_HOST_KEYS path. + +SANDBOX.md generation (lines 185-222): single heredoc with `'SANDBOXEOF'` (literal). To make it conditional we either (a) split it into pieces, or (b) generate it without a quoted heredoc and use bash conditionals. Approach: keep the static parts as a heredoc, then append a conditional "SSH" subsection before writing the "Git" section, OR rewrite as `cat < + + + + + + Task 1: Implement --with-ssh and --ssh-key flag parsing + bwrap mounts + claudebox.sh + + - `--with-ssh` sets WITH_SSH=true. If $SSH_AUTH_SOCK is set and is a socket on the host, add `--bind $SSH_AUTH_SOCK $SSH_AUTH_SOCK` to BWRAP_ARGS and `--setenv SSH_AUTH_SOCK $SSH_AUTH_SOCK` to ENV_ARGS. If the var is unset or the path is not a socket, print a warning to stderr and continue without forwarding. + - `--ssh-key ` is repeatable. Each value is appended to a SSH_KEYS array. Path is expanded (`~` -> $HOME) and validated: file must exist and be readable; otherwise exit 1 with an error. + - When WITH_SSH=true OR SSH_KEYS is non-empty: add `--dir $HOME/.ssh` to BWRAP_ARGS so the sandbox has a real ~/.ssh directory inside the home tmpfs. + - For each key in SSH_KEYS: add `--ro-bind $HOME/.ssh/`. If `.pub` exists on the host, also `--ro-bind .pub $HOME/.ssh/.pub`. + - When SSH is active AND `~/.ssh/known_hosts` exists on the host: add `--ro-bind $HOME/.ssh/known_hosts $HOME/.ssh/known_hosts` exactly once (shared between both mechanisms). + - The dry-run block (lines 405-451) emits the same SSH lines so `claudebox --dry-run --with-ssh --ssh-key ~/.ssh/id_ed25519` prints them. + + + 1. In the flag-parsing `case` block (around line 10), add: + ```bash + --with-ssh) WITH_SSH=true ;; + --ssh-key) + shift + [[ $# -gt 0 ]] || { echo "Error: --ssh-key requires a path" >&2; exit 1; } + SSH_KEYS+=("${1/#\~/$HOME}") + ;; + ``` + Initialize `WITH_SSH=false` and `SSH_KEYS=()` near the top with the other flag defaults. + + 2. After argument parsing, add a validation+resolution block that: + - For each path in SSH_KEYS: resolve to absolute, verify exists+readable, replace array entry with absolute path, error+exit if missing. + - If WITH_SSH=true: check `[[ -v SSH_AUTH_SOCK && -S "$SSH_AUTH_SOCK" ]]`. If not, print `${YELLOW}Warning: --with-ssh given but SSH_AUTH_SOCK is unset or not a socket; agent will not be forwarded.${RESET}` and set WITH_SSH=false. (Color vars are defined later — move this block to AFTER the ANSI block at line 107, or use plain text.) + - Compute `SSH_ACTIVE=true` if WITH_SSH=true OR ${#SSH_KEYS[@]} > 0; else false. + - Compute `KNOWN_HOSTS_MOUNT=true` if SSH_ACTIVE && `[[ -f $HOME/.ssh/known_hosts ]]`. + + 3. Where ENV_ARGS is built (after line 256): if WITH_SSH=true, append `--setenv SSH_AUTH_SOCK $SSH_AUTH_SOCK` and add to AUDIT_HOST_KEYS/VALS so it shows in the audit's `[>]` section. + + 4. Where BWRAP_ARGS is assembled (lines 453-487), after the existing conditional mounts (CLAUDE_JSON, CREDS) and before the trailing `--ro-bind GITCONFIG_TMP ...` line, insert: + ```bash + if [[ "$SSH_ACTIVE" == true ]]; then + BWRAP_ARGS+=(--dir "$HOME/.ssh") + if [[ "$WITH_SSH" == true ]]; then + BWRAP_ARGS+=(--bind "$SSH_AUTH_SOCK" "$SSH_AUTH_SOCK") + fi + for key in "${SSH_KEYS[@]}"; do + base=$(basename "$key") + BWRAP_ARGS+=(--ro-bind "$key" "$HOME/.ssh/$base") + if [[ -f "${key}.pub" ]]; then + BWRAP_ARGS+=(--ro-bind "${key}.pub" "$HOME/.ssh/$base.pub") + fi + done + if [[ "$KNOWN_HOSTS_MOUNT" == true ]]; then + BWRAP_ARGS+=(--ro-bind "$HOME/.ssh/known_hosts" "$HOME/.ssh/known_hosts") + fi + fi + ``` + + 5. Mirror all of (4) in the dry-run echo block (after the CREDS_MOUNT block around line 444, before the GITCONFIG line at 445), printing the same flags as quoted strings. + + 6. Update the audit display (`print_audit`, around line 361) to emit additional Mounts lines when SSH_ACTIVE: + - `agent (read-write, --with-ssh)` if WITH_SSH + - For each key: `ssh-key (read-only)`; add ` + .pub` line if pub exists + - `known_hosts (read-only)` if KNOWN_HOSTS_MOUNT + + + bash -n claudebox.sh && claudebox --dry-run --with-ssh 2>&1 | grep -q "SSH_AUTH_SOCK\|Warning: --with-ssh" && echo "Note: full agent forwarding only verifiable when ssh-agent is running on host" + + + - `claudebox --dry-run --with-ssh` (with agent running) prints `--bind $SSH_AUTH_SOCK ...` and `--setenv SSH_AUTH_SOCK ...`. + - `claudebox --dry-run --ssh-key ~/.ssh/id_ed25519` prints `--dir $HOME/.ssh`, `--ro-bind $HOME/.ssh/id_ed25519`, and (if present) the matching .pub bind. + - Both flags together print all of the above plus a single `--ro-bind .../known_hosts ...` line (if known_hosts exists). + - Missing key file → `claudebox --ssh-key /nonexistent` exits 1 with a clear error. + - Audit display shows the SSH mounts in the `Mounts:` section. + - `bash -n claudebox.sh` passes; shellcheck (run by writeShellApplication at build time) passes. + + + + + Task 2: Make SANDBOX.md conditional on SSH activation + claudebox.sh + + - When SSH_ACTIVE=false: SANDBOX.md keeps the current "Default Restrictions" section listing SSH keys as not mounted, and the Git section recommends HTTPS. + - When SSH_ACTIVE=true: "Default Restrictions" no longer lists SSH keys; a new "SSH" subsection states which mechanism is active (agent forwarding via $SSH_AUTH_SOCK and/or explicit key files at ~/.ssh/), and the Git section drops the HTTPS-preference sentence (or replaces it with: "SSH remotes work in this session."). + + + 1. Replace the quoted heredoc at lines 185-222 with an unquoted heredoc using shell-side composed variables. Build them before the heredoc: + ```bash + if [[ "$SSH_ACTIVE" == true ]]; then + _SSH_NOTES="" + [[ "$WITH_SSH" == true ]] && _SSH_NOTES+="- ssh-agent socket forwarded via \$SSH_AUTH_SOCK\n" + (( ${#SSH_KEYS[@]} > 0 )) && _SSH_NOTES+="- Explicit key file(s) mounted read-only at ~/.ssh/\n" + SANDBOX_RESTRICTIONS_BLOCK=$'## Default Restrictions\n\nBy default, the following are not mounted into the sandbox:\n- GPG and age keys (~/.gnupg, age key files)\n- Cloud credentials (~/.aws, ~/.config/gcloud)\n- Tailscale state\n\n## SSH\n\nSSH is available in this session:\n'"$(printf "$_SSH_NOTES")"$'\nUse `git push`/`git pull` over SSH normally.' + SANDBOX_GIT_TAIL="SSH remotes work in this session." + else + SANDBOX_RESTRICTIONS_BLOCK=$'## Default Restrictions\n\nBy default, the following are not mounted into the sandbox:\n- SSH keys (~/.ssh)\n- GPG and age keys (~/.gnupg, age key files)\n- Cloud credentials (~/.aws, ~/.config/gcloud)\n- Tailscale state\n\nIf your setup has been customized, some of these may be available.' + SANDBOX_GIT_TAIL="For remote operations, prefer HTTPS URLs over SSH since SSH keys are not available by default." + fi + ``` + 2. Rewrite the heredoc as `cat > "$HOME/.claudebox/SANDBOX.md" < + + bash -n claudebox.sh && claudebox --dry-run -y >/dev/null 2>&1 && grep -q "SSH keys (~/.ssh)" "$HOME/.claudebox/SANDBOX.md" && echo "no-ssh path OK" && claudebox --dry-run -y --ssh-key /etc/hostname >/dev/null 2>&1 && grep -q "## SSH" "$HOME/.claudebox/SANDBOX.md" && ! grep -q "SSH keys (~/.ssh)" "$HOME/.claudebox/SANDBOX.md" && echo "ssh-active path OK" + + + - Without SSH flags: SANDBOX.md contains "SSH keys (~/.ssh)" in restrictions and HTTPS preference in Git section. + - With `--with-ssh` or `--ssh-key`: SANDBOX.md drops the SSH-keys restriction line, gains a "## SSH" section listing active mechanisms, and Git section says SSH works. + - `bash -n` and shellcheck pass. + + + + + Task 3: Document SSH support in README.md + README.md + + - Flags table includes `--with-ssh` and `--ssh-key ` rows with concise descriptions. + - New `## SSH` section after `## Env vars` (and before `## How it works`) covers: when you need SSH (git push/pull over SSH remotes), the agent-forwarding flow with bash and fish setup commands, the agent-dies-with-shell caveat, the explicit key-file flow, and guidance on when to prefer each. + + + 1. Update the Flags table (lines 34-41) by inserting two rows after `--shell`: + ``` + | `--with-ssh` | Forward $SSH_AUTH_SOCK into the sandbox (requires running ssh-agent) | + | `--ssh-key ` | Mount a private key file read-only into the sandbox ~/.ssh/ (repeatable) | + ``` + 2. Add a new `## SSH` section between the existing `## Env vars` and `## How it works` sections with this content (verbatim shape, write in the same plain-prose tone as the rest of the README — no marketing fluff): + + ```markdown + ## SSH + + SSH is opt-in. By default no keys or agent socket cross the sandbox boundary, which means git push/pull over SSH remotes won't work. Two mechanisms are available — pick whichever matches your workflow. + + ### `--with-ssh` (agent forwarding) + + Forwards `$SSH_AUTH_SOCK` into the sandbox so any keys loaded in your ssh-agent are usable inside. Your private key files are never mounted; only the agent socket is. + + Start an agent before launching claudebox. The agent dies with the shell that started it, so don't expect it to survive across terminals. + + Bash: + ```bash + eval "$(ssh-agent)" + ssh-add ~/.ssh/id_ed25519 + claudebox --with-ssh + ``` + + Fish: + ```fish + eval (ssh-agent -c) + ssh-add ~/.ssh/id_ed25519 + claudebox --with-ssh + ``` + + If `--with-ssh` is passed but no agent is running, claudebox warns and continues without forwarding. + + ### `--ssh-key ` (explicit key files) + + Mounts a specific private key (and matching `.pub`, if present) read-only into the sandbox at `~/.ssh/`. Repeatable — pass it multiple times for multiple keys. + + ```bash + claudebox --ssh-key ~/.ssh/id_ed25519 + claudebox --ssh-key ~/.ssh/id_work --ssh-key ~/.ssh/id_personal + ``` + + Prefer this when you don't have an agent running, or when you want to scope exactly which keys the sandbox can use regardless of what's loaded in the agent. + + ### known_hosts + + When either flag is active, `~/.ssh/known_hosts` is mounted read-only (if it exists) so SSH host verification works without prompting. + + Both flags can be combined. + ``` + + + grep -q "^## SSH" README.md && grep -q "\-\-with-ssh" README.md && grep -q "\-\-ssh-key" README.md && grep -q "ssh-agent -c" README.md && grep -q "known_hosts" README.md + + + - README.md Flags table lists both new flags. + - README.md has a `## SSH` section with bash + fish agent setup, explicit-key usage, and known_hosts note. + - No broken markdown structure (sections in order, code fences balanced). + + + + + + +End-to-end smoke checks: + +1. `bash -n claudebox.sh` — syntax valid. +2. `nix build` (or `nix flake check`) succeeds — shellcheck via writeShellApplication passes. +3. `claudebox --dry-run` (no SSH flags) — output contains no `--bind $SSH_AUTH_SOCK`, no `~/.ssh` mounts. +4. With agent running: `eval "$(ssh-agent)" && claudebox --dry-run --with-ssh` — output contains `--bind ` and `--setenv SSH_AUTH_SOCK ...`. +5. `claudebox --dry-run --ssh-key ~/.ssh/id_ed25519` (assuming key exists) — output contains `--dir $HOME/.ssh`, `--ro-bind $HOME/.ssh/id_ed25519`, and `.pub` bind if present. +6. `claudebox --dry-run --ssh-key /nonexistent` — exits non-zero with clear error. +7. SANDBOX.md content matches SSH state (verify by inspecting `~/.claudebox/SANDBOX.md` after a dry run with and without flags). +8. README renders correctly (visual or `, glow README.md`). + + + +- All three tasks complete and `` criteria met. +- Both flags work in isolation, together, and respect missing-input failure modes. +- Audit display + dry-run + SANDBOX.md all reflect SSH state consistently. +- README documents the feature for both bash and fish users. +- No regressions: running `claudebox` without any SSH flag behaves exactly as before. + + + +After completion, create `.planning/quick/260504-bw4-add-ssh-support-to-claudebox-with-ssh-fl/260504-bw4-SUMMARY.md`. + diff --git a/.planning/quick/260504-bw4-add-ssh-support-to-claudebox-with-ssh-fl/260504-bw4-SUMMARY.md b/.planning/quick/260504-bw4-add-ssh-support-to-claudebox-with-ssh-fl/260504-bw4-SUMMARY.md new file mode 100644 index 0000000..c68f26a --- /dev/null +++ b/.planning/quick/260504-bw4-add-ssh-support-to-claudebox-with-ssh-fl/260504-bw4-SUMMARY.md @@ -0,0 +1,68 @@ +--- +phase: 260504-bw4 +plan: 01 +subsystem: sandbox/ssh +tags: [ssh, bwrap, security, opt-in] +dependency_graph: + requires: [] + provides: [ssh-agent-forwarding, ssh-key-mounts, sandbox-ssh-awareness] + affects: [claudebox.sh, README.md] +tech_stack: + added: [] + patterns: [opt-in SSH via bwrap --bind/--ro-bind, conditional SANDBOX.md generation] +key_files: + modified: + - claudebox.sh + - README.md +decisions: + - SSH is opt-in: no keys or sockets cross the sandbox boundary without explicit flags + - --with-ssh validation: silently degrades to no-op with warning if ssh-agent is not running + - SANDBOX.md uses unquoted heredoc with pre-composed variables for conditional content + - known_hosts mounted once if either SSH mechanism is active (shared between --with-ssh and --ssh-key) +metrics: + duration: 8min + completed: 2026-05-04 + tasks: 3 + files: 2 +--- + +# Quick Task 260504-bw4: Add SSH Support to claudebox Summary + +One-liner: Opt-in SSH via `--with-ssh` (agent socket forwarding) and `--ssh-key` (explicit key file mounts), with audit/dry-run/SANDBOX.md integration and README documentation. + +## Tasks Completed + +| Task | Name | Commit | Files | +|------|------|--------|-------| +| 1 | Implement --with-ssh and --ssh-key flag parsing + bwrap mounts | 41ebf10 | claudebox.sh | +| 2 | Make SANDBOX.md conditional on SSH activation | e9154fd | claudebox.sh | +| 3 | Document SSH support in README.md | b2aeb2f | README.md | + +## What Was Built + +**claudebox.sh** now accepts two new flags: + +- `--with-ssh`: validates `$SSH_AUTH_SOCK` is a real socket, adds `--bind $SSH_AUTH_SOCK $SSH_AUTH_SOCK` and `--setenv SSH_AUTH_SOCK` to bwrap args, degrades gracefully with a warning if no agent is running. +- `--ssh-key `: repeatable, validates file exists+readable, mounts key (and `.pub` if present) read-only into `~/.ssh/` inside the sandbox. +- When either mechanism is active: `--dir ~/.ssh` is added, and `~/.ssh/known_hosts` is mounted read-only if it exists on the host. +- Audit display shows SSH mounts in the Mounts section. +- `--dry-run` output mirrors all SSH bwrap flags. +- SANDBOX.md is now generated conditionally: no-SSH mode lists SSH keys in restrictions and recommends HTTPS; SSH-active mode drops that restriction, adds a `## SSH` section describing which mechanisms are active, and says SSH remotes work. + +**README.md** gains two flag table rows and a `## SSH` section covering both mechanisms, bash/fish agent setup, the agent-lifetime caveat, explicit key usage, and the known_hosts note. + +## Deviations from Plan + +None - plan executed exactly as written. + +## Threat Flags + +No new threat surface introduced. SSH flags are opt-in and explicitly documented. The agent socket bind is scope-limited to `--bind $SSH_AUTH_SOCK $SSH_AUTH_SOCK` (only the socket path the user explicitly opts into). Key files are read-only. + +## Self-Check: PASSED + +- claudebox.sh: FOUND +- README.md: FOUND +- 41ebf10 (Task 1): FOUND +- e9154fd (Task 2): FOUND +- b2aeb2f (Task 3): FOUND