claudebox/.planning/quick/260504-bw4-add-ssh-support-to-claudebox-with-ssh-fl/260504-bw4-PLAN.md

16 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
260504-bw4 01 execute 1
claudebox.sh
README.md
true
SSH-01
SSH-02
SSH-03
SSH-04
truths artifacts key_links
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
path provides contains
claudebox.sh --with-ssh and --ssh-key flags, SSH bwrap mounts, conditional SANDBOX.md, audit/dry-run integration --with-ssh
path provides contains
README.md SSH section + Flags table entries ## SSH
from to via pattern
claudebox.sh flag parser BWRAP_ARGS SSH mount block WITH_SSH and SSH_KEYS array set during arg parsing, consumed when assembling bwrap args WITH_SSH|SSH_KEYS
from to via pattern
claudebox.sh SSH state SANDBOX.md heredoc generation conditional Default Restrictions text based on WITH_SSH/SSH_KEYS SANDBOX.md
from to via pattern
claudebox.sh SSH state print_audit + dry-run output Mounts section emits SSH lines when active 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):

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 <<SANDBOXEOF (unquoted) with a ${SSH_RESTRICTIONS_NOTE} placeholder. Prefer the placeholder approach for readability.

Dry-run output (lines 405-451): mirrors BWRAP_ARGS construction. Any new bwrap flags added to BWRAP_ARGS must also be emitted in the dry-run echo block.

BWRAP_ARGS construction (lines 453-494): conditional mounts (CLAUDE_JSON_MOUNT, CREDS_MOUNT) are appended after the base array. SSH mounts follow the same pattern.

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       <socket-path>   (read-write, --with-ssh)` if WITH_SSH
   - For each key: `ssh-key     <path>   (read-only)`; add ` + .pub` line if pub exists
   - `known_hosts <path>   (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 ; then _SSH_NOTES="" && _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 <path>` (explicit key files)

   Mounts a specific private key (and matching `.pub`, if present) read-only into the sandbox at `~/.ssh/<basename>`. 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 <socket> <socket> and --setenv SSH_AUTH_SOCK ....
  5. claudebox --dry-run --ssh-key ~/.ssh/id_ed25519 (assuming key exists) — output contains --dir $HOME/.ssh, --ro-bind <key> $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).

<success_criteria>

  • All three tasks complete and <done> 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. </success_criteria>
After completion, create `.planning/quick/260504-bw4-add-ssh-support-to-claudebox-with-ssh-fl/260504-bw4-SUMMARY.md`.