Claude Desktop hard-codes SSH_AUTH_SOCK from "ssh -G ccd-identityagent-probe", breaking host-conditional IdentityAgent configs

Open 💬 0 comments Opened Jun 12, 2026 by ericnorris

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-code 2.1.170 (the version actually launched by the desktop app, not the CLI on PATH)
  • 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

  1. Use the SSH config above (or any config where IdentityAgent differs by host and the catch-all points to a concrete socket path).
  2. 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
``

  1. Launch Claude Desktop normally (from the Dock/Finder).
  2. In a Claude terminal/Bash tool, run:

``
$ echo "$SSH_AUTH_SOCK"
/Users/<user>/.1password/agent.sock # ← wrong: the catch-all agent
``

  1. 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-host IdentityAgent directives in the user's config resolve correctly when an actual ssh command runs (with its real hostname).
  • Actual: SSH_AUTH_SOCK is 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)

  1. Don't override an already-correct inherited value. If WWr()'s probe result differs from the inherited process.env.SSH_AUTH_SOCK and the config is host-conditional, prefer the inherited value rather than latching the catch-all.
  2. Detect host-conditional IdentityAgent. If IdentityAgent resolution varies across hostnames (or appears under a Match/Host block rather than as a global default), treat it as "no single global agent" and fall back to the inherited SSH_AUTH_SOCK.
  3. Make the probe opt-out-able / documented. At minimum, document the ccd-identityagent-probe behavior and expose a setting to disable it, so users aren't forced to add a Host ccd-identityagent-probe block 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 the ssh -G IdentityAgent probe as the primary resolver.

View original on GitHub ↗