diff --git a/sandbox-ssh-fix/.claude-plugin/plugin.json b/sandbox-ssh-fix/.claude-plugin/plugin.json index 50f9598..e43b646 100644 --- a/sandbox-ssh-fix/.claude-plugin/plugin.json +++ b/sandbox-ssh-fix/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "sandbox-ssh-fix", - "version": "1.0.0", - "description": "Fixes git-over-SSH in the Claude Code sandbox on macOS by replacing the broken BSD nc SOCKS5 proxy with ncat", + "version": "1.0.1", + "description": "Fixes git-over-SSH in the Claude Code sandbox on macOS by wrapping BSD nc with ncat for SOCKS5 proxy auth", "author": { "name": "cblecker", "email": "admin@toph.ca" diff --git a/sandbox-ssh-fix/CLAUDE.md b/sandbox-ssh-fix/CLAUDE.md deleted file mode 100644 index 8c7bac0..0000000 --- a/sandbox-ssh-fix/CLAUDE.md +++ /dev/null @@ -1,25 +0,0 @@ -# sandbox-ssh-fix - -Workaround for [anthropics/claude-code#70684](https://github.com/anthropics/claude-code/issues/70684). - -## Detection and fix logic - -The SessionStart hook (`scripts/fix-git-ssh.sh`) runs on every session start and -checks three conditions before acting: - -| Condition | Variable | Check | -|-----------|----------|-------| -| Inside sandbox | `SANDBOX_RUNTIME` | `== 1` | -| Broken proxy | `GIT_SSH_COMMAND` | contains `nc -X 5` | -| Auth required | `ALL_PROXY` | contains `@` | - -When all conditions are true, the script parses `ALL_PROXY` -(`socks5h://user:pass@host:port`) and writes a corrected `GIT_SSH_COMMAND` to -`$CLAUDE_ENV_FILE`. - -The `ncat` path uses `127.0.0.1` instead of `localhost` because ncat resolves -`localhost` differently and the connection is refused. - -SSH options `ControlMaster=no` and `ControlPath=none` are preserved from the -original command because SSH mux sockets aren't in the sandbox's allowed Unix -socket paths. diff --git a/sandbox-ssh-fix/README.md b/sandbox-ssh-fix/README.md index a8ec50e..e31a5a1 100644 --- a/sandbox-ssh-fix/README.md +++ b/sandbox-ssh-fix/README.md @@ -13,19 +13,20 @@ Tracked upstream: ## How it works -A SessionStart hook detects the broken pattern and overrides `GIT_SSH_COMMAND` -via `$CLAUDE_ENV_FILE`: +This plugin ships an `nc` wrapper (`bin/nc`) that intercepts SOCKS5 proxy calls +and delegates to `ncat` with the auth credentials parsed from `ALL_PROXY`. -- If `ncat` is available: uses `ncat --proxy-type socks5 --proxy-auth` with - credentials parsed from `ALL_PROXY` -- If `ncat` is not available: falls back to plain `ssh` (bypasses the proxy, - relies on the sandbox network allowlist) +### Flow -The fix only activates when all conditions are met: +1. SessionStart hook prepends `plugin/bin/` to `PATH` via `CLAUDE_ENV_FILE` +2. Sandbox injects `GIT_SSH_COMMAND` containing `nc -X 5 -x localhost:PORT %h %p` +3. SSH runs ProxyCommand, shell finds `bin/nc` wrapper first via PATH +4. Wrapper checks for the SOCKS5 pattern (`-X 5 -x`), reads `ALL_PROXY` + (available at runtime in sandbox), and delegates to `ncat` with `--proxy-auth` +5. Non-SOCKS5 calls or missing `ncat` fall through to `/usr/bin/nc` -1. Running inside the sandbox (`SANDBOX_RUNTIME=1`) -2. `GIT_SSH_COMMAND` contains the broken `nc -X 5` pattern -3. `ALL_PROXY` contains credentials (has `@`) +The wrapper uses `127.0.0.1` instead of `localhost` because `ncat` resolves +`localhost` differently and the connection is refused. ## Prerequisites @@ -35,8 +36,8 @@ Install `ncat` (from nmap) for full SOCKS5 proxy support: brew install nmap ``` -Without `ncat`, the plugin falls back to direct SSH which still works but -bypasses the sandbox's SOCKS5 proxy. +Without `ncat`, the plugin falls back to the system `nc` which will fail for +authenticated SOCKS5 proxies. ## License diff --git a/sandbox-ssh-fix/bin/nc b/sandbox-ssh-fix/bin/nc new file mode 100755 index 0000000..a1a1eea --- /dev/null +++ b/sandbox-ssh-fix/bin/nc @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +if [[ "$#" -ge 6 ]] \ + && [[ "$1" == "-X" && "$2" == "5" && "$3" == "-x" ]] \ + && [[ "${ALL_PROXY:-}" == *"@"* ]] \ + && command -v ncat >/dev/null 2>&1; then + _auth="${ALL_PROXY#*://}" + _auth="${_auth%@*}" + _proxy="${4/localhost/127.0.0.1}" + exec ncat --proxy-type socks5 --proxy-auth "$_auth" --proxy "$_proxy" "${@: -2}" +fi +exec /usr/bin/nc "$@" diff --git a/sandbox-ssh-fix/scripts/fix-git-ssh.sh b/sandbox-ssh-fix/scripts/fix-git-ssh.sh index c79a12e..6ef0d38 100755 --- a/sandbox-ssh-fix/scripts/fix-git-ssh.sh +++ b/sandbox-ssh-fix/scripts/fix-git-ssh.sh @@ -1,29 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -# Only run inside the Claude Code sandbox -[[ "${SANDBOX_RUNTIME:-}" == "1" ]] || exit 0 - -# Only fix the broken BSD nc SOCKS5 proxy pattern -[[ "${GIT_SSH_COMMAND:-}" == *"nc -X 5"* ]] || exit 0 - -# Only needed when ALL_PROXY has credentials (contains @) -[[ "${ALL_PROXY:-}" == *"@"* ]] || exit 0 - -# CLAUDE_ENV_FILE is required to export the fix [[ -n "${CLAUDE_ENV_FILE:-}" ]] || exit 0 -# Parse ALL_PROXY: socks5h://user:pass@host:port -proxy_auth="${ALL_PROXY#*://}" # user:pass@host:port -proxy_auth="${proxy_auth%@*}" # user:pass -proxy_port="${ALL_PROXY##*:}" # port - -if command -v ncat >/dev/null 2>&1; then - cat >> "${CLAUDE_ENV_FILE}" <> "${CLAUDE_ENV_FILE}" - echo "SessionStart:startup hook success: sandbox-ssh-fix: ncat not found, bypassing SOCKS5 proxy (using direct SSH)" -fi +echo "export PATH=\"${CLAUDE_PLUGIN_ROOT}/bin:\${PATH}\"" >> "${CLAUDE_ENV_FILE}" +echo "SessionStart:startup hook success: sandbox-ssh-fix: added nc wrapper to PATH"