You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When an SSH connection is set to agent authentication and the user's ~/.ssh/config resolution for that host includes IdentitiesOnly yes together with an IdentityFile, the connection always fails with All configured authentication methods failed even though the agent is reachable and holds exactly the right key.
Root cause (in apps/emdash-desktop/src/main/core/ssh/connect/ssh-connect-auth.ts): when the IdentitiesOnly branch fires, buildAuthConfig passes config.agent = new IdentityFilteredAgent(...). That class is declared as
classIdentityFilteredAgentimplementsBaseAgent{
implements is type-level only - there's no prototype link to ssh2's BaseAgent. ssh2's client config validation accepts an agent only as a string path or via isAgent(val), which is literally val instanceof BaseAgent (ssh2 lib/client.js ~L219–224, lib/agent.jsisAgent). The instanceof check fails, so ssh2 silently sets this.config.agent = undefined and connects with no auth methods at all: it sends the none probe, receives USERAUTH_FAILURE (publickey,password), and disconnects without ever offering a key.
The identity-filtering logic itself is correct. Verified getIdentities filters to exactly the expected key when called directly. The only problem is that ssh2 never accepts the wrapper object.
Suggested fix: make the class extends BaseAgent, which needs three small edits as below (tested locally: with this change ssh2 accepts the filtered agent and auth succeeds):
Note this failure mode is easy to hit: IdentitiesOnly yes under Host * was part of an ssh-config hardening pattern suggested to me, and any user who has it gets a guaranteed auth failure in agent mode.
Steps to Reproduce
Have a working ssh-agent with a key that authenticates to some host.
In ~/.ssh/config, have the host match a block containing both directives, e.g.:
In Emdash, create an SSH connection to that host with authentication = SSH agent.
Connect (or use Test connection).
Removing IdentitiesOnly yes (or overriding it to no for that host) makes the same connection succeed immediately. That's the workaround I'm using.
Actual vs Expected Behavior
Actual: connection fails with All configured authentication methods failed. The ssh2 debug log shows the handshake completing and then no publickey attempt at all:
Expected: the agent key matching the IdentityFile is offered and authentication succeeds.
Verified directly against ssh2 1.17.0: passing a wrapper object of this shape as agent fails with the same error; passing the raw socket path to the identical connect() call succeeds. Happy to share the standalone repro script if useful.
emdash Version
1.1.39
Operating System
macOS 26.5.2
Additional Context
The bundled ssh2 is 1.17.0; its isAgent() is val instanceof BaseAgent, so any agent object that merely has the right methods, without actually inheriting from BaseAgent, is silently discarded during Client.connect config normalization. No error is surfaced, which is why the resulting message points users at their credentials instead of the real cause.
Bug Description
When an SSH connection is set to agent authentication and the user's
~/.ssh/configresolution for that host includesIdentitiesOnly yestogether with anIdentityFile, the connection always fails withAll configured authentication methods failedeven though the agent is reachable and holds exactly the right key.Root cause (in
apps/emdash-desktop/src/main/core/ssh/connect/ssh-connect-auth.ts): when theIdentitiesOnlybranch fires,buildAuthConfigpassesconfig.agent = new IdentityFilteredAgent(...). That class is declared asimplementsis type-level only - there's no prototype link to ssh2'sBaseAgent. ssh2's client config validation accepts an agent only as a string path or viaisAgent(val), which is literallyval instanceof BaseAgent(ssh2lib/client.js~L219–224,lib/agent.jsisAgent). The instanceof check fails, so ssh2 silently setsthis.config.agent = undefinedand connects with no auth methods at all: it sends thenoneprobe, receivesUSERAUTH_FAILURE (publickey,password), and disconnects without ever offering a key.The identity-filtering logic itself is correct. Verified
getIdentitiesfilters to exactly the expected key when called directly. The only problem is that ssh2 never accepts the wrapper object.Suggested fix: make the class
extends BaseAgent, which needs three small edits as below (tested locally: with this change ssh2 accepts the filtered agent and auth succeeds):import ssh2, { - type BaseAgent, + BaseAgent, type ConnectConfig, ... } from 'ssh2'; -class IdentityFilteredAgent implements BaseAgent { +class IdentityFilteredAgent extends BaseAgent { readonly kind = 'identity-filtered-agent'; declare getStream?: BaseAgent['getStream']; constructor( readonly socketPath: string, private readonly agent: BaseAgent, private readonly allowedKeys: ParsedKey[] ) { + super(); if (agent.getStream) { this.getStream = agent.getStream.bind(agent); } }Note this failure mode is easy to hit:
IdentitiesOnly yesunderHost *was part of an ssh-config hardening pattern suggested to me, and any user who has it gets a guaranteed auth failure in agent mode.Steps to Reproduce
Have a working ssh-agent with a key that authenticates to some host.
In
~/.ssh/config, have the host match a block containing both directives, e.g.:In Emdash, create an SSH connection to that host with authentication = SSH agent.
Connect (or use Test connection).
Removing
IdentitiesOnly yes(or overriding it tonofor that host) makes the same connection succeed immediately. That's the workaround I'm using.Actual vs Expected Behavior
Actual: connection fails with
All configured authentication methods failed. The ssh2 debug log shows the handshake completing and then no publickey attempt at all:Expected: the agent key matching the
IdentityFileis offered and authentication succeeds.Verified directly against ssh2 1.17.0: passing a wrapper object of this shape as
agentfails with the same error; passing the raw socket path to the identicalconnect()call succeeds. Happy to share the standalone repro script if useful.emdash Version
1.1.39
Operating System
macOS 26.5.2
Additional Context
isAgent()isval instanceof BaseAgent, so any agent object that merely has the right methods, without actually inheriting fromBaseAgent, is silently discarded duringClient.connectconfig normalization. No error is surfaced, which is why the resulting message points users at their credentials instead of the real cause.extends BaseAgentfix if useful.