Remote SSH: bundled ssh2 treats IdentityFile as raw privateKey buffer, breaks with 1Password public-key stubs
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
.pubstub did not exist (e.g. 1Password vault didn't yet contain that key),fs.readFilefails,t4ereturnsundefined, the identity-files array stays empty, the agent gate!(identitiesOnly && !hasFiles)evaluates truthy, and auth via the agent succeeds. - If the
.pubstub 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
- Configure 1Password's SSH integration so an
IdentityFiledirective in~/.ssh/configresolves to a.pubfile on disk (the public-only stub 1Password writes). - From a Claude Code session, attempt remote SSH to that host.
- Observe connect failure. ssh.log shows
keys: 1and theprivateKey value does not contain a (valid) private keyrejection; the agent is never queried. - 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:
- Pre-Include block in
~/.ssh/config:
````
Host <affected-hosts>
IdentitiesOnly no
- Per-host
IdentityFile noneat the bottom of each affectedHostblock (clears the identity-files list thatssh -Gemits, since Claude Code's parser honoursnoneas 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
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗