Claude Desktop hard-codes SSH_AUTH_SOCK from "ssh -G ccd-identityagent-probe", breaking host-conditional IdentityAgent configs
Summary
Claude Desktop determines the terminal's SSH_AUTH_SOCK by running ssh -G ccd-identityagent-probe — a synthetic, hard-coded hostname — and adopting whatever IdentityAgent that resolves to. For users whose ~/.ssh/config sets IdentityAgent conditionally per host, the synthetic probe hostname never matches any real Host/Match block and always falls through to the catch-all (Match final all). As a result, Claude Desktop hard-codes SSH_AUTH_SOCK to the catch-all agent and overrides the correct value inherited from the OS (launchctl getenv SSH_AUTH_SOCK).
This silently routes SSH (and git-over-SSH) inside Claude through the wrong agent.
Environment
- macOS 15.6 (Darwin 24.6.0), Apple Silicon
- Claude Desktop
1.12603.1 - Bundled
claude-code2.1.170(the version actually launched by the desktop app, not the CLI onPATH) - 1Password SSH agent used as a conditional fallback only
A representative SSH config
The key property is that IdentityAgent is host-dependent: some hosts use the inherited system agent, and a catch-all sends everything else to a different agent.
# Corporate/managed hosts: use whatever agent the OS/login session provides
Match host "*.corp.example.com"
IdentityAgent SSH_AUTH_SOCK
# Everything else: fall through to 1Password
Match final all
IdentityAgent ~/.1password/agent.sock
Steps to reproduce
- Use the SSH config above (or any config where
IdentityAgentdiffers by host and the catch-all points to a concrete socket path). - Confirm the OS-level value is correct, e.g. the system ssh-agent:
````
$ launchctl getenv SSH_AUTH_SOCK
/private/tmp/com.apple.launchd.XXXXXX/Listeners
- Launch Claude Desktop normally (from the Dock/Finder).
- In a Claude terminal/Bash tool, run:
````
$ echo "$SSH_AUTH_SOCK"
/Users/<user>/.1password/agent.sock # ← wrong: the catch-all agent
- Reproduce the probe the app runs:
```
$ /usr/bin/ssh -G ccd-identityagent-probe | grep -i identityagent
identityagent /Users/<user>/.1password/agent.sock
`
The desktop log confirms it:
``
[CCD] Resolved SSH IdentityAgent via ssh -G: /Users/<user>/.1password/agent.sock
Expected vs. actual
- Expected: the terminal inherits the OS-provided
SSH_AUTH_SOCK(the system agent), so per-hostIdentityAgentdirectives in the user's config resolve correctly when an actualsshcommand runs (with its real hostname). - Actual:
SSH_AUTH_SOCKis hard-set to the catch-all agent for every session, so hosts that were configured to use the inherited agent (IdentityAgent SSH_AUTH_SOCK) instead silently use the catch-all agent.
Root cause
In Claude.app/Contents/Resources/app.asar, the terminal-environment builder resolves SSH_AUTH_SOCK via a probe with a synthetic hostname (ccd-identityagent-probe) and uses the resulting IdentityAgent:
const VWr = "ccd-identityagent-probe";
const $Wr = new Set(["SSH_AUTH_SOCK", "none"]); // values to ignore
async function WWr() {
const ssh = process.platform === "darwin" ? "/usr/bin/ssh" : "ssh";
const { stdout } = await es(ssh, ["-G", VWr], { timeout: 5000 });
for (const line of stdout.split("\n"))
if (line.startsWith("identityagent ")) {
let t = line.slice(14);
if ($Wr.has(t)) return null; // literal "SSH_AUTH_SOCK"/"none" → ignore
const m = /^\$([A-Za-z_]\w*)$/.exec(t); // $VAR form → expand
if (m) { t = process.env[m[1]]; if (!t) return null; }
return t; // ← becomes SSH_AUTH_SOCK
}
return null;
}
// jWr() stats WWr() for isSocket(); YSA() returns it, else falls back to the
// login-shell SSH_AUTH_SOCK (skipping it if it equals the already-inherited value).
The flawed assumption is that IdentityAgent is a single global value. A synthetic hostname can never match real Host/Match rules, so on any host-conditional config it deterministically resolves to the Match final all (catch-all) agent. There is no real connection target here — this probe runs purely to populate the terminal env — so adopting the catch-all value is incorrect whenever the config is host-dependent.
Suggested fixes (any one would resolve it)
- Don't override an already-correct inherited value. If
WWr()'s probe result differs from the inheritedprocess.env.SSH_AUTH_SOCKand the config is host-conditional, prefer the inherited value rather than latching the catch-all. - Detect host-conditional
IdentityAgent. IfIdentityAgentresolution varies across hostnames (or appears under aMatch/Hostblock rather than as a global default), treat it as "no single global agent" and fall back to the inheritedSSH_AUTH_SOCK. - Make the probe opt-out-able / documented. At minimum, document the
ccd-identityagent-probebehavior and expose a setting to disable it, so users aren't forced to add aHost ccd-identityagent-probeblock to their SSH config (a fragile workaround that depends on the internal probe name remaining stable).
Current workaround
Add a block early in ~/.ssh/config so the probe resolves to the ignore-listed literal:
Host ccd-identityagent-probe
IdentityAgent SSH_AUTH_SOCK
This makes WWr() return null (since SSH_AUTH_SOCK is in the ignore set), so Claude Desktop falls back to the inherited value. It works, but it relies on an undocumented internal hostname.
Related
- #29717 describes the inverse symptom (SSH_AUTH_SOCK not passed through) on an older build, in the same
SSH_AUTH_SOCK/ shell-extraction subsystem. This report is about a newer build that added thessh -GIdentityAgentprobe as the primary resolver.