diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 1e2fd8d..2cfb93c 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -32,6 +32,11 @@ "name": "rh-dataverse", "source": "./rh-dataverse", "strict": true + }, + { + "name": "sandbox-ssh-fix", + "source": "./sandbox-ssh-fix", + "strict": true } ] } diff --git a/README.md b/README.md index 87a683d..9a70e0c 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ claude plugin marketplace add cblecker/claude-plugins | [pr-review-toolkit](./pr-review-toolkit) | Comprehensive PR review board using shared workflow context | | [gws](./gws) | Google Workspace CLI skills for Gmail, Calendar, Drive, Docs, Sheets, Slides, and Meet | | [rh-dataverse](./rh-dataverse) | Red Hat Dataverse MCP server | +| [sandbox-ssh-fix](./sandbox-ssh-fix) | Fixes git-over-SSH in the Claude Code sandbox on macOS by replacing the broken BSD nc SOCKS5 proxy | ## License diff --git a/sandbox-ssh-fix/.claude-plugin/plugin.json b/sandbox-ssh-fix/.claude-plugin/plugin.json new file mode 100644 index 0000000..50f9598 --- /dev/null +++ b/sandbox-ssh-fix/.claude-plugin/plugin.json @@ -0,0 +1,11 @@ +{ + "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", + "author": { + "name": "cblecker", + "email": "admin@toph.ca" + }, + "keywords": ["sandbox", "ssh", "git", "macos", "workaround"], + "homepage": "https://github.com/anthropics/claude-code/issues/70684" +} diff --git a/sandbox-ssh-fix/CLAUDE.md b/sandbox-ssh-fix/CLAUDE.md new file mode 100644 index 0000000..8c7bac0 --- /dev/null +++ b/sandbox-ssh-fix/CLAUDE.md @@ -0,0 +1,25 @@ +# 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 new file mode 100644 index 0000000..a8ec50e --- /dev/null +++ b/sandbox-ssh-fix/README.md @@ -0,0 +1,43 @@ +# sandbox-ssh-fix + +Workaround for broken git-over-SSH in the Claude Code sandbox on macOS. + +## Problem + +The Claude Code sandbox injects `GIT_SSH_COMMAND` with a `ProxyCommand` using +BSD `nc -X 5` for SOCKS5 proxying. On macOS, `nc` doesn't support SOCKS5 +authentication, and the proxy requires auth — breaking all git-over-SSH +operations (`git fetch`, `git push`, `git pull`). + +Tracked upstream: + +## How it works + +A SessionStart hook detects the broken pattern and overrides `GIT_SSH_COMMAND` +via `$CLAUDE_ENV_FILE`: + +- 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) + +The fix only activates when all conditions are met: + +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 `@`) + +## Prerequisites + +Install `ncat` (from nmap) for full SOCKS5 proxy support: + +```bash +brew install nmap +``` + +Without `ncat`, the plugin falls back to direct SSH which still works but +bypasses the sandbox's SOCKS5 proxy. + +## License + +Apache License 2.0. See [LICENSE](../LICENSE) for details. diff --git a/sandbox-ssh-fix/hooks/hooks.json b/sandbox-ssh-fix/hooks/hooks.json new file mode 100644 index 0000000..3e74881 --- /dev/null +++ b/sandbox-ssh-fix/hooks/hooks.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/fix-git-ssh.sh" + } + ] + } + ] + } +} diff --git a/sandbox-ssh-fix/scripts/fix-git-ssh.sh b/sandbox-ssh-fix/scripts/fix-git-ssh.sh new file mode 100755 index 0000000..c79a12e --- /dev/null +++ b/sandbox-ssh-fix/scripts/fix-git-ssh.sh @@ -0,0 +1,29 @@ +#!/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