@alanbem/dclaude 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +36 -3
  2. package/dclaude +885 -30
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -132,12 +132,15 @@ dclaude gh # Interactive GitHub login
132
132
  dclaude exec gh pr list # Use gh commands
133
133
  ```
134
134
 
135
- ### SSH Server for IDEs
135
+ ### SSH Key and Server Management
136
136
 
137
- Connect JetBrains Gateway, VS Code Remote, or any SSH client:
137
+ Load SSH keys and start SSH server for JetBrains Gateway, VS Code Remote, or any SSH client:
138
138
 
139
139
  ```bash
140
- dclaude ssh # Start SSH server, shows port
140
+ dclaude ssh # Load keys + start SSH server
141
+ dclaude ssh keys # Load SSH keys into agent
142
+ dclaude ssh server # Start SSH server, shows port
143
+ dclaude ssh server --stop # Stop SSH server
141
144
  # Connect: ssh claude@localhost -p <port>
142
145
  # Password: claude
143
146
  ```
@@ -151,6 +154,32 @@ dclaude chrome # Launch Chrome with DevTools
151
154
  dclaude # Claude can now interact with the browser
152
155
  ```
153
156
 
157
+ ### AWS CLI Integration
158
+
159
+ AWS CLI v2 is pre-installed in the container. Configure how AWS credentials are provided:
160
+
161
+ ```bash
162
+ # Auto (default): mounts ~/.aws from host if it exists, otherwise no config
163
+ dclaude
164
+
165
+ # Mount host's ~/.aws directory (read-write, shared with host)
166
+ DCLAUDE_AWS_CLI=mount dclaude
167
+
168
+ # Use isolated Docker volume (persists across container recreations)
169
+ DCLAUDE_AWS_CLI=volume dclaude
170
+
171
+ # No AWS config mounting
172
+ DCLAUDE_AWS_CLI=none dclaude
173
+ ```
174
+
175
+ **Volume mode** provides isolated AWS config per namespace:
176
+
177
+ ```bash
178
+ dclaude aws configure # Copy ~/.aws/config (profiles, regions) into container
179
+ dclaude aws login # Run 'aws login' in container
180
+ dclaude aws login --profile staging # Login with specific profile
181
+ ```
182
+
154
183
  ### iTerm2 Shell Integration
155
184
 
156
185
  If you use iTerm2 on macOS, dclaude automatically enables [iTerm2 Shell Integration](https://iterm2.com/documentation-shell-integration.html):
@@ -173,6 +202,7 @@ dclaude automatically tells Claude about its container environment so it can giv
173
202
  - **Network mode** - Whether `localhost` works or needs `host.docker.internal`
174
203
  - **Docker access** - Whether Docker commands are available
175
204
  - **SSH auth method** - How git authentication is configured
205
+ - **AWS CLI** - Whether and how AWS credentials are configured
176
206
  - **Path mirroring** - That file paths match the host
177
207
 
178
208
  This helps Claude understand its environment without you explaining it. Disable if needed:
@@ -195,6 +225,7 @@ DCLAUDE_SYSTEM_CONTEXT=false dclaude
195
225
  | `DCLAUDE_QUIET` | `false` | Suppress info messages |
196
226
  | `DCLAUDE_NO_UPDATE` | `false` | Skip image update check |
197
227
  | `DCLAUDE_SYSTEM_CONTEXT` | `true` | Inform Claude about container environment |
228
+ | `DCLAUDE_AWS_CLI` | `auto` | AWS config: `auto`, `mount`, `volume`, `none` |
198
229
  | `DCLAUDE_ITERM2` | `true` | Enable iTerm2 shell integration (only affects iTerm2) |
199
230
 
200
231
  ## Configuration File
@@ -220,6 +251,7 @@ dclaude walks up the directory tree to find `.dclaude` files. Any dclaude sessio
220
251
  | `MOUNT_ROOT` | Mount directory (relative to config file, or absolute path) |
221
252
  | `DEBUG` | Enable debug output (`true`, `false`) |
222
253
  | `CHROME_PORT` | Chrome DevTools port |
254
+ | `AWS_CLI` | AWS config mode (`mount`, `volume`, `none`) |
223
255
 
224
256
  **Precedence:** Environment variables override `.dclaude` file settings.
225
257
 
@@ -327,6 +359,7 @@ The container includes:
327
359
  - **Docker CLI** and **Docker Compose**
328
360
  - **Git**, **GitHub CLI** (`gh`), common dev tools
329
361
  - **tmux** for session management
362
+ - **AWS CLI v2** for cloud operations
330
363
  - **SSH server** for IDE integration
331
364
 
332
365
  ## Troubleshooting
package/dclaude CHANGED
@@ -135,6 +135,9 @@ load_config_file() {
135
135
  fi
136
136
  fi
137
137
  ;;
138
+ AWS_CLI)
139
+ [[ -z "${DCLAUDE_AWS_CLI:-}" ]] && DCLAUDE_AWS_CLI="$value"
140
+ ;;
138
141
  esac
139
142
  fi
140
143
  done < "$config_file"
@@ -151,6 +154,7 @@ readonly DEBUG="${DCLAUDE_DEBUG:-false}"
151
154
  readonly GIT_AUTH_MODE="${DCLAUDE_GIT_AUTH:-auto}" # auto, agent-forwarding, key-mount, none
152
155
  readonly NAMESPACE="${DCLAUDE_NAMESPACE:-}"
153
156
  CHROME_PORT="${DCLAUDE_CHROME_PORT:-9222}"
157
+ readonly AWS_CLI_MODE="${DCLAUDE_AWS_CLI:-auto}" # auto, mount, volume, none
154
158
 
155
159
  # Show config file if loaded (always at info level, details at debug)
156
160
  if [[ -n "$DCLAUDE_CONFIG_FILE" ]]; then
@@ -161,6 +165,7 @@ if [[ -n "$DCLAUDE_CONFIG_FILE" ]]; then
161
165
  [[ -n "${DCLAUDE_GIT_AUTH:-}" ]] && debug " GIT_AUTH=$DCLAUDE_GIT_AUTH"
162
166
  [[ -n "${DCLAUDE_CHROME_PORT:-}" ]] && debug " CHROME_PORT=$DCLAUDE_CHROME_PORT"
163
167
  [[ -n "${DCLAUDE_MOUNT_ROOT:-}" ]] && debug " MOUNT_ROOT=$DCLAUDE_MOUNT_ROOT"
168
+ [[ -n "${DCLAUDE_AWS_CLI:-}" ]] && debug " AWS_CLI=$DCLAUDE_AWS_CLI"
164
169
  fi
165
170
  fi
166
171
 
@@ -510,6 +515,36 @@ get_volume_name() {
510
515
  fi
511
516
  }
512
517
 
518
+ # Get AWS volume name (includes namespace if set)
519
+ get_aws_volume_name() {
520
+ if [[ -n "$NAMESPACE" ]]; then
521
+ echo "${VOLUME_PREFIX}-${NAMESPACE}-aws"
522
+ else
523
+ echo "${VOLUME_PREFIX}-aws"
524
+ fi
525
+ }
526
+
527
+ # Resolve AWS CLI mode from configured value
528
+ # auto → mount (if ~/.aws exists) or none
529
+ resolve_aws_cli_mode() {
530
+ case "$AWS_CLI_MODE" in
531
+ mount|volume|none)
532
+ echo "$AWS_CLI_MODE"
533
+ ;;
534
+ auto)
535
+ if [[ -d "${HOME}/.aws" ]]; then
536
+ echo "mount"
537
+ else
538
+ echo "none"
539
+ fi
540
+ ;;
541
+ *)
542
+ warning "Unknown AWS_CLI mode: $AWS_CLI_MODE (using none)"
543
+ echo "none"
544
+ ;;
545
+ esac
546
+ }
547
+
513
548
  # Create Docker volumes if they don't exist
514
549
  create_volumes() {
515
550
  # Create essential volume for Claude CLI persistence
@@ -526,6 +561,24 @@ create_volumes() {
526
561
  else
527
562
  debug "Volume exists: $volume"
528
563
  fi
564
+
565
+ # Create AWS volume if volume mode is active
566
+ local aws_mode
567
+ aws_mode=$(resolve_aws_cli_mode)
568
+ if [[ "$aws_mode" == "volume" ]]; then
569
+ local aws_volume
570
+ aws_volume=$(get_aws_volume_name)
571
+ if ! docker volume inspect "$aws_volume" &> /dev/null; then
572
+ info "Creating AWS volume: $aws_volume"
573
+ if ! docker volume create "$aws_volume" > /dev/null; then
574
+ error "Failed to create volume: $aws_volume"
575
+ exit 1
576
+ fi
577
+ debug "Volume created successfully: $aws_volume"
578
+ else
579
+ debug "Volume exists: $aws_volume"
580
+ fi
581
+ fi
529
582
  }
530
583
 
531
584
  # Pull or update the Docker image
@@ -660,6 +713,7 @@ generate_system_context() {
660
713
  local has_docker="${3:-false}"
661
714
  local platform="${4:-unknown}"
662
715
  local docker_socket="${5:-}"
716
+ local aws_cli_mode="${6:-none}"
663
717
 
664
718
  cat <<'EOF'
665
719
 
@@ -806,8 +860,36 @@ EOF
806
860
  ## Development Tools Available
807
861
  - **Languages**: Node.js 20+, Python 3
808
862
  - **Package Managers**: npm, pip, Homebrew/Linuxbrew
809
- - **Tools**: git, gh (GitHub CLI), docker, docker-compose, curl, tmux, nano
863
+ - **Tools**: git, gh (GitHub CLI), docker, docker-compose, aws (AWS CLI v2), curl, tmux, nano
810
864
  - **Shell**: bash (your commands execute in bash shell)
865
+ EOF
866
+
867
+ # AWS CLI context
868
+ case "$aws_cli_mode" in
869
+ mount)
870
+ cat <<'EOF'
871
+ - **AWS CLI**: Configured — credentials mounted from host's `~/.aws` directory
872
+ - All host AWS profiles, SSO config, and credentials are available
873
+ - Changes to AWS config (e.g., `aws sso login`) are shared with host
874
+ EOF
875
+ ;;
876
+ volume)
877
+ cat <<'EOF'
878
+ - **AWS CLI**: Configured — credentials stored in persistent Docker volume
879
+ - AWS config is isolated from host and persists across container recreations
880
+ - Run `aws configure` or `aws configure sso` to set up credentials
881
+ EOF
882
+ ;;
883
+ *)
884
+ cat <<'EOF'
885
+ - **AWS CLI**: Installed but no credentials configured
886
+ - User can set `DCLAUDE_AWS_CLI=mount` when launching dclaude to use host's `~/.aws` config
887
+ - User can set `DCLAUDE_AWS_CLI=volume` when launching dclaude for isolated persistent config
888
+ EOF
889
+ ;;
890
+ esac
891
+
892
+ cat <<'EOF'
811
893
 
812
894
  ## Git Configuration Requirements
813
895
  **IMPORTANT**: Before performing any git operations (commit, push, etc.), you MUST:
@@ -987,6 +1069,423 @@ handle_git_auth() {
987
1069
  fi
988
1070
  }
989
1071
 
1072
+ # ============================================================================
1073
+ # Agent Teams Tmux Passthrough — Relay Functions
1074
+ # ============================================================================
1075
+ # When the user runs dclaude from inside a host tmux session, these functions
1076
+ # set up a TCP relay that forwards Claude Code's tmux commands to the host.
1077
+ # This makes Agent Teams sub-agent panes visible in the host's tmux session.
1078
+
1079
+ # Check if user is in a host tmux session
1080
+ detect_host_tmux() {
1081
+ [[ -n "${TMUX:-}" ]]
1082
+ }
1083
+
1084
+ # Check if host has required tools for relay (jq + socat or ncat)
1085
+ check_relay_deps() {
1086
+ if ! command -v jq &>/dev/null; then
1087
+ debug "Relay dep missing: jq"
1088
+ return 1
1089
+ fi
1090
+ if command -v socat &>/dev/null; then
1091
+ debug "Relay listener: socat"
1092
+ return 0
1093
+ fi
1094
+ if command -v ncat &>/dev/null; then
1095
+ debug "Relay listener: ncat"
1096
+ return 0
1097
+ fi
1098
+ debug "Relay dep missing: socat or ncat"
1099
+ return 1
1100
+ }
1101
+
1102
+ # Get the address the container uses to reach the host relay
1103
+ get_relay_host() {
1104
+ local platform="$1"
1105
+ local network_mode="$2"
1106
+ # Linux host mode: container shares host network, use loopback
1107
+ if [[ "$platform" == "linux" && "$network_mode" == "host" ]]; then
1108
+ echo "127.0.0.1"
1109
+ else
1110
+ # macOS (any mode) or Linux bridge: use Docker's host gateway
1111
+ echo "host.docker.internal"
1112
+ fi
1113
+ }
1114
+
1115
+ # Get the address the relay binds to on the host
1116
+ get_relay_bind_addr() {
1117
+ local platform="$1"
1118
+ local network_mode="$2"
1119
+ # Linux bridge: container reaches host via bridge gateway, not loopback
1120
+ if [[ "$platform" == "linux" && "$network_mode" == "bridge" ]]; then
1121
+ echo "0.0.0.0"
1122
+ else
1123
+ echo "127.0.0.1"
1124
+ fi
1125
+ }
1126
+
1127
+ # Get or create relay nonce for a container (reused across reattachments)
1128
+ get_or_create_relay_nonce() {
1129
+ local container_name="$1"
1130
+ local nonce_file="$HOME/.dclaude/tmux-relay-nonce-${container_name}"
1131
+
1132
+ mkdir -p "$HOME/.dclaude"
1133
+
1134
+ if [[ -f "$nonce_file" ]]; then
1135
+ cat "$nonce_file"
1136
+ return 0
1137
+ fi
1138
+
1139
+ local nonce
1140
+ nonce=$(head -c 32 /dev/urandom | base64 | tr -d '=/+' | head -c 32)
1141
+ echo "$nonce" > "$nonce_file"
1142
+ echo "$nonce"
1143
+ }
1144
+
1145
+ # Generate the relay handler script (executed by socat/ncat for each connection)
1146
+ # Uses a non-quoted heredoc for baked-in values, then a single-quoted heredoc for logic
1147
+ generate_relay_handler() {
1148
+ local container_name="$1"
1149
+ local relay_port="$2"
1150
+ local nonce="$3"
1151
+ local handler_path="$4"
1152
+ local tracking_file="$5"
1153
+ local relay_host="$6"
1154
+
1155
+ # Header with baked-in values (variable expansion)
1156
+ cat > "$handler_path" <<HANDLER_HEADER
1157
+ #!/bin/bash
1158
+ CONTAINER="$container_name"
1159
+ NONCE="$nonce"
1160
+ RELAY_PORT="$relay_port"
1161
+ RELAY_HOST="$relay_host"
1162
+ TRACKING_FILE="$tracking_file"
1163
+ HANDLER_HEADER
1164
+
1165
+ # Logic body (single-quoted heredoc — no escaping needed)
1166
+ cat >> "$handler_path" <<'HANDLER_BODY'
1167
+
1168
+ # Read one JSON line (max 65536 bytes to prevent memory exhaustion)
1169
+ read -r -n 65536 request
1170
+ if [[ -z "$request" ]]; then
1171
+ printf '{"code":1,"stdout":"","stderr":"empty request"}\n'
1172
+ exit 1
1173
+ fi
1174
+
1175
+ # Validate nonce
1176
+ req_nonce=$(printf '%s' "$request" | jq -r '.nonce // empty')
1177
+ if [[ "$req_nonce" != "$NONCE" ]]; then
1178
+ printf '{"code":1,"stdout":"","stderr":"authentication failed"}\n'
1179
+ exit 1
1180
+ fi
1181
+
1182
+ # Extract command array (bash 3.2 compatible — no mapfile)
1183
+ cmd=()
1184
+ while IFS= read -r line; do
1185
+ cmd+=("$line")
1186
+ done < <(printf '%s' "$request" | jq -r '.cmd[]')
1187
+ subcmd="${cmd[0]}"
1188
+ cwd=$(printf '%s' "$request" | jq -r '.cwd // empty')
1189
+ req_pane=$(printf '%s' "$request" | jq -r '.pane // empty')
1190
+
1191
+ # Allowlist tmux subcommands
1192
+ case "$subcmd" in
1193
+ split-window|send-keys|list-panes|select-layout|resize-pane|\
1194
+ select-pane|kill-pane|has-session|list-windows|display-message|list-sessions)
1195
+ ;;
1196
+ *)
1197
+ printf '{"code":1,"stdout":"","stderr":"disallowed: %s"}\n' "$subcmd"
1198
+ exit 1
1199
+ ;;
1200
+ esac
1201
+
1202
+ # Build tmux args based on subcommand
1203
+ args=()
1204
+
1205
+ if [[ "$subcmd" == "split-window" ]]; then
1206
+ args+=("split-window")
1207
+
1208
+ # Parse split-window flags, strip -c (handled by docker exec -w)
1209
+ # and strip trailing shell command (replaced by docker exec)
1210
+ i=1
1211
+ while (( i < ${#cmd[@]} )); do
1212
+ case "${cmd[$i]}" in
1213
+ -b|-d|-f|-h|-I|-v|-P|-Z)
1214
+ args+=("${cmd[$i]}")
1215
+ ;;
1216
+ -c)
1217
+ # Capture directory, use for docker exec -w
1218
+ (( i++ ))
1219
+ cwd="${cmd[$i]}"
1220
+ ;;
1221
+ -e|-l|-t|-F)
1222
+ # Flags with a value argument
1223
+ args+=("${cmd[$i]}")
1224
+ (( i++ ))
1225
+ args+=("${cmd[$i]}")
1226
+ ;;
1227
+ -*)
1228
+ # Unknown flag, pass through
1229
+ args+=("${cmd[$i]}")
1230
+ ;;
1231
+ *)
1232
+ # Shell command — skip (replaced by docker exec)
1233
+ break
1234
+ ;;
1235
+ esac
1236
+ (( i++ ))
1237
+ done
1238
+
1239
+ # Validate cwd: absolute path, safe characters only
1240
+ if [[ -n "$cwd" && ! "$cwd" =~ ^[a-zA-Z0-9/._-]+$ ]]; then
1241
+ printf '{"code":1,"stdout":"","stderr":"invalid cwd"}\n'
1242
+ exit 1
1243
+ fi
1244
+
1245
+ # Append docker exec as the pane command (no sh -c, args as array)
1246
+ args+=(
1247
+ "--"
1248
+ "docker" "exec" "-it"
1249
+ "-e" "DCLAUDE_TMUX_RELAY_PORT=$RELAY_PORT"
1250
+ "-e" "DCLAUDE_TMUX_RELAY_HOST=$RELAY_HOST"
1251
+ "-e" "DCLAUDE_TMUX_RELAY_NONCE=$NONCE"
1252
+ "-e" "DCLAUDE_CONTAINER=$CONTAINER"
1253
+ "-u" "claude"
1254
+ "-w" "${cwd:-/home/claude}"
1255
+ "$CONTAINER"
1256
+ "bash"
1257
+ )
1258
+
1259
+ elif [[ "$subcmd" == "send-keys" ]]; then
1260
+ args+=("send-keys")
1261
+
1262
+ # Validate target pane is one we created
1263
+ for (( i=1; i < ${#cmd[@]}; i++ )); do
1264
+ if [[ "${cmd[$i]}" == "-t" && $(( i + 1 )) -lt ${#cmd[@]} ]]; then
1265
+ target="${cmd[$(( i + 1 ))]}"
1266
+ if [[ -f "$TRACKING_FILE" ]] && ! grep -qxF "$target" "$TRACKING_FILE"; then
1267
+ printf '{"code":1,"stdout":"","stderr":"pane not tracked: %s"}\n' "$target"
1268
+ exit 1
1269
+ fi
1270
+ break
1271
+ fi
1272
+ done
1273
+
1274
+ # Pass through all args
1275
+ for (( i=1; i < ${#cmd[@]}; i++ )); do
1276
+ args+=("${cmd[$i]}")
1277
+ done
1278
+
1279
+ elif [[ "$subcmd" == "kill-pane" ]]; then
1280
+ args+=("kill-pane")
1281
+ for (( i=1; i < ${#cmd[@]}; i++ )); do
1282
+ args+=("${cmd[$i]}")
1283
+ done
1284
+ else
1285
+ # All other allowed commands: pass through args
1286
+ # Inject -t $req_pane for commands that need pane context
1287
+ # (relay runs outside tmux pane context, so tmux needs explicit targeting)
1288
+ args+=("$subcmd")
1289
+ has_target=false
1290
+ for (( i=1; i < ${#cmd[@]}; i++ )); do
1291
+ [[ "${cmd[$i]}" == "-t" ]] && has_target=true
1292
+ [[ "${cmd[$i]}" =~ ^-t.+ ]] && has_target=true
1293
+ done
1294
+ # Only inject -t for commands that accept it (not list-sessions, has-session)
1295
+ case "$subcmd" in
1296
+ display-message|list-panes|list-windows|select-layout|resize-pane|select-pane)
1297
+ if [[ "$has_target" == "false" && -n "$req_pane" ]]; then
1298
+ args+=("-t" "$req_pane")
1299
+ fi
1300
+ ;;
1301
+ esac
1302
+ for (( i=1; i < ${#cmd[@]}; i++ )); do
1303
+ args+=("${cmd[$i]}")
1304
+ done
1305
+ fi
1306
+
1307
+ # Execute tmux command, capture output
1308
+ stdout_file=$(mktemp)
1309
+ stderr_file=$(mktemp)
1310
+ tmux "${args[@]}" >"$stdout_file" 2>"$stderr_file"
1311
+ code=$?
1312
+ stdout=$(cat "$stdout_file")
1313
+ stderr=$(cat "$stderr_file")
1314
+ rm -f "$stdout_file" "$stderr_file"
1315
+
1316
+ # Post-processing for split-window: track new pane ID
1317
+ if [[ "$subcmd" == "split-window" && $code -eq 0 ]]; then
1318
+ pane_id=$(echo "$stdout" | head -1 | tr -d '[:space:]')
1319
+ if [[ "$pane_id" =~ ^%[0-9]+$ ]]; then
1320
+ echo "$pane_id" >> "$TRACKING_FILE"
1321
+ fi
1322
+ fi
1323
+
1324
+ # Post-processing for kill-pane: clean up orphaned processes after delay
1325
+ if [[ "$subcmd" == "kill-pane" && $code -eq 0 ]]; then
1326
+ (
1327
+ sleep 2
1328
+ docker exec "$CONTAINER" bash -c '
1329
+ ps -eo pid,ppid,tty,comm --no-headers 2>/dev/null | while read pid ppid tty comm; do
1330
+ [[ "$ppid" == "0" && "$comm" == "bash" ]] || continue
1331
+ [[ "$tty" =~ ^pts/([0-9]+)$ ]] || continue
1332
+ (( ${BASH_REMATCH[1]} >= 2 )) || continue
1333
+ children=$(ps --ppid "$pid" --no-headers 2>/dev/null | wc -l)
1334
+ if [[ "$children" -eq 0 ]]; then
1335
+ kill -9 "$pid" 2>/dev/null
1336
+ fi
1337
+ done
1338
+ '
1339
+ ) &>/dev/null &
1340
+ fi
1341
+
1342
+ # Return JSON response
1343
+ jq -nc --arg code "$code" --arg stdout "$stdout" --arg stderr "$stderr" \
1344
+ '{code: ($code | tonumber), stdout: $stdout, stderr: $stderr}'
1345
+ HANDLER_BODY
1346
+ }
1347
+
1348
+ # Start the relay listener on the host
1349
+ start_relay() {
1350
+ local container_name="$1"
1351
+ local relay_port="$2"
1352
+ local nonce="$3"
1353
+ local bind_addr="$4"
1354
+ local relay_host="$5"
1355
+
1356
+ local handler_path="$HOME/.dclaude/tmux-relay-handler-${container_name}.sh"
1357
+ local pid_file="$HOME/.dclaude/tmux-relay-pid-${container_name}"
1358
+ local tracking_file="$HOME/.dclaude/tmux-relay-panes-${container_name}"
1359
+
1360
+ mkdir -p "$HOME/.dclaude"
1361
+
1362
+ # Generate handler script
1363
+ generate_relay_handler "$container_name" "$relay_port" "$nonce" \
1364
+ "$handler_path" "$tracking_file" "$relay_host"
1365
+ chmod +x "$handler_path"
1366
+
1367
+ # Clear pane tracking
1368
+ > "$tracking_file"
1369
+
1370
+ # Start listener
1371
+ if command -v socat &>/dev/null; then
1372
+ debug "Starting socat relay on $bind_addr:$relay_port"
1373
+ socat "TCP-LISTEN:$relay_port,bind=$bind_addr,reuseaddr,fork,max-children=10" \
1374
+ "EXEC:bash $handler_path" 2>/dev/null &
1375
+ elif command -v ncat &>/dev/null; then
1376
+ debug "Starting ncat relay on $bind_addr:$relay_port"
1377
+ ncat -l -k "$bind_addr" "$relay_port" --sh-exec "$handler_path" &
1378
+ else
1379
+ error "No relay listener available (need socat or ncat)"
1380
+ return 1
1381
+ fi
1382
+
1383
+ echo $! > "$pid_file"
1384
+ debug "Relay PID: $(cat "$pid_file")"
1385
+ }
1386
+
1387
+ # Stop the relay and clean up
1388
+ stop_relay() {
1389
+ local container_name="$1"
1390
+
1391
+ local pid_file="$HOME/.dclaude/tmux-relay-pid-${container_name}"
1392
+ local handler_path="$HOME/.dclaude/tmux-relay-handler-${container_name}.sh"
1393
+ local tracking_file="$HOME/.dclaude/tmux-relay-panes-${container_name}"
1394
+
1395
+ if [[ -f "$pid_file" ]]; then
1396
+ local pid
1397
+ pid=$(cat "$pid_file")
1398
+ if [[ -n "$pid" ]]; then
1399
+ # Kill forked handler processes first
1400
+ pkill -P "$pid" 2>/dev/null || true
1401
+ # Verify process identity before killing (guard against PID recycling)
1402
+ local comm
1403
+ comm=$(ps -p "$pid" -o comm= 2>/dev/null || true)
1404
+ if [[ "$comm" =~ (socat|ncat) ]]; then
1405
+ kill "$pid" 2>/dev/null || true
1406
+ fi
1407
+ fi
1408
+ fi
1409
+
1410
+ # Clean up orphaned bash processes in container
1411
+ cleanup_container_orphans "$container_name"
1412
+
1413
+ # Remove relay files (but keep nonce for reattachment)
1414
+ rm -f "$pid_file" "$handler_path" "$tracking_file"
1415
+ }
1416
+
1417
+ # Kill orphaned bash sessions in container (from dead docker exec panes)
1418
+ cleanup_container_orphans() {
1419
+ local container_name="$1"
1420
+
1421
+ # Check container is running before trying
1422
+ if ! docker ps -q -f name="^${container_name}$" 2>/dev/null | grep -q .; then
1423
+ return 0
1424
+ fi
1425
+
1426
+ docker exec "$container_name" bash -c '
1427
+ ps -eo pid,ppid,tty,comm --no-headers 2>/dev/null | while read pid ppid tty comm; do
1428
+ [[ "$ppid" == "0" && "$comm" == "bash" ]] || continue
1429
+ [[ "$tty" =~ ^pts/([0-9]+)$ ]] || continue
1430
+ (( ${BASH_REMATCH[1]} >= 2 )) || continue
1431
+ children=$(ps --ppid "$pid" --no-headers 2>/dev/null | wc -l)
1432
+ if [[ "$children" -eq 0 ]]; then
1433
+ kill -9 "$pid" 2>/dev/null
1434
+ fi
1435
+ done
1436
+ ' 2>/dev/null || true
1437
+ }
1438
+
1439
+ # Set up Agent Teams relay if conditions are met
1440
+ # Sets globals: RELAY_STARTED, TMUX_SESSION_ENV_ARGS
1441
+ setup_agent_teams_relay() {
1442
+ local container_name="$1"
1443
+ local relay_port="$2"
1444
+ local platform="$3"
1445
+ local network_mode="$4"
1446
+
1447
+ RELAY_STARTED=false
1448
+ TMUX_SESSION_ENV_ARGS=()
1449
+
1450
+ if [[ -z "$relay_port" ]]; then
1451
+ debug "No relay port configured for this container"
1452
+ TMUX_SESSION_ENV_ARGS=(-e "DCLAUDE_HIDE_TMUX=1")
1453
+ return
1454
+ fi
1455
+
1456
+ if ! detect_host_tmux; then
1457
+ debug "Not in host tmux - Agent Teams will use in-process mode"
1458
+ TMUX_SESSION_ENV_ARGS=(-e "DCLAUDE_HIDE_TMUX=1")
1459
+ return
1460
+ fi
1461
+
1462
+ if ! check_relay_deps; then
1463
+ info "Tip: Install jq + socat (or ncat) for Agent Teams pane mode in tmux"
1464
+ TMUX_SESSION_ENV_ARGS=(-e "DCLAUDE_HIDE_TMUX=1")
1465
+ return
1466
+ fi
1467
+
1468
+ local bind_addr relay_host relay_nonce host_pane
1469
+ bind_addr=$(get_relay_bind_addr "$platform" "$network_mode")
1470
+ relay_host=$(get_relay_host "$platform" "$network_mode")
1471
+ relay_nonce=$(get_or_create_relay_nonce "$container_name")
1472
+ host_pane=$(tmux display-message -p '#{pane_id}')
1473
+
1474
+ start_relay "$container_name" "$relay_port" "$relay_nonce" "$bind_addr" "$relay_host"
1475
+ RELAY_STARTED=true
1476
+
1477
+ TMUX_SESSION_ENV_ARGS=(
1478
+ -e "DCLAUDE_TMUX_RELAY_PORT=$relay_port"
1479
+ -e "DCLAUDE_TMUX_RELAY_HOST=$relay_host"
1480
+ -e "DCLAUDE_TMUX_RELAY_NONCE=$relay_nonce"
1481
+ -e "DCLAUDE_HOST_PANE=$host_pane"
1482
+ -e "DCLAUDE_CONTAINER=$container_name"
1483
+ )
1484
+
1485
+ debug "Agent Teams relay: $bind_addr:$relay_port -> $container_name (pane $host_pane)"
1486
+ }
1487
+ # ============================================================================
1488
+
990
1489
  # Detect TTY availability and return appropriate Docker flags
991
1490
  # Detect TTY status early (before any subshells)
992
1491
  # Must be done at script top-level since $() subshells don't inherit TTY
@@ -1031,6 +1530,17 @@ should_skip_tmux() {
1031
1530
  return 1
1032
1531
  }
1033
1532
 
1533
+ # Debug countdown before launching Claude (gives time to read debug output)
1534
+ # Press Enter to skip the countdown and launch immediately
1535
+ debug_countdown() {
1536
+ if [[ "$DEBUG" == "true" ]]; then
1537
+ for i in 5 4 3 2 1; do
1538
+ printf '\r%b' "${CYAN}Debug: Launching in ${i}... (press Enter to skip)${NC}" >&2
1539
+ read -t 1 -s 2>/dev/null && break
1540
+ done
1541
+ printf '\r%b\n' "${CYAN}Debug: Launching... ${NC}" >&2
1542
+ fi
1543
+ }
1034
1544
 
1035
1545
  # Main execution
1036
1546
  main() {
@@ -1145,14 +1655,27 @@ main() {
1145
1655
  debug "Git auth: $resolved_git_auth (user-specified)"
1146
1656
  fi
1147
1657
 
1658
+ # Warn if agent-forwarding is active but no keys are loaded
1659
+ if [[ "$resolved_git_auth" == "agent-forwarding" ]]; then
1660
+ local ssh_add_rc=0
1661
+ ssh-add -l >/dev/null 2>&1 || ssh_add_rc=$?
1662
+ if [[ $ssh_add_rc -eq 1 ]]; then
1663
+ warning "SSH agent has no keys loaded — SSH won't work inside container."
1664
+ warning "Run 'dclaude ssh keys' to load your keys."
1665
+ fi
1666
+ fi
1667
+
1148
1668
  # Generate system context for Claude (if enabled) - must be done early for all code paths
1149
1669
  local claude_args=()
1150
1670
  if [[ "$ENABLE_SYSTEM_CONTEXT" == "true" ]]; then
1151
1671
  local has_docker="false"
1152
1672
  [[ -n "$DOCKER_SOCKET" ]] && [[ -S "$DOCKER_SOCKET" ]] && has_docker="true"
1153
1673
 
1674
+ local resolved_aws_cli_mode
1675
+ resolved_aws_cli_mode=$(resolve_aws_cli_mode)
1676
+
1154
1677
  local system_context
1155
- system_context=$(generate_system_context "$network_mode" "$resolved_git_auth" "$has_docker" "$platform" "$DOCKER_SOCKET")
1678
+ system_context=$(generate_system_context "$network_mode" "$resolved_git_auth" "$has_docker" "$platform" "$DOCKER_SOCKET" "$resolved_aws_cli_mode")
1156
1679
 
1157
1680
  claude_args+=("--append-system-prompt" "$system_context")
1158
1681
  debug "System context enabled (${#system_context} chars, has_docker=$has_docker, git_auth=$resolved_git_auth, platform=$platform)"
@@ -1244,10 +1767,23 @@ main() {
1244
1767
  [[ -n "${COLORTERM:-}" ]] && exec_env_args+=(-e "COLORTERM=${COLORTERM}")
1245
1768
  [[ -n "${DCLAUDE_ITERM2:-}" ]] && exec_env_args+=(-e "DCLAUDE_ITERM2=${DCLAUDE_ITERM2}")
1246
1769
 
1770
+ # Set up Agent Teams relay (or hide $TMUX for in-process mode)
1771
+ local relay_port
1772
+ relay_port=$(docker inspect --format='{{index .Config.Labels "dclaude.relay.port"}}' "$container_name" 2>/dev/null || true)
1773
+ setup_agent_teams_relay "$container_name" "$relay_port" "$platform" "$network_mode"
1774
+ if [[ "$RELAY_STARTED" == "true" ]]; then
1775
+ trap "stop_relay '$container_name'" EXIT
1776
+ fi
1777
+
1247
1778
  debug "Creating new tmux session running Claude"
1248
1779
  debug "Claude args count: ${#claude_args[@]}, user args: $*"
1249
1780
  info "Starting new Claude session..."
1250
- docker exec -it -u claude "${exec_env_args[@]}" "$container_name" tmux -f /home/claude/.tmux.conf new-session -s "$tmux_session" claude "${claude_args[@]}" "$@"
1781
+ debug_countdown
1782
+ docker exec -it -u claude "${exec_env_args[@]}" "$container_name" \
1783
+ /usr/bin/tmux -L dclaude-inner -f /home/claude/.tmux.conf \
1784
+ new-session -s "$tmux_session" \
1785
+ "${TMUX_SESSION_ENV_ARGS[@]}" \
1786
+ claude-launcher.sh "${claude_args[@]}" "$@"
1251
1787
  exit $?
1252
1788
  else
1253
1789
  # Non-interactive or print mode - run claude directly without tmux
@@ -1298,6 +1834,17 @@ main() {
1298
1834
  debug "SSH port reserved: $ssh_port"
1299
1835
  DOCKER_ARGS+=(--label "dclaude.ssh.port=${ssh_port}")
1300
1836
 
1837
+ # Reserve relay port for Agent Teams tmux passthrough
1838
+ local relay_port
1839
+ relay_port=$(find_available_port 30000 60000)
1840
+ debug "Relay port reserved: $relay_port"
1841
+ DOCKER_ARGS+=(--label "dclaude.relay.port=${relay_port}")
1842
+
1843
+ # Add host.docker.internal for Linux bridge mode (needed for relay connectivity)
1844
+ if [[ "$platform" == "linux" && "$network_mode" == "bridge" ]]; then
1845
+ DOCKER_ARGS+=(--add-host "host.docker.internal:host-gateway")
1846
+ fi
1847
+
1301
1848
  # Port mapping only needed for bridge mode (host mode shares network stack)
1302
1849
  if [[ "$network_mode" != "host" ]]; then
1303
1850
  DOCKER_ARGS+=(-p "${ssh_port}:${ssh_port}")
@@ -1341,6 +1888,32 @@ main() {
1341
1888
  [[ -n "$ssh_arg" ]] && DOCKER_ARGS+=("$ssh_arg")
1342
1889
  done < <(handle_git_auth)
1343
1890
 
1891
+ # Handle AWS CLI configuration mounting
1892
+ local resolved_aws_mode
1893
+ resolved_aws_mode=$(resolve_aws_cli_mode)
1894
+ # Pass mode to entrypoint so it knows whether to chown .aws (volume only)
1895
+ DOCKER_ARGS+=(-e "DCLAUDE_AWS_CLI_MODE=${resolved_aws_mode}")
1896
+ case "$resolved_aws_mode" in
1897
+ mount)
1898
+ if [[ -d "${HOME}/.aws" ]]; then
1899
+ DOCKER_ARGS+=(-v "${HOME}/.aws:/home/claude/.aws")
1900
+ info "AWS CLI: mounting host ~/.aws"
1901
+ debug "AWS config mounted from host: ${HOME}/.aws"
1902
+ else
1903
+ warning "AWS_CLI=mount but ~/.aws not found, skipping"
1904
+ fi
1905
+ ;;
1906
+ volume)
1907
+ local aws_volume
1908
+ aws_volume=$(get_aws_volume_name)
1909
+ DOCKER_ARGS+=(-v "${aws_volume}:/home/claude/.aws")
1910
+ debug "AWS config using volume: $aws_volume"
1911
+ ;;
1912
+ none)
1913
+ debug "AWS CLI config mounting disabled"
1914
+ ;;
1915
+ esac
1916
+
1344
1917
  # Add any additional environment variables
1345
1918
  if [[ -n "${CLAUDE_MODEL:-}" ]]; then
1346
1919
  DOCKER_ARGS+=(-e "CLAUDE_MODEL=${CLAUDE_MODEL}")
@@ -1397,10 +1970,21 @@ main() {
1397
1970
  [[ -n "${COLORTERM:-}" ]] && exec_env_args+=(-e "COLORTERM=${COLORTERM}")
1398
1971
  [[ -n "${DCLAUDE_ITERM2:-}" ]] && exec_env_args+=(-e "DCLAUDE_ITERM2=${DCLAUDE_ITERM2}")
1399
1972
 
1973
+ # Set up Agent Teams relay (or hide $TMUX for in-process mode)
1974
+ setup_agent_teams_relay "$container_name" "$relay_port" "$platform" "$network_mode"
1975
+ if [[ "$RELAY_STARTED" == "true" ]]; then
1976
+ trap "stop_relay '$container_name'" EXIT
1977
+ fi
1978
+
1400
1979
  debug "Creating new tmux session running Claude"
1401
1980
  debug "Claude args count: ${#claude_args[@]}, user args: $*"
1402
1981
  info "Starting new Claude session..."
1403
- docker exec -it -u claude "${exec_env_args[@]}" "$container_name" tmux -f /home/claude/.tmux.conf new-session -s "$tmux_session" claude "${claude_args[@]}" "$@"
1982
+ debug_countdown
1983
+ docker exec -it -u claude "${exec_env_args[@]}" "$container_name" \
1984
+ /usr/bin/tmux -L dclaude-inner -f /home/claude/.tmux.conf \
1985
+ new-session -s "$tmux_session" \
1986
+ "${TMUX_SESSION_ENV_ARGS[@]}" \
1987
+ claude-launcher.sh "${claude_args[@]}" "$@"
1404
1988
  exit $?
1405
1989
  else
1406
1990
  # Non-interactive or print mode - run claude directly without tmux
@@ -1530,11 +2114,11 @@ cmd_attach() {
1530
2114
  fi
1531
2115
  fi
1532
2116
 
1533
- # Check if session exists
1534
- if ! docker exec -u claude "$container_name" tmux has-session -t "$session_name" 2>/dev/null; then
2117
+ # Check if session exists (use -L dclaude-inner for namespaced socket)
2118
+ if ! docker exec -u claude "$container_name" /usr/bin/tmux -L dclaude-inner has-session -t "$session_name" 2>/dev/null; then
1535
2119
  error "Session '$session_name' not found in container"
1536
2120
  info "Available sessions:"
1537
- docker exec -u claude "$container_name" tmux list-sessions 2>/dev/null || echo " (no sessions running)"
2121
+ docker exec -u claude "$container_name" /usr/bin/tmux -L dclaude-inner list-sessions 2>/dev/null || echo " (no sessions running)"
1538
2122
  exit 1
1539
2123
  fi
1540
2124
 
@@ -1546,9 +2130,22 @@ cmd_attach() {
1546
2130
  [[ -n "${COLORTERM:-}" ]] && exec_env_args+=(-e "COLORTERM=${COLORTERM}")
1547
2131
  [[ -n "${DCLAUDE_ITERM2:-}" ]] && exec_env_args+=(-e "DCLAUDE_ITERM2=${DCLAUDE_ITERM2}")
1548
2132
 
2133
+ # Restart relay if the session uses it (relay was stopped when previous dclaude exited)
2134
+ local relay_port
2135
+ relay_port=$(docker inspect --format='{{index .Config.Labels "dclaude.relay.port"}}' "$container_name" 2>/dev/null || true)
2136
+ local platform
2137
+ platform=$(detect_platform)
2138
+ local network_mode
2139
+ network_mode=$(docker inspect --format='{{.HostConfig.NetworkMode}}' "$container_name" 2>/dev/null || echo "bridge")
2140
+ setup_agent_teams_relay "$container_name" "$relay_port" "$platform" "$network_mode"
2141
+ if [[ "$RELAY_STARTED" == "true" ]]; then
2142
+ trap "stop_relay '$container_name'" EXIT
2143
+ fi
2144
+
1549
2145
  # Attach to existing session
1550
2146
  info "Attaching to session: $session_name"
1551
- docker exec -it -u claude "${exec_env_args[@]}" "$container_name" tmux -f /home/claude/.tmux.conf attach-session -t "$session_name"
2147
+ docker exec -it -u claude "${exec_env_args[@]}" "$container_name" \
2148
+ /usr/bin/tmux -L dclaude-inner -f /home/claude/.tmux.conf attach-session -t "$session_name"
1552
2149
  exit $?
1553
2150
  }
1554
2151
 
@@ -1770,7 +2367,7 @@ cmd_stop() {
1770
2367
  if [[ "$container_status" == "running" ]]; then
1771
2368
  # Count active tmux sessions
1772
2369
  local session_count
1773
- session_count=$(docker exec -u claude "$container_name" tmux list-sessions 2>/dev/null | wc -l | tr -d '[:space:]') || session_count=0
2370
+ session_count=$(docker exec -u claude "$container_name" /usr/bin/tmux -L dclaude-inner list-sessions 2>/dev/null | wc -l | tr -d '[:space:]') || session_count=0
1774
2371
 
1775
2372
  if [[ "$session_count" =~ ^[0-9]+$ ]] && [[ "$session_count" -gt 0 ]]; then
1776
2373
  info "Stopping container $container_name ($session_count active session(s))..."
@@ -1778,6 +2375,9 @@ cmd_stop() {
1778
2375
  info "Stopping container $container_name..."
1779
2376
  fi
1780
2377
 
2378
+ # Stop relay if running
2379
+ stop_relay "$container_name"
2380
+
1781
2381
  if docker stop "$container_name" >/dev/null; then
1782
2382
  success "Container stopped"
1783
2383
  else
@@ -1790,8 +2390,117 @@ cmd_stop() {
1790
2390
  exit 0
1791
2391
  }
1792
2392
 
1793
- # Subcommand: SSH server for remote access (JetBrains Gateway, debugging, etc.)
2393
+ # Subcommand: SSH dispatches to 'keys', 'server', or both
1794
2394
  cmd_ssh() {
2395
+ local rc=0
2396
+ case "${1:-}" in
2397
+ keys)
2398
+ shift
2399
+ cmd_ssh_keys "$@" || rc=$?
2400
+ ;;
2401
+ server)
2402
+ shift
2403
+ cmd_ssh_server "$@" || rc=$?
2404
+ ;;
2405
+ --help|-h)
2406
+ cat << 'SSH_HELP'
2407
+ dclaude ssh - SSH Key and Server Management
2408
+
2409
+ Usage:
2410
+ dclaude ssh Load SSH keys into agent and start SSH server
2411
+ dclaude ssh keys Load SSH keys into agent
2412
+ dclaude ssh server Start SSH server and show connection info
2413
+ dclaude ssh server --stop Stop SSH server
2414
+
2415
+ SSH_HELP
2416
+ exit 0
2417
+ ;;
2418
+ "")
2419
+ # Bare 'dclaude ssh': load keys (non-fatal), then start server
2420
+ cmd_ssh_keys "$@" || true
2421
+ echo ""
2422
+ cmd_ssh_server "$@" || rc=$?
2423
+ ;;
2424
+ *)
2425
+ error "Unknown subcommand: $1"
2426
+ info "Usage: dclaude ssh [keys | server]"
2427
+ exit 1
2428
+ ;;
2429
+ esac
2430
+ exit "$rc"
2431
+ }
2432
+
2433
+ # Subcommand: SSH key loading
2434
+ cmd_ssh_keys() {
2435
+ # Parse flags
2436
+ while [[ $# -gt 0 ]]; do
2437
+ case "$1" in
2438
+ --help|-h)
2439
+ cat << 'SSH_KEYS_HELP'
2440
+ dclaude ssh keys - Load SSH Keys into Agent
2441
+
2442
+ Usage:
2443
+ dclaude ssh keys Detect and load SSH keys from ~/.ssh/
2444
+
2445
+ Detects private keys (id_rsa, id_ed25519, etc.) in ~/.ssh/ and loads
2446
+ them into the SSH agent via ssh-add. May prompt for passphrases.
2447
+
2448
+ SSH_KEYS_HELP
2449
+ return 0
2450
+ ;;
2451
+ *)
2452
+ error "Unknown option: $1"
2453
+ info "Usage: dclaude ssh keys"
2454
+ return 1
2455
+ ;;
2456
+ esac
2457
+ done
2458
+
2459
+ local ssh_dir="${HOME}/.ssh"
2460
+ if [[ ! -d "$ssh_dir" ]]; then
2461
+ error "No ~/.ssh directory found"
2462
+ return 1
2463
+ fi
2464
+
2465
+ # Detect private key files
2466
+ local keys=()
2467
+ for key_file in "$ssh_dir"/id_*; do
2468
+ [[ -f "$key_file" ]] || continue
2469
+ # Skip public keys and certificates
2470
+ [[ "$key_file" == *.pub ]] && continue
2471
+ [[ "$key_file" == *-cert.pub ]] && continue
2472
+ keys+=("$key_file")
2473
+ done
2474
+
2475
+ if [[ ${#keys[@]} -eq 0 ]]; then
2476
+ warning "No SSH keys found in ~/.ssh/"
2477
+ info "Generate a key with: ssh-keygen -t ed25519"
2478
+ return 1
2479
+ fi
2480
+
2481
+ info "Loading SSH keys..."
2482
+ local loaded=0
2483
+ for key_file in "${keys[@]}"; do
2484
+ local display_path="~/.ssh/$(basename "$key_file")"
2485
+ if ssh-add "$key_file" 2>/dev/null; then
2486
+ success " $display_path"
2487
+ loaded=$((loaded + 1))
2488
+ else
2489
+ warning " $display_path (failed — wrong passphrase or unsupported key)"
2490
+ fi
2491
+ done
2492
+
2493
+ echo ""
2494
+ if [[ $loaded -gt 0 ]]; then
2495
+ success "$loaded key(s) loaded into SSH agent."
2496
+ else
2497
+ error "No keys were loaded"
2498
+ return 1
2499
+ fi
2500
+ }
2501
+
2502
+ # Subcommand: SSH server for remote access (JetBrains Gateway, debugging, etc.)
2503
+ cmd_ssh_server() {
1795
2504
  local action=""
1796
2505
 
1797
2506
  # Parse flags
@@ -1802,12 +2511,12 @@ cmd_ssh() {
1802
2511
  shift
1803
2512
  ;;
1804
2513
  --help|-h)
1805
- cat << 'SSH_HELP'
1806
- dclaude ssh - SSH Server for Remote Access
2514
+ cat << 'SSH_SERVER_HELP'
2515
+ dclaude ssh server - SSH Server for Remote Access
1807
2516
 
1808
2517
  Usage:
1809
- dclaude ssh Start SSH server and show connection info
1810
- dclaude ssh --stop Stop SSH server
2518
+ dclaude ssh server Start SSH server and show connection info
2519
+ dclaude ssh server --stop Stop SSH server
1811
2520
 
1812
2521
  Connection:
1813
2522
  ssh claude@localhost -p <port>
@@ -1822,20 +2531,20 @@ Use Cases:
1822
2531
 
1823
2532
  JetBrains Gateway Setup:
1824
2533
  1. Start container: dclaude
1825
- 2. Start SSH: dclaude ssh
2534
+ 2. Start SSH: dclaude ssh server
1826
2535
  3. Open JetBrains Gateway
1827
2536
  4. New Connection → SSH → localhost:<port shown above>
1828
2537
  5. Username: claude, Password: claude
1829
2538
  6. Gateway will download and install IDE backend automatically
1830
2539
  7. Select your project directory
1831
2540
 
1832
- SSH_HELP
1833
- exit 0
2541
+ SSH_SERVER_HELP
2542
+ return 0
1834
2543
  ;;
1835
2544
  *)
1836
2545
  error "Unknown option: $1"
1837
- info "Usage: dclaude ssh [--stop]"
1838
- exit 1
2546
+ info "Usage: dclaude ssh server [--stop]"
2547
+ return 1
1839
2548
  ;;
1840
2549
  esac
1841
2550
  done
@@ -1846,7 +2555,7 @@ SSH_HELP
1846
2555
  if ! docker ps -a --format '{{.Names}}' | grep -q "^${container_name}$"; then
1847
2556
  error "No container found for this directory"
1848
2557
  info "Run 'dclaude' first to create a persistent container"
1849
- exit 1
2558
+ return 1
1850
2559
  fi
1851
2560
 
1852
2561
  local container_status=$(docker inspect --format='{{.State.Status}}' "$container_name" 2>/dev/null)
@@ -1858,7 +2567,7 @@ SSH_HELP
1858
2567
  sleep 1
1859
2568
  else
1860
2569
  error "Container $container_name is in unexpected state: $container_status"
1861
- exit 1
2570
+ return 1
1862
2571
  fi
1863
2572
  fi
1864
2573
 
@@ -1874,9 +2583,9 @@ SSH_HELP
1874
2583
  echo ""
1875
2584
  echo " dclaude rm -f"
1876
2585
  echo " dclaude"
1877
- echo " dclaude ssh"
2586
+ echo " dclaude ssh server"
1878
2587
  echo ""
1879
- exit 1
2588
+ return 1
1880
2589
  fi
1881
2590
 
1882
2591
  case "$action" in
@@ -1918,8 +2627,6 @@ SSH_HELP
1918
2627
  echo ""
1919
2628
  ;;
1920
2629
  esac
1921
-
1922
- exit 0
1923
2630
  }
1924
2631
 
1925
2632
  # Subcommand: remove container for current directory
@@ -1956,7 +2663,7 @@ cmd_rm() {
1956
2663
  if [[ "$force" == "true" ]]; then
1957
2664
  # Count active tmux sessions
1958
2665
  local session_count
1959
- session_count=$(docker exec -u claude "$container_name" tmux list-sessions 2>/dev/null | wc -l | tr -d '[:space:]') || session_count=0
2666
+ session_count=$(docker exec -u claude "$container_name" /usr/bin/tmux -L dclaude-inner list-sessions 2>/dev/null | wc -l | tr -d '[:space:]') || session_count=0
1960
2667
 
1961
2668
  if [[ "$session_count" =~ ^[0-9]+$ ]] && [[ "$session_count" -gt 0 ]]; then
1962
2669
  info "Removing running container $container_name ($session_count active session(s))..."
@@ -1964,6 +2671,10 @@ cmd_rm() {
1964
2671
  info "Removing running container $container_name..."
1965
2672
  fi
1966
2673
 
2674
+ # Clean up relay (including nonce file since container is being removed)
2675
+ stop_relay "$container_name"
2676
+ rm -f "$HOME/.dclaude/tmux-relay-nonce-${container_name}"
2677
+
1967
2678
  if docker rm -f "$container_name" >/dev/null; then
1968
2679
  success "Container removed"
1969
2680
  else
@@ -1978,6 +2689,11 @@ cmd_rm() {
1978
2689
  fi
1979
2690
  else
1980
2691
  info "Removing container $container_name..."
2692
+
2693
+ # Clean up relay files (including nonce since container is being removed)
2694
+ stop_relay "$container_name"
2695
+ rm -f "$HOME/.dclaude/tmux-relay-nonce-${container_name}"
2696
+
1981
2697
  if docker rm "$container_name" >/dev/null; then
1982
2698
  success "Container removed"
1983
2699
  else
@@ -1989,6 +2705,133 @@ cmd_rm() {
1989
2705
  }
1990
2706
 
1991
2707
  # Subcommand: configure git identity
2708
+ cmd_aws_configure() {
2709
+ local container_name="$1"
2710
+
2711
+ # This subcommand only makes sense for volume mode
2712
+ local resolved_mode
2713
+ resolved_mode=$(resolve_aws_cli_mode)
2714
+ if [[ "$resolved_mode" != "volume" ]]; then
2715
+ error "dclaude aws configure is only for AWS_CLI=volume mode"
2716
+ if [[ "$resolved_mode" == "mount" ]]; then
2717
+ info "Current mode is 'mount' — host's ~/.aws is already available in the container"
2718
+ else
2719
+ info "Set DCLAUDE_AWS_CLI=volume to use persistent AWS volume"
2720
+ fi
2721
+ exit 1
2722
+ fi
2723
+
2724
+ echo ""
2725
+ echo "AWS CLI Configuration"
2726
+ echo "─────────────────────"
2727
+
2728
+ # Check existing config in container
2729
+ local existing_config
2730
+ existing_config=$(docker exec -u claude "$container_name" cat /home/claude/.aws/config 2>/dev/null || echo "")
2731
+
2732
+ if [[ -n "$existing_config" ]]; then
2733
+ echo "Current config in container:"
2734
+ echo "$existing_config" | sed 's/^/ /'
2735
+ echo ""
2736
+ read -p "Overwrite with host config? [y/N]: " overwrite
2737
+ if [[ "$overwrite" != "y" && "$overwrite" != "Y" ]]; then
2738
+ exit 0
2739
+ fi
2740
+ fi
2741
+
2742
+ # Check host config
2743
+ if [[ ! -f "${HOME}/.aws/config" ]]; then
2744
+ error "No ~/.aws/config found on host"
2745
+ info "Run 'aws configure sso' on your host first, then re-run this command"
2746
+ exit 1
2747
+ fi
2748
+
2749
+ echo "Found on host:"
2750
+ sed 's/^/ /' "${HOME}/.aws/config"
2751
+ echo ""
2752
+ info "Only config (profiles, regions, SSO URLs) will be copied — no credentials or tokens"
2753
+ echo ""
2754
+ read -p "Copy to container? [Y/n]: " copy
2755
+ if [[ "$copy" == "n" || "$copy" == "N" ]]; then
2756
+ exit 0
2757
+ fi
2758
+
2759
+ # Copy config file into container (docker cp copies as root, fix ownership after)
2760
+ docker exec -u claude "$container_name" mkdir -p /home/claude/.aws
2761
+ docker cp "${HOME}/.aws/config" "${container_name}:/home/claude/.aws/config"
2762
+ docker exec "$container_name" chown claude:claude /home/claude/.aws /home/claude/.aws/config
2763
+ docker exec "$container_name" chmod 600 /home/claude/.aws/config
2764
+
2765
+ echo ""
2766
+ success "AWS config copied to container"
2767
+ info "Run 'dclaude aws login' or 'aws login' inside dclaude to authenticate"
2768
+ exit 0
2769
+ }
2770
+
2771
+ cmd_aws_login() {
2772
+ local container_name="$1"
2773
+ shift
2774
+
2775
+ local tty_flags
2776
+ tty_flags=$(detect_tty_flags)
2777
+
2778
+ info "Starting AWS login..."
2779
+ # shellcheck disable=SC2086
2780
+ exec docker exec $tty_flags -u claude "$container_name" aws login "$@"
2781
+ }
2782
+
2783
+ cmd_aws() {
2784
+ local container_name=$(get_container_name "$HOST_PATH")
2785
+
2786
+ # Check if container exists
2787
+ if ! docker ps -a --format '{{.Names}}' | grep -q "^${container_name}$"; then
2788
+ error "No container found for this directory"
2789
+ info "Run 'dclaude' first to create a container"
2790
+ exit 1
2791
+ fi
2792
+
2793
+ # Check if container is running
2794
+ local container_status=$(docker inspect --format='{{.State.Status}}' "$container_name" 2>/dev/null)
2795
+ if [[ "$container_status" != "running" ]]; then
2796
+ if [[ "$container_status" == "exited" ]]; then
2797
+ info "Starting container: $container_name"
2798
+ docker start "$container_name" >/dev/null
2799
+ sleep 1
2800
+ else
2801
+ error "Container $container_name is in unexpected state: $container_status"
2802
+ exit 1
2803
+ fi
2804
+ fi
2805
+
2806
+ # Dispatch subcommands
2807
+ local subcmd="${1:-}"
2808
+ case "$subcmd" in
2809
+ configure)
2810
+ shift
2811
+ cmd_aws_configure "$container_name" "$@"
2812
+ ;;
2813
+ login)
2814
+ shift
2815
+ cmd_aws_login "$container_name" "$@"
2816
+ ;;
2817
+ --help|-h|"")
2818
+ echo "Usage:"
2819
+ echo " dclaude aws configure Copy AWS config from host (volume mode)"
2820
+ echo " dclaude aws login Run AWS login in container"
2821
+ echo " dclaude aws login --profile <name> Login with specific profile"
2822
+ exit 0
2823
+ ;;
2824
+ *)
2825
+ error "Unknown subcommand: $subcmd"
2826
+ echo "Usage:"
2827
+ echo " dclaude aws configure Copy AWS config from host (volume mode)"
2828
+ echo " dclaude aws login Run AWS login in container"
2829
+ echo " dclaude aws login --profile <name> Login with specific profile"
2830
+ exit 1
2831
+ ;;
2832
+ esac
2833
+ }
2834
+
1992
2835
  cmd_git() {
1993
2836
  local container_name=$(get_container_name "$HOST_PATH")
1994
2837
 
@@ -2128,6 +2971,10 @@ if [[ $# -gt 0 ]]; then
2128
2971
  shift
2129
2972
  cmd_git "$@"
2130
2973
  ;;
2974
+ aws)
2975
+ shift
2976
+ cmd_aws "$@"
2977
+ ;;
2131
2978
  --help|-h|help)
2132
2979
  cat << 'EOF'
2133
2980
  dclaude - Dockerized Claude Code Launcher
@@ -2140,8 +2987,12 @@ Usage:
2140
2987
  dclaude update Update Claude CLI inside container
2141
2988
  dclaude stop Stop container for current directory
2142
2989
  dclaude rm [-f] Remove container for current directory
2143
- dclaude ssh Start SSH server for remote access
2990
+ dclaude ssh Load SSH keys and start SSH server
2991
+ dclaude ssh keys Load SSH keys into agent
2992
+ dclaude ssh server Start SSH server for remote access
2144
2993
  dclaude git Configure git identity (name/email)
2994
+ dclaude aws configure Copy AWS config from host to container (volume mode)
2995
+ dclaude aws login Run AWS login in container
2145
2996
  dclaude chrome [options] Launch Chrome with DevTools and MCP integration
2146
2997
  dclaude gh Authenticate GitHub CLI (runs gh auth login)
2147
2998
  dclaude exec [command] Execute command in container (default: bash)
@@ -2156,6 +3007,7 @@ Environment Variables:
2156
3007
  DCLAUDE_GIT_AUTH SSH auth for Git: auto, agent-forwarding, key-mount, none
2157
3008
  DCLAUDE_NETWORK Network mode: auto, host, bridge
2158
3009
  DCLAUDE_MOUNT_ROOT Mount parent directory (absolute or relative path)
3010
+ DCLAUDE_AWS_CLI AWS config mode: auto, mount, volume, none
2159
3011
  DCLAUDE_DOCKER_SOCKET Override Docker socket path
2160
3012
  DCLAUDE_TMUX_SESSION Custom tmux session name (default: claude-TIMESTAMP)
2161
3013
  DCLAUDE_CHROME_BIN Chrome executable path (auto-detected if not set)
@@ -2167,6 +3019,7 @@ Configuration File (.dclaude):
2167
3019
  Create a .dclaude file at project root to configure dclaude for that tree:
2168
3020
  NAMESPACE=mycompany
2169
3021
  NETWORK=host
3022
+ AWS_CLI=mount
2170
3023
  DEBUG=true
2171
3024
  MOUNT_ROOT=. # Mount config file's directory
2172
3025
  MOUNT_ROOT=.. # Mount parent of config file's directory
@@ -2205,9 +3058,11 @@ Examples:
2205
3058
  dclaude rm # Remove stopped container
2206
3059
  dclaude rm -f # Force remove running container
2207
3060
 
2208
- # SSH server for remote access (JetBrains Gateway, VS Code Remote, etc.)
2209
- dclaude ssh # Start SSH server, show connection info
2210
- dclaude ssh --stop # Stop SSH server
3061
+ # SSH key and server management
3062
+ dclaude ssh # Load keys + start SSH server
3063
+ dclaude ssh keys # Load SSH keys into agent
3064
+ dclaude ssh server # Start SSH server, show connection info
3065
+ dclaude ssh server --stop # Stop SSH server
2211
3066
 
2212
3067
  # Launch Chrome with DevTools for MCP integration
2213
3068
  dclaude chrome
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alanbem/dclaude",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "description": "Dockerized Claude Code CLI launcher with MCP support",
5
5
  "main": "dclaude",
6
6
  "bin": {