Remote SSH: bundled ssh2 treats IdentityFile as raw privateKey buffer, breaks with 1Password public-key stubs

Resolved 💬 3 comments Opened Apr 26, 2026 by rjlg Closed Apr 26, 2026

Summary

Claude Code's remote SSH client (bundled Node ssh2) reads every IdentityFile resolved by ssh -G and passes the raw file bytes to ssh2.Client.connect as the privateKey option. With 1Password's SSH integration — which exposes only .pub stubs on disk and signs via the agent — those bytes parse as a valid public key but contain no private PEM, so ssh2.Client.connect throws "privateKey value does not contain a (valid) private key". The thrown error string is not "authentication methods failed", which short-circuits ssh2's retry/fallback loop, so the agent is never tried even though it is present and holds the key.

The result is that any Host block whose IdentityFile resolves to a 1Password .pub stub fails to connect via Claude Code remote SSH, even though plain ssh <host> from the same shell works fine.

The buggy wrapper (from the bundled JS)

async function t4e(e) {                       // per-identityFile loader
  const A = await fs.readFile(expand(e));
  if (!A) return;
  const t = ssh2.utils.parseKey(A);
  if (t instanceof Error) return;             // logs "Skipping identity file..."
  return A;                                   // returns the RAW buffer
}
// ...
a.push({ path: Q, buffer: h });
a.length > 0 && (r.privateKey = a[0].buffer); // buffer becomes privateKey

And inside ssh2.Client.connect:

if (parsedKey.getPrivatePEM() === null)
    throw new Error("privateKey value does not contain a (valid) private key");

.pub stubs parse fine (valid ed25519 public keys) but getPrivatePEM() returns null, so connect throws synchronously. Because the message is not "authentication methods failed", the surrounding fallback logic does not retry without privateKey, and the agent code path is never reached.

Why it looks intermittent across hosts

The ssh -G resolution is cached in an in-memory Map for the lifetime of the Claude Code process. Whether a given host hits the bug depends on whether its IdentityFile happened to exist on disk at first-resolution time:

  • If the .pub stub did not exist (e.g. 1Password vault didn't yet contain that key), fs.readFile fails, t4e returns undefined, the identity-files array stays empty, the agent gate !(identitiesOnly && !hasFiles) evaluates truthy, and auth via the agent succeeds.
  • If the .pub stub did exist, the buggy path above fires and connect fails.

So the same config can produce "host A works, host B doesn't" purely based on which key was materialised when. Restarting Claude Code can flip a working host into a broken state, which makes the bug confusing to diagnose.

Reproduction

  1. Configure 1Password's SSH integration so an IdentityFile directive in ~/.ssh/config resolves to a .pub file on disk (the public-only stub 1Password writes).
  2. From a Claude Code session, attempt remote SSH to that host.
  3. Observe connect failure. ssh.log shows keys: 1 and the privateKey value does not contain a (valid) private key rejection; the agent is never queried.
  4. From a plain Terminal, ssh <host> succeeds via the 1Password agent using the same config.

Current workaround

Two settings are required together; either alone is insufficient:

  1. Pre-Include block in ~/.ssh/config:

``
Host <affected-hosts>
IdentitiesOnly no
``

  1. Per-host IdentityFile none at the bottom of each affected Host block (clears the identity-files list that ssh -G emits, since Claude Code's parser honours none as a reset).

Clearing the file list alone disables the agent gate; clearing IdentitiesOnly alone still runs the buggy public-key-as-privateKey path. Both halves are needed, on every machine, for every affected host.

Suggested fix

In the per-identity loader (t4e), treat a parsed-but-public-only key the same as a parse error:

const parsed = ssh2.utils.parseKey(A);
if (parsed instanceof Error) return;
if (parsed.getPrivatePEM && parsed.getPrivatePEM() === null) {
  // log: "Skipping identity file <path>: public key only"
  return;
}
return A;

That single check makes 1Password's .pub-stub layout work with Claude Code remote SSH out of the box, with no user-side ~/.ssh/config changes.

Environment

  • macOS 26.4.1
  • Claude Code 2.1.119 (latest at filing)
  • 1Password 8 with SSH agent + "Match Host" rules pinning per-host keys
  • Affected hosts use ed25519 keys exposed by 1Password as ~/.ssh/1Password/SHA256_<hash>.pub

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗