Table of Contents

Migrating from SSH.NET

This guide maps SSH.NET code to SshSharp. Before you start, check that SshSharp covers what you need (see "Why not SSH.NET?" in the README): it targets .NET 8 and later only, and it does not speak legacy algorithms (CBC ciphers, SHA-1 MACs, ssh-rsa signatures, DH group 1).

The main differences:

  • Async only. Every network operation is async and takes a CancellationToken. Clients, sessions and forwarders are IAsyncDisposable, so use await using.
  • Connecting is one call. SshClient.ConnectAsync returns a client that is already connected and authenticated. There is no separate constructor, Connect or Disconnect; disposing the client disconnects.
  • Options are immutable. SshClientOptions uses init properties, set once. It replaces ConnectionInfo, the authentication method objects and the client properties.
  • Host keys are always verified. HostKeyVerifier is required. There is no "accept anything" default and no event to forget to handle.
  • One connection for everything. Commands, shells, port forwarding and SCP all run over the same SshClient. SSH.NET's ScpClient opens its own connection.

What you gain

Features that SSH.NET users have asked for in its issue tracker, and that SshSharp has:

Request SshSharp
Verify host keys against known_hosts KnownHostsVerifier, with hashed hosts, @cert-authority, @revoked and StrictHostKeyChecking (see Host keys)
Jump hosts / bastions ProxyJump chains and ProxyCommand from ssh_config, or by hand: OpenTcpTunnelAsync to the next host and SshClient.ConnectAsync(stream, ...) over it (see Connecting)
Use the keys in ssh-agent; agent forwarding SshAgent and ForwardAgent, on Unix sockets, the Windows OpenSSH agent and PuTTY's Pageant (SshAgent.FromPageant())
Add, remove and lock agent keys (ssh-add) agent.AddKeyAsync(key, constraints) (lifetime, confirmation), RemoveKeyAsync, RemoveAllKeysAsync, LockAsync and UnlockAsync; AddKeysToAgent in ssh_config (see Authentication)
A subsystem other than sftp client.StartSubsystemAsync(name)
A terminal for a single command (ssh -t host cmd) StartCommandAsync(command, new SshSessionOptions { Terminal = ... })
Send a signal to a command session.SendSignalAsync("TERM")
Forward to a Unix socket client.OpenUnixSocketTunnelAsync(path); the other way, the server's socket to this machine, with client.ForwardRemoteUnixSocketAsync(remotePath, ...)
A SOCKS proxy on the server's side (ssh -R port) client.ForwardRemoteDynamicPortAsync(bindAddress, port, allowDestination)
HttpClient through the connection client.CreateHttpConnectCallback() for SocketsHttpHandler.ConnectCallback
Fetch host keys without logging in (ssh-keyscan) SshClient.ScanHostKeysAsync(host, port)
Generate keys (ssh-keygen -t) SshPrivateKey.Generate(type) and SaveAsync(path, passphrase)
Sign and verify files or git commits (ssh-keygen -Y) SshSignature and SshAllowedSigners (see Signing)
Connect from a chosen local address or interface SshClientOptions.LocalEndPoint, BindAddress and BindInterface in ssh_config
Agent keys limited to some hosts (ssh-add -h) adding them: SshAgentKeyConstraints.Destinations (restrict-destination-v00@openssh.com); using them: session binding and publickey-hostbound, automatically
Send a BREAK to a serial console session.SendBreakAsync() (RFC 4335, as OpenSSH's ~B)
Revoked host keys KnownHostsVerifier.RevokedHostKeys (RevokedHostKeys in ssh_config): OpenSSH KRLs from ssh-keygen -k, or key lists
Measure the round trip client.PingAsync() (ping@openssh.com, or a keepalive@openssh.com request on older servers)
Close idle channels SshClientOptions.ChannelTimeouts (ChannelTimeout in ssh_config, as OpenSSH 9.2+)
Faster SFTP downloads over high-latency links SftpTransferOptions.Segments: one file in parallel ranges, each over its own SFTP session (see SFTP)
Kerberos / Active Directory sign-in (GSSAPI) SshClientOptions.Gssapi, or GSSAPIAuthentication yes in ssh_config (.NET 10; see Authentication)
Remote files as an IFileSystem or IFileProvider the SshSharp.IO.Abstractions and SshSharp.FileProviders packages
Detect a stalled connection, also while a write is blocked keep-alive (KeepAliveInterval, KeepAliveCountMax): the probes don't wait for pending writes, so a server that stops reading is still detected
RSA SHA-2 signatures with servers that announce them server-sig-algs (RFC 8308) is used to pick rsa-sha2-512 or rsa-sha2-256
FIPS-approved algorithms only SshClientOptions.FipsMode, including post-quantum key exchange on .NET 10
Cancellation everywhere a CancellationToken on every network operation

Connecting

SSH.NET:

using var client = new SshClient("example.com", 22, "deploy", password);
client.HostKeyReceived += (_, e) => e.CanTrust = e.FingerPrintSHA256 == expectedFingerprint;
client.Connect();

SshSharp:

await using var client = await SshClient.ConnectAsync("example.com", 22, new SshClientOptions
{
    UserName = "deploy",
    Password = password,
    HostKeyVerifier = SshHostKeyVerifier.Pinned("SHA256:" + expectedFingerprint),
});

FingerPrintSHA256 in SSH.NET is the base64 hash without a prefix. SshSharp fingerprints are written as ssh-keygen -l prints them, with the SHA256: prefix.

Or connect the way ssh does, with ~/.ssh/config, known_hosts, the user's keys and the agent:

await using var client = await SshClient.ConnectAsync("deploy@example.com");

See Connecting for streams, jump hosts and timeouts.

Connection settings

SSH.NET SshSharp
new SshClient(host, port, user, password) SshClient.ConnectAsync(host, port, new SshClientOptions { UserName, Password, HostKeyVerifier })
new SshClient(host, user, keyFiles) SshClientOptions.PrivateKeys (see Keys)
new SshClient(connectionInfo) SshClientOptions; or ssh_config with SshClient.ConnectAsync(destination)
client.Connect(), ConnectAsync(ct) SshClient.ConnectAsync(...): returns a connected client
client.Disconnect(), Dispose() await client.DisposeAsync()
ConnectionInfo.Timeout a CancellationToken (for example new CancellationTokenSource(TimeSpan.FromSeconds(10))), or ConnectTimeout in ssh_config
client.KeepAliveInterval SshClientOptions.KeepAliveInterval and KeepAliveCountMax (ServerAliveInterval in ssh_config)
ConnectionInfo.Encryptions / KeyExchangeAlgorithms / HostKeyAlgorithms (removing entries) Ciphers, KexAlgorithms, HostKeyAlgorithms in ssh_config (the defaults are modern only; SshClientOptions.FipsMode restricts them to FIPS algorithms)
ConnectionInfo.ProxyType, ProxyHost, ProxyPort, ProxyUsername, ProxyPassword SshClientOptions.Proxy = SshProxy.Http(host, port, credentials) (or Socks5, Socks4, or SshProxy.FromUri(new Uri("socks5://proxy:1080")))
client.IsConnected client.IsConnected; client.ConnectionClosed (a CancellationToken) fires when the connection drops
client.ErrorOccurred event client.ConnectionClosed and client.ConnectionError
client.ConnectionInfo.ServerVersion client.ServerIdentification

Host keys

SSH.NET SshSharp
HostKeyReceived with e.CanTrust = e.FingerPrintSHA256 == ... SshHostKeyVerifier.Pinned("SHA256:...")
HostKeyReceived with your own logic SshHostKeyVerifier.FromDelegate((context, ct) => ...); context.HostKey has the key, type and fingerprint
(no handler: every key is accepted) no equivalent: a verifier is required
(no known_hosts support) KnownHostsVerifier.ForCurrentUser(StrictHostKeyChecking.Yes)

A rejected key throws SshHostKeyException, which carries the key the server presented. See Host keys.

Authentication

SSH.NET SshSharp
PasswordAuthenticationMethod(user, password) SshClientOptions.Password
PrivateKeyAuthenticationMethod(user, keyFiles) SshClientOptions.PrivateKeys
KeyboardInteractiveAuthenticationMethod and its AuthenticationPrompt event SshClientOptions.KeyboardInteractive, a callback that returns the answers
several methods in ConnectionInfo set several options: the client tries public keys, keyboard-interactive, then password, as ssh does, and follows the server's multi-factor chains
(no agent support) SshAgent.FromEnvironment() and GetKeysAsync(); ForwardAgent for agent forwarding
SshAuthenticationException SshAuthenticationException, with AllowedMethods listing what the server would accept

When the server takes passwords only through keyboard-interactive (PAM with PasswordAuthentication no), Password answers that prompt, so you need no handler for it.

Keys

SSH.NET SshSharp
new PrivateKeyFile(path), new PrivateKeyFile(path, passphrase) await SshPrivateKey.LoadAsync(path, passphrase)
new PrivateKeyFile(stream) SshPrivateKey.Parse(text, passphrase)
OpenSSH, PKCS#1, PKCS#8, PuTTY .ppk the same, plus SEC1; ssh.com keys are not read (convert them with ssh-keygen -i)
ssh-rsa (SHA-1) signatures RSA keys sign with rsa-sha2-256 or rsa-sha2-512 only
(no key generation) SshPrivateKey.Generate(SshKeyType.Ed25519), then SaveAsync(path, passphrase) (see Authentication)

SshPrivateKey is IDisposable: disposing it clears the key material.

Commands

SSH.NET:

var command = client.RunCommand("df -h /");
Console.WriteLine($"exit {command.ExitStatus}: {command.Result}");

SshSharp:

var result = await client.RunCommandAsync("df -h /");
Console.WriteLine($"exit {result.ExitStatus}: {result.StandardOutputText}");
SSH.NET SshSharp
client.RunCommand(text), command.ExecuteAsync(ct) await client.RunCommandAsync(text, ct)
command.Result, command.Error result.StandardOutputText, result.StandardErrorText (or the raw bytes: StandardOutput, StandardError)
command.ExitStatus, command.ExitSignal result.ExitStatus, result.ExitSignal?.Name
command.OutputStream, ExtendedOutputStream, CreateInputStream() await client.StartCommandAsync(text), then session.StandardOutput, StandardError, StandardInput
command.CancelAsync() cancel the token, or session.SendSignalAsync("TERM")
command.CommandTimeout a CancellationToken

RunCommandAsync collects all output in memory, up to SshClientOptions.MaxCommandOutputBytes (a safety limit). For large or endless output, stream it:

await using var session = await client.StartCommandAsync("tail -f /var/log/syslog");
using var reader = new StreamReader(session.StandardOutput);
while (await reader.ReadLineAsync(cancellationToken) is { } line)
{
    Console.WriteLine(line);
}

Shells

SSH.NET:

using var shell = client.CreateShellStream("xterm", 80, 24, 0, 0, 4096);
shell.WriteLine("uptime");
var line = shell.ReadLine();

SshSharp offers three ways, depending on what the shell is for.

Option 1: one command, no shell. Often the shell only runs a command and reads its output. A command needs no terminal, so there is no echo or prompt to skip, and you get the exit status too:

var result = await client.RunCommandAsync("uptime");
var line = result.StandardOutputText.TrimEnd('\n');

Option 2: an interactive shell, line by line, with SshExpect. It decodes the output and keeps a buffer, so lines, prompts and partial reads mix safely:

await using var shell = await client.StartShellAsync(new SshSessionOptions
{
    Terminal = new SshTerminalOptions { TerminalType = "xterm", Columns = 80, Rows = 24 },
});
var io = new SshExpect(shell);
await io.SendLineAsync("uptime");
var echo = await io.ReadLineAsync(); // the terminal echoes the command first, as with ShellStream
var line = await io.ReadLineAsync();

A prompt ends without a line feed, so wait for it with ExpectAsync (below) rather than ReadLineAsync, which would wait for the line to end.

Option 3: the session's streams, for full control. StandardInput and StandardOutput are plain streams, for your own reader and writer, or to copy to a file or the console:

await using var shell = await client.StartShellAsync(new SshSessionOptions
{
    Terminal = new SshTerminalOptions { TerminalType = "xterm", Columns = 80, Rows = 24 },
});
var input = new StreamWriter(shell.StandardInput) { AutoFlush = true };
var output = new StreamReader(shell.StandardOutput);
await input.WriteLineAsync("uptime");
var line = await output.ReadLineAsync();

Use one way per session: SshExpect and a StreamReader both buffer what they read, so reading the same output with both loses data.

For ShellStream.Expect, wrap the session in SshExpect:

SSH.NET SshSharp
shell.Expect("$ ", timeout) await expect.ExpectAsync("$ ", token): returns Before and Value; throws EndOfStreamException when the output ends first
shell.Expect(new ExpectAction(regex, action)) var match = await expect.ExpectAsync(regex, token), then act on match.Match.Groups
shell.WriteLine(text) await expect.SendLineAsync(text, token)

Create it with var expect = new SshExpect(shell). Resize with shell.ResizeTerminalAsync(columns, rows). See Commands and shells.

Port forwarding

SSH.NET:

var forward = new ForwardedPortLocal("127.0.0.1", 15432, "db.internal", 5432);
client.AddForwardedPort(forward);
forward.Start();

SshSharp:

await using var forward = await client.ForwardLocalPortAsync(new IPEndPoint(IPAddress.Loopback, 15432), "db.internal", 5432);
SSH.NET SshSharp
ForwardedPortLocal (ssh -L) client.ForwardLocalPortAsync(localEndPoint, remoteHost, remotePort)
ForwardedPortRemote (ssh -R) client.ForwardRemotePortAsync(bindAddress, remotePort, localHost, localPort)
(no remote dynamic forwarding) client.ForwardRemoteDynamicPortAsync(bindAddress, remotePort, allowDestination) (ssh -R port)
ForwardedPortDynamic (ssh -D) client.ForwardDynamicPortAsync(localEndPoint)
forward.Start(), Stop() started when returned; stopped by DisposeAsync
(no remote Unix-socket forwarding) client.ForwardRemoteUnixSocketAsync(remoteSocketPath, localHost, localPort) or (remoteSocketPath, localSocketPath) (ssh -R /socket:target)
client.ForwardedPorts client.LocalForwardings, RemoteForwardings, RemoteSocketForwardings, DynamicForwardings (the ones started from ssh_config)
(none) client.OpenTcpTunnelAsync(host, port) and OpenUnixSocketTunnelAsync(path): a Stream to the far end, without a local listener

With ssh_config, LocalForward, RemoteForward and DynamicForward start with the connection when SshConfigConnectOptions.StartForwardings is set.

SFTP

SSH.NET:

using var sftp = new SftpClient("files.example.com", "deploy", password);
sftp.Connect();
using var file = File.OpenRead("report.pdf");
sftp.UploadFile(file, "incoming/report.pdf");

SshSharp (see File transfer (SFTP)):

using SshSharp.Sftp;

await using var sftp = await client.OpenSftpAsync();
await sftp.UploadFileAsync("report.pdf", "incoming/report.pdf");
SSH.NET SshSharp (using SshSharp.Sftp;)
new SftpClient(...), Connect() await client.OpenSftpAsync() on a connected SshClient (one connection for SFTP, commands and forwarding)
UploadFile(stream, path), UploadFileAsync UploadAsync(stream, path), or UploadFileAsync(localPath, path)
DownloadFile(path, stream), DownloadFileAsync DownloadAsync(path, stream), or DownloadFileAsync(path, localPath)
ListDirectory(path), ListDirectoryAsync ListDirectoryAsync(path): IAsyncEnumerable<SftpFileInfo>, without . and ..
Exists, Get, GetAttributes ExistsAsync, GetAttributesAsync
CreateDirectory, DeleteDirectory, DeleteFile, Delete CreateDirectoryAsync, DeleteDirectoryAsync, DeleteFileAsync
RenameFile(old, new, isPosix) RenameAsync(old, new, overwrite)
Open, OpenRead, OpenWrite, Create, AppendText OpenAsync(path, FileMode, FileAccess), OpenReadAsync, OpenWriteAsync
ChangePermissions, SetLastWriteTime, SetAttributes SetAttributesAsync(path, new SftpFileAttributes { Permissions = ..., LastWriteTime = ... })
SymbolicLink, ReadLink-style calls CreateSymbolicLinkAsync, ReadSymbolicLinkAsync; also CreateHardLinkAsync
WorkingDirectory, ChangeDirectory none: pass full paths, or paths relative to the login directory (GetHomeDirectoryAsync)
SftpPathNotFoundException, SftpPermissionDeniedException SftpException with Status NoSuchFile, PermissionDenied (and Path)
upload/download callbacks SftpTransferOptions.Progress
BufferSize, MaxPendingReads taken from the server's limits; SftpTransferOptions.MaxRequestsInFlight

SCP

SSH.NET:

using var scp = new ScpClient("example.com", "deploy", password);
scp.Connect();
scp.Upload(new FileInfo("site.tar.gz"), "/tmp/site.tar.gz");

SshSharp:

using SshSharp.Scp;

await client.UploadFileAsync("site.tar.gz", "/tmp/site.tar.gz");
SSH.NET SshSharp (using SshSharp.Scp;)
scp.Upload(Stream, path) client.UploadAsync(stream, remotePath)
scp.Upload(FileInfo, path) client.UploadFileAsync(localPath, remotePath)
scp.Upload(DirectoryInfo, path) client.UploadDirectoryAsync(localDirectory, remotePath)
scp.Download(path, Stream) client.DownloadAsync(remotePath, stream)
scp.Download(path, FileInfo) / DirectoryInfo client.DownloadFileAsync(...) / DownloadDirectoryAsync(...)
Uploading / Downloading events SshScpOptions.Progress
RemotePathTransformation not needed: remote paths are always quoted as one word for the server's shell

See File transfer (SCP).

Errors

SSH.NET SshSharp
SshConnectionException SshException with Reason (ConnectFailed, ConnectionClosed, ProtocolError, KeyExchangeFailed, ...)
SshAuthenticationException SshAuthenticationException (a subclass of SshException)
SshOperationTimeoutException OperationCanceledException when your token was cancelled; SshException with Reason = Timeout for ConnectTimeout
ProxyException SshException with Reason = ConnectFailed, saying what the proxy answered
ScpException SshException with Reason = TransferFailed

See Errors and diagnostics for logging (SshClientOptions.Logger), tracing and metrics.

See also