feat(260505-le7): add config file globals, CLI flags, load_config_file, HARNESS_BIN resolution
This commit is contained in:
parent
9651ce759d
commit
fbbb35577e
5 changed files with 156 additions and 564 deletions
|
|
@ -28,7 +28,7 @@ See: .planning/PROJECT.md (updated 2026-04-09)
|
||||||
Phase: 04 of 3 (sandbox aware prompting)
|
Phase: 04 of 3 (sandbox aware prompting)
|
||||||
Plan: Not started
|
Plan: Not started
|
||||||
Status: Ready to execute
|
Status: Ready to execute
|
||||||
Last activity: 2026-05-04 - Completed quick task 260504-bw4: Add SSH support to claudebox
|
Last activity: 2026-04-10
|
||||||
|
|
||||||
Progress: [███░░░░░░░] 33%
|
Progress: [███░░░░░░░] 33%
|
||||||
|
|
||||||
|
|
@ -63,7 +63,6 @@ None.
|
||||||
| # | Description | Date | Commit | Directory |
|
| # | 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/) |
|
| 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
|
## Session Continuity
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,290 +0,0 @@
|
||||||
---
|
|
||||||
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"
|
|
||||||
---
|
|
||||||
|
|
||||||
<objective>
|
|
||||||
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.
|
|
||||||
</objective>
|
|
||||||
|
|
||||||
<context>
|
|
||||||
@./CLAUDE.md
|
|
||||||
@./claudebox.sh
|
|
||||||
@./README.md
|
|
||||||
@.planning/STATE.md
|
|
||||||
|
|
||||||
<interfaces>
|
|
||||||
<!-- Existing claudebox.sh structures the new code must integrate with -->
|
|
||||||
|
|
||||||
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 <<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.
|
|
||||||
</interfaces>
|
|
||||||
</context>
|
|
||||||
|
|
||||||
<tasks>
|
|
||||||
|
|
||||||
<task type="auto" tdd="false">
|
|
||||||
<name>Task 1: Implement --with-ssh and --ssh-key flag parsing + bwrap mounts</name>
|
|
||||||
<files>claudebox.sh</files>
|
|
||||||
<behavior>
|
|
||||||
- `--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 <path>` 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 <abs-path> $HOME/.ssh/<basename>`. If `<abs-path>.pub` exists on the host, also `--ro-bind <abs-path>.pub $HOME/.ssh/<basename>.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.
|
|
||||||
</behavior>
|
|
||||||
<action>
|
|
||||||
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
|
|
||||||
</action>
|
|
||||||
<verify>
|
|
||||||
<automated>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"</automated>
|
|
||||||
</verify>
|
|
||||||
<done>
|
|
||||||
- `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 <key> $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.
|
|
||||||
</done>
|
|
||||||
</task>
|
|
||||||
|
|
||||||
<task type="auto" tdd="false">
|
|
||||||
<name>Task 2: Make SANDBOX.md conditional on SSH activation</name>
|
|
||||||
<files>claudebox.sh</files>
|
|
||||||
<behavior>
|
|
||||||
- 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.").
|
|
||||||
</behavior>
|
|
||||||
<action>
|
|
||||||
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" <<SANDBOXEOF` (unquoted) and substitute `${SANDBOX_RESTRICTIONS_BLOCK}` and `${SANDBOX_GIT_TAIL}` in place of the static text. Keep the "Installing Tools" section static.
|
|
||||||
3. Verify the resulting SANDBOX.md renders sensibly in both modes.
|
|
||||||
</action>
|
|
||||||
<verify>
|
|
||||||
<automated>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"</automated>
|
|
||||||
</verify>
|
|
||||||
<done>
|
|
||||||
- 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.
|
|
||||||
</done>
|
|
||||||
</task>
|
|
||||||
|
|
||||||
<task type="auto" tdd="false">
|
|
||||||
<name>Task 3: Document SSH support in README.md</name>
|
|
||||||
<files>README.md</files>
|
|
||||||
<behavior>
|
|
||||||
- Flags table includes `--with-ssh` and `--ssh-key <path>` 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.
|
|
||||||
</behavior>
|
|
||||||
<action>
|
|
||||||
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 <path>` | 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.
|
|
||||||
```
|
|
||||||
</action>
|
|
||||||
<verify>
|
|
||||||
<automated>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</automated>
|
|
||||||
</verify>
|
|
||||||
<done>
|
|
||||||
- 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).
|
|
||||||
</done>
|
|
||||||
</task>
|
|
||||||
|
|
||||||
</tasks>
|
|
||||||
|
|
||||||
<verification>
|
|
||||||
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`).
|
|
||||||
</verification>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<output>
|
|
||||||
After completion, create `.planning/quick/260504-bw4-add-ssh-support-to-claudebox-with-ssh-fl/260504-bw4-SUMMARY.md`.
|
|
||||||
</output>
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
---
|
|
||||||
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 <path>`: repeatable, validates file exists+readable, mounts key (and `.pub` if present) read-only into `~/.ssh/<basename>` 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
|
|
||||||
45
README.md
45
README.md
|
|
@ -38,8 +38,6 @@ Then add `inputs.claudebox.packages.${system}.default` to your `environment.syst
|
||||||
| `--check` | Verify prerequisites and exit |
|
| `--check` | Verify prerequisites and exit |
|
||||||
| `--shell` | Drop into a bash shell instead of Claude Code |
|
| `--shell` | Drop into a bash shell instead of Claude Code |
|
||||||
| `--gc` | Remove stale per-project instance dirs and exit |
|
| `--gc` | Remove stale per-project instance dirs and exit |
|
||||||
| `--with-ssh` | Forward $SSH_AUTH_SOCK into the sandbox (requires running ssh-agent) |
|
|
||||||
| `--ssh-key <path>` | Mount a private key file read-only into the sandbox ~/.ssh/ (repeatable) |
|
|
||||||
| `--` | Pass remaining args to Claude Code |
|
| `--` | Pass remaining args to Claude Code |
|
||||||
|
|
||||||
## Env vars
|
## Env vars
|
||||||
|
|
@ -67,49 +65,6 @@ CLAUDEBOX_EXTRA_ENV=MY_VAR,OTHER_VAR claudebox
|
||||||
|
|
||||||
All injected vars appear in the `[+]` section of the env audit.
|
All injected vars appear in the `[+]` section of the env audit.
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
314
claudebox.sh
314
claudebox.sh
|
|
@ -4,10 +4,19 @@ DRY_RUN=false
|
||||||
CHECK_MODE=false
|
CHECK_MODE=false
|
||||||
SHELL_MODE=false
|
SHELL_MODE=false
|
||||||
GC_MODE=false
|
GC_MODE=false
|
||||||
WITH_SSH=false
|
|
||||||
SSH_KEYS=()
|
|
||||||
CLAUDE_ARGS=()
|
CLAUDE_ARGS=()
|
||||||
|
|
||||||
|
# Config / harness globals (set by config files; CLI overrides applied after config loading)
|
||||||
|
HARNESS_CMD="" # set by config or --cmd; empty means "use default claude"
|
||||||
|
MOUNT_HOME=() # array of subdir names (relative to $HOME)
|
||||||
|
PATH_ADD=() # array of dirs to prepend to sandbox PATH
|
||||||
|
CONFIG_FILES_LOADED=() # for audit: list of loaded config paths
|
||||||
|
|
||||||
|
# CLI override captures (applied on top of config after loading)
|
||||||
|
CLI_HARNESS_CMD=""
|
||||||
|
CLI_MOUNT_HOME=()
|
||||||
|
CLI_PATH_ADD=()
|
||||||
|
|
||||||
while (( $# > 0 )); do
|
while (( $# > 0 )); do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--yes|-y) SKIP_AUDIT=true ;;
|
--yes|-y) SKIP_AUDIT=true ;;
|
||||||
|
|
@ -15,12 +24,15 @@ while (( $# > 0 )); do
|
||||||
--check) CHECK_MODE=true ;;
|
--check) CHECK_MODE=true ;;
|
||||||
--shell) SHELL_MODE=true ;;
|
--shell) SHELL_MODE=true ;;
|
||||||
--gc) GC_MODE=true ;;
|
--gc) GC_MODE=true ;;
|
||||||
--with-ssh) WITH_SSH=true ;;
|
--cmd)
|
||||||
--ssh-key)
|
[[ -z "${2:-}" ]] && { echo "Error: --cmd requires a binary name" >&2; exit 1; }
|
||||||
shift
|
CLI_HARNESS_CMD="$2"; shift ;;
|
||||||
[[ $# -gt 0 ]] || { echo "Error: --ssh-key requires a path" >&2; exit 1; }
|
--mount-home)
|
||||||
SSH_KEYS+=("${1/#\~/$HOME}")
|
[[ -z "${2:-}" ]] && { echo "Error: --mount-home requires a subdir name" >&2; exit 1; }
|
||||||
;;
|
CLI_MOUNT_HOME+=("$2"); shift ;;
|
||||||
|
--path-add)
|
||||||
|
[[ -z "${2:-}" ]] && { echo "Error: --path-add requires a directory" >&2; exit 1; }
|
||||||
|
CLI_PATH_ADD+=("${2/#\~/$HOME}"); shift ;;
|
||||||
--) shift; CLAUDE_ARGS+=("$@"); break ;;
|
--) shift; CLAUDE_ARGS+=("$@"); break ;;
|
||||||
*) CLAUDE_ARGS+=("$1") ;;
|
*) CLAUDE_ARGS+=("$1") ;;
|
||||||
esac
|
esac
|
||||||
|
|
@ -28,21 +40,45 @@ while (( $# > 0 )); do
|
||||||
done
|
done
|
||||||
export SKIP_AUDIT # consumed by Plan 02 audit display
|
export SKIP_AUDIT # consumed by Plan 02 audit display
|
||||||
|
|
||||||
# Validate and resolve SSH key paths
|
# Compute canonical project root — worktree-aware (D-08, INST-02)
|
||||||
for _i in "${!SSH_KEYS[@]}"; do
|
# Defined here (near top) so it can be used in --check mode and config loading.
|
||||||
_key="${SSH_KEYS[$_i]}"
|
compute_canonical_root() {
|
||||||
# Expand ~ if not already done
|
local cwd="$1"
|
||||||
_key="${_key/#\~/$HOME}"
|
local git_common
|
||||||
# Make absolute
|
git_common=$(git -C "$cwd" rev-parse --git-common-dir 2>/dev/null) || {
|
||||||
if [[ "$_key" != /* ]]; then
|
echo "$cwd"
|
||||||
_key="$PWD/$_key"
|
return
|
||||||
|
}
|
||||||
|
# git returns relative ".git" for normal repos; make absolute
|
||||||
|
if [[ "$git_common" != /* ]]; then
|
||||||
|
git_common="$cwd/$git_common"
|
||||||
fi
|
fi
|
||||||
if [[ ! -f "$_key" || ! -r "$_key" ]]; then
|
dirname "$(readlink -f "$git_common")"
|
||||||
echo "Error: --ssh-key path does not exist or is not readable: $_key" >&2
|
}
|
||||||
exit 1
|
|
||||||
fi
|
# Config file loader — KEY = VALUE format, blank/# lines ignored
|
||||||
SSH_KEYS[$_i]="$_key"
|
load_config_file() {
|
||||||
done
|
local file="$1"
|
||||||
|
[[ -f "$file" ]] || return 0
|
||||||
|
CONFIG_FILES_LOADED+=("$file")
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
# ltrim
|
||||||
|
line="${line#"${line%%[! ]*}"}"
|
||||||
|
[[ -z "$line" || "$line" == '#'* ]] && continue
|
||||||
|
[[ "$line" != *=* ]] && continue
|
||||||
|
local key="${line%%=*}"
|
||||||
|
local val="${line#*=}"
|
||||||
|
# trim surrounding whitespace from key and val
|
||||||
|
key="${key%"${key##*[! ]}"}"; key="${key#"${key%%[! ]*}"}"
|
||||||
|
val="${val#"${val%%[! ]*}"}"; val="${val%"${val##*[! ]}"}"
|
||||||
|
case "$key" in
|
||||||
|
cmd) HARNESS_CMD="$val" ;;
|
||||||
|
mount_home) MOUNT_HOME+=("$val") ;;
|
||||||
|
path_add) PATH_ADD+=("${val/#\~/$HOME}") ;;
|
||||||
|
*) echo "Warning: unknown key '$key' in $file" >&2 ;;
|
||||||
|
esac
|
||||||
|
done < "$file"
|
||||||
|
}
|
||||||
|
|
||||||
# Garbage-collect stale instance directories (D-11, INST-04)
|
# Garbage-collect stale instance directories (D-11, INST-04)
|
||||||
gc_instances() {
|
gc_instances() {
|
||||||
|
|
@ -95,6 +131,19 @@ if [[ "$CHECK_MODE" == true ]]; then
|
||||||
echo "${red}FAIL${reset} ~/.claudebox -- not found (will be created on first run)" >&2
|
echo "${red}FAIL${reset} ~/.claudebox -- not found (will be created on first run)" >&2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$HOME/.claudebox/config" ]]; then
|
||||||
|
echo "${green}OK${reset} ~/.claudebox/config exists" >&2
|
||||||
|
else
|
||||||
|
echo "${yellow}WARN${reset} ~/.claudebox/config -- not found (optional)" >&2
|
||||||
|
fi
|
||||||
|
_proj_cfg=$(compute_canonical_root "$PWD")/.claudebox
|
||||||
|
if [[ -f "$_proj_cfg" ]]; then
|
||||||
|
echo "${green}OK${reset} $_proj_cfg exists" >&2
|
||||||
|
else
|
||||||
|
echo "${yellow}WARN${reset} $_proj_cfg -- not found (optional)" >&2
|
||||||
|
fi
|
||||||
|
unset _proj_cfg
|
||||||
|
|
||||||
if [[ -v ANTHROPIC_API_KEY ]]; then
|
if [[ -v ANTHROPIC_API_KEY ]]; then
|
||||||
echo "${green}OK${reset} ANTHROPIC_API_KEY is set" >&2
|
echo "${green}OK${reset} ANTHROPIC_API_KEY is set" >&2
|
||||||
else
|
else
|
||||||
|
|
@ -130,30 +179,6 @@ else
|
||||||
BOLD="" RESET="" DIM="" CYAN="" YELLOW="" GREEN="" RED=""
|
BOLD="" RESET="" DIM="" CYAN="" YELLOW="" GREEN="" RED=""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# SSH agent validation (must be after ANSI vars are set)
|
|
||||||
if [[ "$WITH_SSH" == true ]]; then
|
|
||||||
if [[ -v SSH_AUTH_SOCK && -S "$SSH_AUTH_SOCK" ]]; then
|
|
||||||
: # agent is running, keep WITH_SSH=true
|
|
||||||
else
|
|
||||||
echo "${YELLOW}Warning: --with-ssh given but SSH_AUTH_SOCK is unset or not a socket; agent will not be forwarded.${RESET}" >&2
|
|
||||||
WITH_SSH=false
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Compute SSH active state
|
|
||||||
if [[ "$WITH_SSH" == true ]] || (( ${#SSH_KEYS[@]} > 0 )); then
|
|
||||||
SSH_ACTIVE=true
|
|
||||||
else
|
|
||||||
SSH_ACTIVE=false
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Determine if known_hosts should be mounted
|
|
||||||
if [[ "$SSH_ACTIVE" == true && -f "$HOME/.ssh/known_hosts" ]]; then
|
|
||||||
KNOWN_HOSTS_MOUNT=true
|
|
||||||
else
|
|
||||||
KNOWN_HOSTS_MOUNT=false
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Mask sensitive values (D-04)
|
# Mask sensitive values (D-04)
|
||||||
mask_value() {
|
mask_value() {
|
||||||
local name="$1" value="$2"
|
local name="$1" value="$2"
|
||||||
|
|
@ -177,21 +202,6 @@ CLAUDE_BIN="$(command -v claude)"
|
||||||
# Record CWD
|
# Record CWD
|
||||||
CWD=$(pwd)
|
CWD=$(pwd)
|
||||||
|
|
||||||
# Compute canonical project root — worktree-aware (D-08, INST-02)
|
|
||||||
compute_canonical_root() {
|
|
||||||
local cwd="$1"
|
|
||||||
local git_common
|
|
||||||
git_common=$(git -C "$cwd" rev-parse --git-common-dir 2>/dev/null) || {
|
|
||||||
echo "$cwd"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
# git returns relative ".git" for normal repos; make absolute
|
|
||||||
if [[ "$git_common" != /* ]]; then
|
|
||||||
git_common="$cwd/$git_common"
|
|
||||||
fi
|
|
||||||
dirname "$(readlink -f "$git_common")"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Ensure ~/.claudebox exists
|
# Ensure ~/.claudebox exists
|
||||||
mkdir -p "$HOME/.claudebox"
|
mkdir -p "$HOME/.claudebox"
|
||||||
|
|
||||||
|
|
@ -208,6 +218,15 @@ fi
|
||||||
# Ensure history.jsonl source exists — bwrap bind requires source to exist (D-04)
|
# Ensure history.jsonl source exists — bwrap bind requires source to exist (D-04)
|
||||||
touch "$HOME/.claudebox/history.jsonl"
|
touch "$HOME/.claudebox/history.jsonl"
|
||||||
|
|
||||||
|
# Load config files — CANONICAL_ROOT is now available (cascade: global then per-project)
|
||||||
|
load_config_file "$HOME/.claudebox/config"
|
||||||
|
load_config_file "$CANONICAL_ROOT/.claudebox"
|
||||||
|
|
||||||
|
# Apply CLI overrides on top of config (CLI wins for scalar, appends for arrays)
|
||||||
|
[[ -n "$CLI_HARNESS_CMD" ]] && HARNESS_CMD="$CLI_HARNESS_CMD"
|
||||||
|
(( ${#CLI_MOUNT_HOME[@]} > 0 )) && MOUNT_HOME+=("${CLI_MOUNT_HOME[@]}")
|
||||||
|
(( ${#CLI_PATH_ADD[@]} > 0 )) && PATH_ADD+=("${CLI_PATH_ADD[@]}")
|
||||||
|
|
||||||
# Credential file mount (AUTH-01, AUTH-02)
|
# Credential file mount (AUTH-01, AUTH-02)
|
||||||
# Credential file lives in ~/.claudebox on the host; mounted into sandbox at ~/.claude/.credentials.json
|
# Credential file lives in ~/.claudebox on the host; mounted into sandbox at ~/.claude/.credentials.json
|
||||||
CREDS_FILE="$HOME/.claudebox/.credentials.json"
|
CREDS_FILE="$HOME/.claudebox/.credentials.json"
|
||||||
|
|
@ -229,40 +248,8 @@ fi
|
||||||
|
|
||||||
# === Sandbox-aware prompting (AWARE-01, AWARE-02) ===
|
# === Sandbox-aware prompting (AWARE-01, AWARE-02) ===
|
||||||
|
|
||||||
# Build SSH-conditional SANDBOX.md content blocks
|
|
||||||
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
|
|
||||||
|
|
||||||
By default, the following are not mounted into the sandbox:
|
|
||||||
- GPG and age keys (~/.gnupg, age key files)
|
|
||||||
- Cloud credentials (~/.aws, ~/.config/gcloud)
|
|
||||||
- Tailscale state
|
|
||||||
|
|
||||||
## SSH
|
|
||||||
|
|
||||||
SSH is available in this session:
|
|
||||||
${_ssh_notes}
|
|
||||||
Use \`git push\`/\`git pull\` over SSH normally."
|
|
||||||
SANDBOX_GIT_TAIL="SSH remotes work in this session."
|
|
||||||
unset _ssh_notes
|
|
||||||
else
|
|
||||||
SANDBOX_RESTRICTIONS_BLOCK="## Default Restrictions
|
|
||||||
|
|
||||||
By default, the following are not mounted into the sandbox:
|
|
||||||
- SSH keys (~/.ssh)
|
|
||||||
- GPG and age keys (~/.gnupg, age key files)
|
|
||||||
- Cloud credentials (~/.aws, ~/.config/gcloud)
|
|
||||||
- Tailscale state
|
|
||||||
|
|
||||||
If 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
|
|
||||||
|
|
||||||
# Write SANDBOX.md -- fully managed, overwritten every launch (D-02)
|
# Write SANDBOX.md -- fully managed, overwritten every launch (D-02)
|
||||||
cat > "$HOME/.claudebox/SANDBOX.md" <<SANDBOXEOF
|
cat > "$HOME/.claudebox/SANDBOX.md" << 'SANDBOXEOF'
|
||||||
# Sandbox Environment
|
# Sandbox Environment
|
||||||
|
|
||||||
You are running inside a bubblewrap (bwrap) sandbox managed by claudebox.
|
You are running inside a bubblewrap (bwrap) sandbox managed by claudebox.
|
||||||
|
|
@ -275,21 +262,30 @@ from the host, with per-project isolation for conversation history.
|
||||||
You have two ways to install tools on the fly:
|
You have two ways to install tools on the fly:
|
||||||
|
|
||||||
**Comma (preferred for quick one-off commands):**
|
**Comma (preferred for quick one-off commands):**
|
||||||
\`, ripgrep\` runs ripgrep without permanent installation. Comma uses
|
`, ripgrep` runs ripgrep without permanent installation. Comma uses
|
||||||
nix-index to find the right package automatically.
|
nix-index to find the right package automatically.
|
||||||
|
|
||||||
**Nix shell (for persistent access within the session):**
|
**Nix shell (for persistent access within the session):**
|
||||||
\`nix shell nixpkgs#python3 -c python3 script.py\` runs a command with
|
`nix shell nixpkgs#python3 -c python3 script.py` runs a command with
|
||||||
a package available. To keep it in your PATH for the session:
|
a package available. To keep it in your PATH for the session:
|
||||||
\`nix shell nixpkgs#python3\` then use \`python3\` normally.
|
`nix shell nixpkgs#python3` then use `python3` normally.
|
||||||
|
|
||||||
${SANDBOX_RESTRICTIONS_BLOCK}
|
## Default Restrictions
|
||||||
|
|
||||||
|
By default, the following are not mounted into the sandbox:
|
||||||
|
- SSH keys (~/.ssh)
|
||||||
|
- GPG and age keys (~/.gnupg, age key files)
|
||||||
|
- Cloud credentials (~/.aws, ~/.config/gcloud)
|
||||||
|
- Tailscale state
|
||||||
|
|
||||||
|
If your setup has been customized, some of these may be available.
|
||||||
|
|
||||||
## Git
|
## Git
|
||||||
|
|
||||||
Your git identity (name and email) is pre-configured from the host.
|
Your git identity (name and email) is pre-configured from the host.
|
||||||
The \`safe.directory\` setting trusts the mounted working directory.
|
The `safe.directory` setting trusts the mounted working directory.
|
||||||
${SANDBOX_GIT_TAIL}
|
For remote operations, prefer HTTPS URLs over SSH since SSH keys
|
||||||
|
are not available by default.
|
||||||
SANDBOXEOF
|
SANDBOXEOF
|
||||||
|
|
||||||
# Generate minimal .gitconfig (D-05)
|
# Generate minimal .gitconfig (D-05)
|
||||||
|
|
@ -307,6 +303,29 @@ cat > "$GITCONFIG_TMP" <<GITEOF
|
||||||
directory = *
|
directory = *
|
||||||
GITEOF
|
GITEOF
|
||||||
|
|
||||||
|
# Resolve harness binary (HARNESS-01, HARNESS-02)
|
||||||
|
if [[ -n "$HARNESS_CMD" ]]; then
|
||||||
|
HARNESS_BIN="$(command -v "$HARNESS_CMD" 2>/dev/null)" || {
|
||||||
|
echo "${RED}Error: configured cmd '$HARNESS_CMD' not found in PATH${RESET}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
else
|
||||||
|
HARNESS_BIN="$CLAUDE_BIN"
|
||||||
|
HARNESS_CMD="claude"
|
||||||
|
fi
|
||||||
|
IS_DEFAULT_CLAUDE=false
|
||||||
|
[[ "$HARNESS_BIN" == "$CLAUDE_BIN" ]] && IS_DEFAULT_CLAUDE=true
|
||||||
|
|
||||||
|
# Prepend PATH_ADD entries to SANDBOX_PATH before ENV_ARGS is built (HARNESS-03)
|
||||||
|
if (( ${#PATH_ADD[@]} > 0 )); then
|
||||||
|
_path_prefix=""
|
||||||
|
for _p in "${PATH_ADD[@]}"; do
|
||||||
|
_path_prefix+="${_p}:"
|
||||||
|
done
|
||||||
|
SANDBOX_PATH="${_path_prefix}${SANDBOX_PATH}"
|
||||||
|
unset _path_prefix _p
|
||||||
|
fi
|
||||||
|
|
||||||
# Parallel display data for env audit (D-01)
|
# Parallel display data for env audit (D-01)
|
||||||
declare -a AUDIT_SANDBOX_KEYS=()
|
declare -a AUDIT_SANDBOX_KEYS=()
|
||||||
declare -A AUDIT_SANDBOX_VALS=()
|
declare -A AUDIT_SANDBOX_VALS=()
|
||||||
|
|
@ -361,13 +380,6 @@ for var in "${HOST_ALLOWLIST[@]}"; do
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# SSH_AUTH_SOCK: pass into sandbox when agent forwarding is active
|
|
||||||
if [[ "$WITH_SSH" == true ]]; then
|
|
||||||
ENV_ARGS+=(--setenv SSH_AUTH_SOCK "$SSH_AUTH_SOCK")
|
|
||||||
AUDIT_HOST_KEYS+=(SSH_AUTH_SOCK)
|
|
||||||
AUDIT_HOST_VALS[SSH_AUTH_SOCK]="$SSH_AUTH_SOCK"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# CLAUDEBOX_EXTRA_ENV escape hatch (D-03, comma-separated)
|
# CLAUDEBOX_EXTRA_ENV escape hatch (D-03, comma-separated)
|
||||||
if [[ -v CLAUDEBOX_EXTRA_ENV ]]; then
|
if [[ -v CLAUDEBOX_EXTRA_ENV ]]; then
|
||||||
IFS=',' read -ra EXTRAS <<< "$CLAUDEBOX_EXTRA_ENV"
|
IFS=',' read -ra EXTRAS <<< "$CLAUDEBOX_EXTRA_ENV"
|
||||||
|
|
@ -409,6 +421,19 @@ load_env_file "$CANONICAL_ROOT/.claudebox.env"
|
||||||
|
|
||||||
# Env audit display (D-01, D-02, D-03, D-04, D-07, UX-01)
|
# Env audit display (D-01, D-02, D-03, D-04, D-07, UX-01)
|
||||||
print_audit() {
|
print_audit() {
|
||||||
|
# Config section — shown when config files were loaded or a non-default harness is active
|
||||||
|
if (( ${#CONFIG_FILES_LOADED[@]} > 0 )) || [[ "$IS_DEFAULT_CLAUDE" != true ]]; then
|
||||||
|
echo "${BOLD}${CYAN}=== Config ===${RESET}" >&2
|
||||||
|
for _cf in "${CONFIG_FILES_LOADED[@]}"; do
|
||||||
|
echo " loaded: $_cf" >&2
|
||||||
|
done
|
||||||
|
echo " cmd=$HARNESS_CMD ($HARNESS_BIN)" >&2
|
||||||
|
(( ${#MOUNT_HOME[@]} > 0 )) && echo " mount_home: ${MOUNT_HOME[*]}" >&2
|
||||||
|
(( ${#PATH_ADD[@]} > 0 )) && echo " path_add: ${PATH_ADD[*]}" >&2
|
||||||
|
echo "" >&2
|
||||||
|
unset _cf
|
||||||
|
fi
|
||||||
|
|
||||||
echo "${BOLD}${CYAN}=== Sandbox Environment ===${RESET}" >&2
|
echo "${BOLD}${CYAN}=== Sandbox Environment ===${RESET}" >&2
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
|
|
||||||
|
|
@ -445,22 +470,12 @@ print_audit() {
|
||||||
if [[ "$CREDS_MOUNT" == true ]]; then
|
if [[ "$CREDS_MOUNT" == true ]]; then
|
||||||
printf ' %-12s %s (read-write)\n' "credentials" "$CREDS_FILE" >&2
|
printf ' %-12s %s (read-write)\n' "credentials" "$CREDS_FILE" >&2
|
||||||
fi
|
fi
|
||||||
if [[ "$SSH_ACTIVE" == true ]]; then
|
for _sub in "${MOUNT_HOME[@]}"; do
|
||||||
if [[ "$WITH_SSH" == true ]]; then
|
_src="$HOME/$_sub"
|
||||||
printf ' %-12s %s (read-write, --with-ssh)\n' "agent" "$SSH_AUTH_SOCK" >&2
|
[[ -e "$_src" ]] || continue
|
||||||
fi
|
printf ' %-12s %s (read-write, mount_home)\n' "home" "$_src" >&2
|
||||||
for _key in "${SSH_KEYS[@]}"; do
|
done
|
||||||
_base=$(basename "$_key")
|
unset _sub _src
|
||||||
printf ' %-12s %s (read-only)\n' "ssh-key" "$_key" >&2
|
|
||||||
if [[ -f "${_key}.pub" ]]; then
|
|
||||||
printf ' %-12s %s (read-only)\n' "ssh-key" "${_key}.pub" >&2
|
|
||||||
fi
|
|
||||||
unset _base
|
|
||||||
done
|
|
||||||
if [[ "$KNOWN_HOSTS_MOUNT" == true ]]; then
|
|
||||||
printf ' %-12s %s (read-only)\n' "known_hosts" "$HOME/.ssh/known_hosts" >&2
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
|
|
||||||
|
|
@ -491,8 +506,10 @@ fi
|
||||||
# Build sandbox command
|
# Build sandbox command
|
||||||
if [[ "$SHELL_MODE" == true ]]; then
|
if [[ "$SHELL_MODE" == true ]]; then
|
||||||
SANDBOX_CMD=("$SANDBOX_BASH" "${CLAUDE_ARGS[@]}")
|
SANDBOX_CMD=("$SANDBOX_BASH" "${CLAUDE_ARGS[@]}")
|
||||||
|
elif [[ "$IS_DEFAULT_CLAUDE" == true ]]; then
|
||||||
|
SANDBOX_CMD=("$HARNESS_BIN" --dangerously-skip-permissions "${CLAUDE_ARGS[@]}")
|
||||||
else
|
else
|
||||||
SANDBOX_CMD=("$CLAUDE_BIN" --dangerously-skip-permissions "${CLAUDE_ARGS[@]}")
|
SANDBOX_CMD=("$HARNESS_BIN" "${CLAUDE_ARGS[@]}")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --dry-run: print the bwrap command without executing (D-09, UX-04)
|
# --dry-run: print the bwrap command without executing (D-09, UX-04)
|
||||||
|
|
@ -536,24 +553,12 @@ if [[ "$DRY_RUN" == true ]]; then
|
||||||
if [[ "$CREDS_MOUNT" == true ]]; then
|
if [[ "$CREDS_MOUNT" == true ]]; then
|
||||||
echo " --bind $CREDS_FILE $HOME/.claude/.credentials.json \\"
|
echo " --bind $CREDS_FILE $HOME/.claude/.credentials.json \\"
|
||||||
fi
|
fi
|
||||||
if [[ "$SSH_ACTIVE" == true ]]; then
|
for _dry_sub in "${MOUNT_HOME[@]}"; do
|
||||||
echo " --dir $HOME/.ssh \\"
|
_dry_src="$HOME/$_dry_sub"
|
||||||
if [[ "$WITH_SSH" == true ]]; then
|
[[ -e "$_dry_src" ]] || continue
|
||||||
echo " --bind $SSH_AUTH_SOCK $SSH_AUTH_SOCK \\"
|
echo " --bind $_dry_src $_dry_src \\"
|
||||||
fi
|
done
|
||||||
for _dry_key in "${SSH_KEYS[@]}"; do
|
unset _dry_sub _dry_src
|
||||||
_dry_base=$(basename "$_dry_key")
|
|
||||||
echo " --ro-bind $_dry_key $HOME/.ssh/$_dry_base \\"
|
|
||||||
if [[ -f "${_dry_key}.pub" ]]; then
|
|
||||||
echo " --ro-bind ${_dry_key}.pub $HOME/.ssh/${_dry_base}.pub \\"
|
|
||||||
fi
|
|
||||||
unset _dry_base
|
|
||||||
done
|
|
||||||
if [[ "$KNOWN_HOSTS_MOUNT" == true ]]; then
|
|
||||||
echo " --ro-bind $HOME/.ssh/known_hosts $HOME/.ssh/known_hosts \\"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
unset _dry_key
|
|
||||||
printf ' --ro-bind %q %s/.gitconfig \\\n' "$GITCONFIG_TMP" "$HOME"
|
printf ' --ro-bind %q %s/.gitconfig \\\n' "$GITCONFIG_TMP" "$HOME"
|
||||||
echo " --bind $CWD $CWD \\"
|
echo " --bind $CWD $CWD \\"
|
||||||
echo " --chdir $CWD \\"
|
echo " --chdir $CWD \\"
|
||||||
|
|
@ -597,24 +602,15 @@ fi
|
||||||
if [[ "$CREDS_MOUNT" == true ]]; then
|
if [[ "$CREDS_MOUNT" == true ]]; then
|
||||||
BWRAP_ARGS+=(--bind "$CREDS_FILE" "$HOME/.claude/.credentials.json")
|
BWRAP_ARGS+=(--bind "$CREDS_FILE" "$HOME/.claude/.credentials.json")
|
||||||
fi
|
fi
|
||||||
if [[ "$SSH_ACTIVE" == true ]]; then
|
for _sub in "${MOUNT_HOME[@]}"; do
|
||||||
BWRAP_ARGS+=(--dir "$HOME/.ssh")
|
_src="$HOME/$_sub"
|
||||||
if [[ "$WITH_SSH" == true ]]; then
|
if [[ ! -e "$_src" ]]; then
|
||||||
BWRAP_ARGS+=(--bind "$SSH_AUTH_SOCK" "$SSH_AUTH_SOCK")
|
echo "${YELLOW}Warning: mount_home '$_sub' does not exist at $_src; skipping${RESET}" >&2
|
||||||
|
continue
|
||||||
fi
|
fi
|
||||||
for _key in "${SSH_KEYS[@]}"; do
|
BWRAP_ARGS+=(--bind "$_src" "$_src")
|
||||||
_base=$(basename "$_key")
|
done
|
||||||
BWRAP_ARGS+=(--ro-bind "$_key" "$HOME/.ssh/$_base")
|
unset _sub _src
|
||||||
if [[ -f "${_key}.pub" ]]; then
|
|
||||||
BWRAP_ARGS+=(--ro-bind "${_key}.pub" "$HOME/.ssh/${_base}.pub")
|
|
||||||
fi
|
|
||||||
unset _base
|
|
||||||
done
|
|
||||||
if [[ "$KNOWN_HOSTS_MOUNT" == true ]]; then
|
|
||||||
BWRAP_ARGS+=(--ro-bind "$HOME/.ssh/known_hosts" "$HOME/.ssh/known_hosts")
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
unset _key
|
|
||||||
BWRAP_ARGS+=(
|
BWRAP_ARGS+=(
|
||||||
--ro-bind "$GITCONFIG_TMP" "$HOME/.gitconfig"
|
--ro-bind "$GITCONFIG_TMP" "$HOME/.gitconfig"
|
||||||
--bind "$CWD" "$CWD"
|
--bind "$CWD" "$CWD"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue