diff --git a/claudebox.sh b/claudebox.sh index a763072..22f7dee 100644 --- a/claudebox.sh +++ b/claudebox.sh @@ -4,6 +4,8 @@ DRY_RUN=false CHECK_MODE=false SHELL_MODE=false GC_MODE=false +WITH_SSH=false +SSH_KEYS=() CLAUDE_ARGS=() while (( $# > 0 )); do @@ -13,6 +15,12 @@ while (( $# > 0 )); do --check) CHECK_MODE=true ;; --shell) SHELL_MODE=true ;; --gc) GC_MODE=true ;; + --with-ssh) WITH_SSH=true ;; + --ssh-key) + shift + [[ $# -gt 0 ]] || { echo "Error: --ssh-key requires a path" >&2; exit 1; } + SSH_KEYS+=("${1/#\~/$HOME}") + ;; --) shift; CLAUDE_ARGS+=("$@"); break ;; *) CLAUDE_ARGS+=("$1") ;; esac @@ -20,6 +28,22 @@ while (( $# > 0 )); do done export SKIP_AUDIT # consumed by Plan 02 audit display +# Validate and resolve SSH key paths +for _i in "${!SSH_KEYS[@]}"; do + _key="${SSH_KEYS[$_i]}" + # Expand ~ if not already done + _key="${_key/#\~/$HOME}" + # Make absolute + if [[ "$_key" != /* ]]; then + _key="$PWD/$_key" + fi + if [[ ! -f "$_key" || ! -r "$_key" ]]; then + echo "Error: --ssh-key path does not exist or is not readable: $_key" >&2 + exit 1 + fi + SSH_KEYS[$_i]="$_key" +done + # Garbage-collect stale instance directories (D-11, INST-04) gc_instances() { local removed=0 @@ -106,6 +130,30 @@ else BOLD="" RESET="" DIM="" CYAN="" YELLOW="" GREEN="" RED="" 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_value() { local name="$1" value="$2" @@ -290,6 +338,13 @@ for var in "${HOST_ALLOWLIST[@]}"; do fi 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) if [[ -v CLAUDEBOX_EXTRA_ENV ]]; then IFS=',' read -ra EXTRAS <<< "$CLAUDEBOX_EXTRA_ENV" @@ -367,6 +422,22 @@ print_audit() { if [[ "$CREDS_MOUNT" == true ]]; then printf ' %-12s %s (read-write)\n' "credentials" "$CREDS_FILE" >&2 fi + if [[ "$SSH_ACTIVE" == true ]]; then + if [[ "$WITH_SSH" == true ]]; then + printf ' %-12s %s (read-write, --with-ssh)\n' "agent" "$SSH_AUTH_SOCK" >&2 + fi + for _key in "${SSH_KEYS[@]}"; do + _base=$(basename "$_key") + 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 @@ -442,6 +513,24 @@ if [[ "$DRY_RUN" == true ]]; then if [[ "$CREDS_MOUNT" == true ]]; then echo " --bind $CREDS_FILE $HOME/.claude/.credentials.json \\" fi + if [[ "$SSH_ACTIVE" == true ]]; then + echo " --dir $HOME/.ssh \\" + if [[ "$WITH_SSH" == true ]]; then + echo " --bind $SSH_AUTH_SOCK $SSH_AUTH_SOCK \\" + fi + for _dry_key in "${SSH_KEYS[@]}"; do + _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" echo " --bind $CWD $CWD \\" echo " --chdir $CWD \\" @@ -485,6 +574,24 @@ fi if [[ "$CREDS_MOUNT" == true ]]; then BWRAP_ARGS+=(--bind "$CREDS_FILE" "$HOME/.claude/.credentials.json") fi +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 + 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+=( --ro-bind "$GITCONFIG_TMP" "$HOME/.gitconfig" --bind "$CWD" "$CWD"