Skip to content

feat: authenticate key-based hosts through an ssh-agent - #36

Merged
so5 merged 2 commits into
mainfrom
feat/ssh-agent-key-auth
Sep 4, 2026
Merged

so5 merged 2 commits into
mainfrom
feat/ssh-agent-key-auth

Conversation

@so5

@so5 so5 commented Sep 4, 2026

Copy link
Copy Markdown
Owner

What

For key-based auth, authentication now goes through an ssh-agent by default instead of screen-scraping the Enter passphrase for key prompt and replaying the passphrase on every master-connection rebuild. The passphrase is consumed once to ssh-add the key; subsequent connections and every ControlPersist rebuild authenticate non-interactively, so the wrapper no longer holds a replayable secret.

Password auth is unchanged.

Agent acquisition (per host, at connect time)

  1. hostInfo.identityAgent if set;
  2. else ssh-add the key (with a -t lifetime) into $SSH_AUTH_SOCK;
  3. else, on POSIX, spawn one shared per-user ssh-agent at a fixed socket (SSH_CLIENT_WRAPPER_AGENT_DIR / $XDG_RUNTIME_DIR / /run/user/<uid> / /tmp) and use that.

On any failure — or useAgent: false, or win32 with no reachable agent — it silently falls back to the legacy pty path.

API

  • New hostInfo options: useAgent (default true when keyFile set), identityAgent, agentKeyTTL (default 3600).
  • New method dispose(): ssh-add -d this host's key, then disconnect(); never kills the shared agent.
  • New hook hostInfo.reauthRequired (unused by current consumers).
  • lib/index.d.ts: added the above plus previously-missing ControlPersistDir / remoteToRemoteCopy; widened the password/passphrase callback return type to allow a Promise.

Tests

  • New test/agent.js (24 unit tests, fake pty / stubbed child_process).
  • Additions to test/sanityCheck.js, test/getSshOption.js, test/interface.js.
  • New integration cases in test/sshExec.js (the "passphrase consumed once" case self-skips unless TEST_PH is set).
  • npm run lint clean; 108 unit tests pass locally; agent paths verified end-to-end against a real ssh-agent.

BREAKING CHANGE

Hosts configured with a keyFile now authenticate via an ssh-agent by default. The passphrase callback is invoked once by ssh-add rather than once per connection, and a per-user ssh-agent process may be spawned and left running. Set hostInfo.useAgent = false to restore the previous pty passphrase-replay behavior.

🤖 Generated with Claude Code

so5 and others added 2 commits September 4, 2026 10:23
When keyFile is set, key-based authentication now goes through an
ssh-agent by default instead of screen-scraping the passphrase prompt
and replaying it on every master-connection rebuild. The passphrase is
consumed once to ssh-add the key; subsequent connections and every
ControlPersist rebuild then authenticate non-interactively, so the
wrapper no longer needs a replayable secret.

Agent acquisition per host, at connect time:
  1. use hostInfo.identityAgent if set;
  2. else ssh-add the key (with a -t lifetime) into $SSH_AUTH_SOCK;
  3. else, on POSIX, spawn one shared per-user ssh-agent at a fixed
     socket (SSH_CLIENT_WRAPPER_AGENT_DIR / $XDG_RUNTIME_DIR /
     /run/user/<uid> / /tmp) and use that.
On any failure - or when useAgent is false, or on win32 with no
reachable agent - it silently falls back to the legacy pty path.

New hostInfo options: useAgent (default true when keyFile set),
identityAgent, agentKeyTTL (default 3600). New method: dispose()
(ssh-add -d this host's key, then disconnect; never kills the shared
agent). New hook: hostInfo.reauthRequired (unused by current consumers).

lib/index.d.ts: added the new options/method plus previously-missing
ControlPersistDir and remoteToRemoteCopy, and widened the password /
passphrase callback return type to allow a Promise.

BREAKING CHANGE: hosts configured with a keyFile now authenticate via
an ssh-agent by default. The passphrase callback is invoked once by
ssh-add rather than once per connection, and a per-user ssh-agent
process may be spawned and left running. Set hostInfo.useAgent = false
to restore the previous pty passphrase-replay behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAC2AN9SHN1KYSt5b8G7zc
lib:
- use node: prefix on builtin imports (agent.js)
- extract resolveAgentCandidate() / effectiveTtl() to cut
  ensureAgentForHost() cognitive complexity below the threshold
- optional chaining in probeAgent(); single Array#push in
  resolveWellKnownDir(); String.raw for the identityAgent schema regex
- hoist the default deps object out of the constructor default param

test:
- node: prefix on builtin imports
- replace /tmp string literals (flagged as publicly-writable-dir use)
  with /run paths in stub-only assertions
- drop the ssh-add -l cross-check in the integration test (avoided a
  PATH-resolved binary call); managedAgentSock assertion is enough
- socketIsTrusted negative test uses a group-readable (0o640) file
  instead of a world-writable one

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAC2AN9SHN1KYSt5b8G7zc
@sonarqubecloud

sonarqubecloud Bot commented Sep 4, 2026

Copy link
Copy Markdown

@so5
so5 merged commit e744425 into main Sep 4, 2026
8 checks passed
@so5
so5 deleted the feat/ssh-agent-key-auth branch September 4, 2026 01:51
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 3.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant