Tmds.Ssh
Tmds.Ssh is a modern, open-source SSH client library for .NET.
Open Source
- MIT licensed
- Source code available
- Open for contributions, see below.
OpenSSH Compatibility
- Supports OpenSSH private key formats and configuration files
- Compatible with
known_hostsfor host key verification
Security First
- Secure cryptographic algorithms (no legacy/insecure algorithms)
- Post-quantum key exchange support
- Uses BCL and Bouncy Castle for cryptography—no custom crypto implementations
Modern .NET
- Built from the ground up with C#
async/awaitandTask/ValueTaskfor efficient asynchronous operations - Optimized for performance with .NET primitives like
Span<T>to minimize allocations - Integration with
Microsoft.Extensions.Logging
Supported Algorithms
This section lists the supported algorithms. If you would like support for other algorithms, you can request it with an issue in the repository. If the requested algorithm is considered insecure by current practice, it is unlikely to be added.
Private key formats*:
- RSA, ECDSA, ED25519 in
OPENSSH PRIVATE KEY(openssh-key-v1) with encryption:- none
- aes[128|192|256]-[cbc|ctr]
- aes[128|256]-gcm@openssh.com
- chacha20-poly1305@openssh.com
Client key algorithms:
- ssh-ed25519-cert-v01@openssh.com
- ecdsa-sha2-nistp521-cert-v01@openssh.com
- ecdsa-sha2-nistp384-cert-v01@openssh.com
- ecdsa-sha2-nistp256-cert-v01@openssh.com
- rsa-sha2-512-cert-v01@openssh.com
- rsa-sha2-256-cert-v01@openssh.com
- ssh-ed25519
- ecdsa-sha2-nistp521
- ecdsa-sha2-nistp384
- ecdsa-sha2-nistp256
- rsa-sha2-512
- rsa-sha2-256
Server key algorithms:
- ssh-ed25519-cert-v01@openssh.com
- ecdsa-sha2-nistp521-cert-v01@openssh.com
- ecdsa-sha2-nistp384-cert-v01@openssh.com
- ecdsa-sha2-nistp256-cert-v01@openssh.com
- rsa-sha2-512-cert-v01@openssh.com
- rsa-sha2-256-cert-v01@openssh.com
- ssh-ed25519
- ecdsa-sha2-nistp521
- ecdsa-sha2-nistp384
- ecdsa-sha2-nistp256
- rsa-sha2-512
- rsa-sha2-256
Key exchange methods:
- mlkem768x25519-sha256
- sntrup761x25519-sha512, sntrup761x25519-sha512@openssh.com
- curve25519-sha256, curve25519-sha256@libssh.org
- ecdh-sha2-nistp256
- ecdh-sha2-nistp384
- ecdh-sha2-nistp521
Encryption algorithms:
- aes256-gcm@openssh.com
- aes128-gcm@openssh.com
- chacha20-poly1305@openssh.com
Message authentication code algorithms:
- none
Compression algorithms:
- none
Authentication algorithms:
- publickey (PrivateKeyCredential)
- publickey from SSH Agent (SshAgentCredentials)
- publickey with OpenSSH certificate (CertificateCredential)
- password (PasswordCredential)
- gssapi-with-mic (KerberosCredential)
- none (NoCredential)
*: Please convert your keys (using ssh-keygen, PuttyGen, ...) to a supported format rather than suggesting the library should support an additional format. If you can motivate why the library should support a additional format, open an issue to request support.
Sponsoring
Tmds.Ssh is open source and free to use under the MIT license. If your organization depends on it, please consider sponsoring its maintenance.
This isn't a support contract or a license fee — the source stays open and the rules stay simple. Sponsoring is a small, predictable way to help sustain the work that goes into bug fixes, security updates, and new features.
Reporting Bugs and Contributing
Found a bug or want to request a feature? Please open an issue on GitHub.
For security vulnerabilities, use GitHub's private security reporting instead.
Interested in contributing? We welcome pull requests on GitHub! Unless you're making a trivial change, open an issue to discuss the change before making a pull request.
Connecting to an SSH server
The library provides two client types:
- SshClient — for executing remote commands, forwarding connections, and performing filesystem operations.
- SftpClient — for performing filesystem operations using SFTP (SSH File Transfer Protocol).
Creating an SshClient
The simplest way to create an SshClient is with a destination string in the format [user@]host[:port]. This uses the SSH credentials for the current user and validates the server against the OpenSSH known_hosts files:
using Tmds.Ssh;
using var sshClient = new SshClient("user@example.com");
using var process = await sshClient.ExecuteAsync("echo 'hello world!'");
(bool isError, string? content) = await process.ReadLineAsync();
Console.WriteLine(content);
By default, the connection is established automatically when the first operation is performed. You can also connect explicitly by calling ConnectAsync(CancellationToken):
using var sshClient = new SshClient("user@example.com");
await sshClient.ConnectAsync();
The Disconnected property provides a CancellationToken that is canceled when the connection is closed — this can be used to detect connection loss when AutoReconnect is not enabled.
For full control over the connection, pass an SshClientSettings instance. The following example configures a private key credential and custom host authentication:
string destination = "user@example.com";
string privatekeyFile = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".ssh/id_rsa");
string trustedFingerprint = "BkEYx77wOyUBL8UZfgoYKPLkwLJ7XMrsTwAu5sQC4C8";
var settings = new SshClientSettings(destination)
{
Credentials = [ new PrivateKeyCredential(privatekeyFile) ],
UserKnownHostsFilePaths = [ ],
HostAuthentication =
(HostAuthenticationContext context, CancellationToken cancellationToken) =>
{
if (context.ConnectionInfo.ServerKey.Key.SHA256FingerPrint == trustedFingerprint)
{
return ValueTask.FromResult(true);
}
return ValueTask.FromResult(false);
}
};
using var sshClient = new SshClient(settings);
If your application wants to use OpenSSH config files for configuring host settings, you can pass a SshConfigSettings. Settings such as hostname, port, user, identity files, and proxy configuration are picked up automatically:
using var sshClient = new SshClient("myhost", SshConfigSettings.DefaultConfig);
DefaultConfig reads the default config file paths. Use NoConfig to skip config files entirely. You can also set options programmatically:
var configSettings = new SshConfigSettings()
{
Options = { [SshConfigOption.IdentityFile] = "/path/to/key" },
};
using var sshClient = new SshClient("myhost", configSettings);
To know what hosts are known in the configuration files, you can use GetHosts():
ISet<string> hosts = SshConfig.GetHosts();
foreach (string host in hosts.Order(StringComparer.OrdinalIgnoreCase))
{
Console.WriteLine(host.ToLowerInvariant());
}
There is also an overload that accepts the list of config files to check.
Creating an SftpClient
The connection to the SSH server is always made by the SshClient. If your application has an SshClient instance, you can open an SFTP session by calling the OpenSftpClientAsync(CancellationToken).
// Open an SFTP session on an `SshClient`.
using var sftpClient = await sshClient.OpenSftpClientAsync();
await sftpClient.UploadFileAsync("/local/file.txt", "/remote/file.txt");
If your application will only perform SFTP operations, you can directly create an SftpClient and connect it to the server. The SftpClient supports the same constructors as the SshClient. Under the hood, the SftpClient will use an SshClient that establishes the connection.
// SftpClient instance owns an SshClient connection.
using var sftpClient = new SftpClient("user@example.com");
await sftpClient.UploadFileAsync("/local/file.txt", "/remote/file.txt");
Client Authentication
The Credentials property controls how the client authenticates with the server. When multiple credentials are provided, they are tried in order until one succeeds.
When no credentials are configured explicitly, the client uses these defaults:
- Private keys from
~/.ssh/:id_ed25519,id_ecdsa,id_rsa - Matching OpenSSH certificates:
id_ed25519-cert.pub,id_ecdsa-cert.pub,id_rsa-cert.pub - SSH agent keys
- Kerberos
- No authentication
Multiple credentials can be combined. For example, to try a private key first and fall back to a password:
var settings = new SshClientSettings("user@example.com")
{
Credentials = [
new PrivateKeyCredential("/home/user/.ssh/id_ed25519"),
new PasswordCredential("password"),
],
};
When the server requires multi-factor authentication, the library handles this automatically. Each successful step produces a partial result, and the client continues with the remaining credentials until all required methods are satisfied.
Private Keys
A PrivateKeyCredential authenticates using a private key file:
var settings = new SshClientSettings("user@example.com")
{
Credentials = [ new PrivateKeyCredential("/home/user/.ssh/id_ed25519") ],
};
If the key is encrypted, you can provide the password directly or through a callback. When queryKey is true, the library checks whether the server accepts the key before prompting for the decryption password:
new PrivateKeyCredential("/path/to/key", password: "passphrase")
new PrivateKeyCredential("/path/to/key", passwordPrompt: () => ReadPassword(), queryKey: true)
SSH Agent Keys
An SshAgentCredentials uses keys managed by an SSH agent:
Credentials = [ new SshAgentCredentials() ]
OpenSSH Certificate Keys
A CertificateCredential authenticates with a private key that is signed by a certificate authority:
Credentials = [ new CertificateCredential("/path/to/cert", new PrivateKeyCredential("/path/to/key")) ]
Password Authentication
A PasswordCredential authenticates with a password. The password can be provided as a string or through a callback:
Credentials = [ new PasswordCredential("password") ]
The callback receives a PasswordPromptContext with connection info and batch mode state:
Credentials = [ new PasswordCredential((context, cancellationToken) =>
{
if (context.IsBatchMode)
{
return ValueTask.FromResult((string?)null);
}
string prompt = $"{context.ConnectionInfo.UserName}@{context.ConnectionInfo.HostName}'s password: ";
Console.Write(prompt);
return ValueTask.FromResult(Console.ReadLine());
}) ]
Kerberos Authentication
A KerberosCredential authenticates using Kerberos. When no credential is provided, a cached Kerberos ticket is used. The delegateCredential parameter allows the SSH server to act on behalf of the user on remote systems:
Credentials = [ new KerberosCredential() ]
Credentials = [ new KerberosCredential(new NetworkCredential("user", "password", "REALM"), delegateCredential: true) ]
Server Authentication
By default, the server's host key is verified against the OpenSSH known_hosts files. The UserKnownHostsFilePaths and GlobalKnownHostsFilePaths properties control which files are used.
For custom verification, set the HostAuthentication delegate. The delegate is called when the host key is not found in any known hosts file. It is not called when the key is already known to be trusted or revoked:
var settings = new SshClientSettings("user@example.com")
{
UserKnownHostsFilePaths = [ ],
HostAuthentication =
(HostAuthenticationContext context, CancellationToken cancellationToken) =>
{
string expected = "BkEYx77wOyUBL8UZfgoYKPLkwLJ7XMrsTwAu5sQC4C8";
return ValueTask.FromResult(context.ConnectionInfo.ServerKey.Key.SHA256FingerPrint == expected);
}
};
Set UpdateKnownHostsFileAfterAuthentication to true to add newly accepted host keys to the known hosts file.
When using SshConfigSettings, the HostAuthentication delegate is called for unknown keys when StrictHostKeyChecking is ask (the default).
Connection settings
SshClientSettings provides additional properties for controlling the connection:
| Property | Default | Description |
|---|---|---|
ConnectTimeout |
15 seconds | Maximum duration for establishing an authenticated connection. |
AutoConnect |
true |
Automatically connect on first operation. |
AutoReconnect |
false |
Reconnect automatically after an unexpected disconnect on the next operation. |
TcpKeepAlive |
true |
Enable TCP keep-alive. |
KeepAliveInterval |
TimeSpan.Zero |
Interval between SSH keep-alive messages. |
KeepAliveCountMax |
3 | Max keep-alive messages before disconnecting. |
BatchMode |
false |
Disable interactive prompts. |
EnableBatchModeWhenConsoleIsRedirected |
true |
Automatically enable batch mode when the console is redirected. |
MinimumRSAKeySize |
2048 | Minimum RSA key size accepted. |
EnvironmentVariables |
Environment variables set for all remote processes. |
Jump hosts
The Proxy property enables connecting through an SSH jump host:
var settings = new SshClientSettings("target-host")
{
Proxy = new SshProxy("jump-host"),
};
using var sshClient = new SshClient(settings);
Multiple proxies can be chained using Chain(params Proxy[]):
var settings = new SshClientSettings("target-host")
{
Proxy = Proxy.Chain(new SshProxy("jump1"), new SshProxy("jump2")),
};
Algorithms
The permitted cryptographic algorithms can be configured through properties like KeyExchangeAlgorithms, ServerHostKeyAlgorithms, EncryptionAlgorithmsClientToServer, and others. Each property accepts an AlgorithmList that specifies the algorithms in preference order.
For example, to restrict key exchange to post-quantum algorithms:
var settings = new SshClientSettings("user@example.com")
{
KeyExchangeAlgorithms = [ "mlkem768x25519-sha256", "sntrup761x25519-sha512" ],
};
Logging
Both SshClient and SftpClient accept an optional ILoggerFactory for diagnostic logging through Microsoft.Extensions.Logging:
using Microsoft.Extensions.Logging;
using Tmds.Ssh;
using ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddConsole();
});
using var sshClient = new SshClient("user@example.com", loggerFactory);
In production, the log level should be set to Information or higher. The Debug and Trace levels expose sensitive data including usernames, hostnames, key types, public keys, and file paths. At Trace level, all packets are logged.
Executing Commands, Shells and Subsystems
Starting a remote process
ExecuteAsync(string, CancellationToken) runs a command on the remote server and returns a RemoteProcess for interacting with it:
using var sshClient = new SshClient("user@example.com");
using var process = await sshClient.ExecuteAsync("echo 'hello world!'");
(bool isError, string? content) = await process.ReadLineAsync();
Console.WriteLine(content);
ExecuteShellAsync(CancellationToken) and ExecuteSubsystemAsync(string, CancellationToken) are similar methods for running a shell and a subsystem. They also return a RemoteProcess.
How the server handles commands, shells, and subsystems is server dependent. Typical behavior for executing a command is to use the user's shell to execute it. For a shell, it is to launch the user's default shell as a login shell. A subsystem is a predefined server-side program identified by name, such as sftp.
Reading output
RemoteProcess provides several ways to read standard output and standard error. The simplest is to read all output at once using ReadToEndAsStringAsync(CancellationToken):
using var process = await sshClient.ExecuteAsync("hostname");
(string stdout, string stderr) = await process.ReadToEndAsStringAsync();
Console.WriteLine(stdout);
To read a single line, use ReadLineAsync(CancellationToken). It returns null when the end of the output is reached:
using var process = await sshClient.ExecuteAsync("echo 'hello world!'");
(bool isError, string? content) = await process.ReadLineAsync();
Console.WriteLine(content);
For line-by-line processing, use ReadAllLinesAsync(CancellationToken):
using var process = await sshClient.ExecuteAsync("ls -la");
await foreach ((bool isError, string content) in process.ReadAllLinesAsync())
{
if (isError)
Console.Error.WriteLine(content);
else
Console.WriteLine(content);
}
For reading into a buffer, use ReadAsync(Memory<byte>?, Memory<byte>?, CancellationToken). To copy all output into a Stream, use ReadToEndAsync(Stream?, Stream?, CancellationToken). To wrap the output as a Stream or StreamReader, use ReadAsStream(StderrHandler) or ReadAsStreamReader(StderrHandler, int).
Writing standard input
Data can be written to the process using WriteAsync(string, CancellationToken):
using var process = await sshClient.ExecuteAsync("cat");
await process.WriteAsync("Hello World!");
process.WriteEof();
(string stdout, string stderr) = await process.ReadToEndAsStringAsync();
Console.WriteLine(stdout);
Other write methods include WriteAsync(ReadOnlyMemory<byte>, CancellationToken) for raw bytes, WriteLineAsync(string?, CancellationToken) for writing a line, and StandardInputStream and StandardInputWriter for stream-based writing.
If you are not writing to standard input, call WriteEof() early to prevent the remote process from blocking on reading input.
Getting the exit code
After reading the output, GetExitCodeAsync(CancellationToken) returns the exit code. If there is any unread output remaining when this method is called, it will be discarded.
using var process = await sshClient.ExecuteAsync("ls /nonexistent");
(string stdout, string stderr) = await process.ReadToEndAsStringAsync();
int exitCode = await process.GetExitCodeAsync();
Console.WriteLine($"Exit code: {exitCode}");
GetExitStatusAsync(CancellationToken) is similar but returns an RemoteProcess.ExitStatus which includes both the exit code and the signal name (if the process was terminated by a signal):
(string stdout, string stderr) = await process.ReadToEndAsStringAsync();
(int exitCode, string? exitSignal) = await process.GetExitStatusAsync();
Detect termination
The ExecutionAborted cancellation token is triggered when the remote process can no longer be interacted with. This includes normal process termination and connection loss. It can be used to cancel other operations that depend on the remote process.
Allocating a Terminal
Some programs need to run with a terminal. To allocate one, set AllocateTerminal to true:
var options = new ExecuteOptions
{
AllocateTerminal = true,
TerminalWidth = 120,
TerminalHeight = 40,
};
using var process = await sshClient.ExecuteAsync("top", options);
The terminal type can be set with TerminalType. You can check whether a terminal was allocated using HasTerminal, and resize it with SetTerminalSize(int, int).
When a terminal is allocated, standard error is merged into standard output.
Sending signals
SendSignal(string) sends a signal to the remote process:
process.SendSignal(SignalName.TERM);
The SignalName class provides constants for common signal names. The method returns true if the signal was sent, or false if the signal can no longer be delivered.
Forwarding Connections
SshClient provides methods for forwarding connections over the SSH connection.
Forward between endpoints
StartForwardAsync binds a local endpoint and forwards incoming connections to a remote endpoint.
using var forward = await sshClient.StartForwardAsync(
new IPEndPoint(IPAddress.Loopback, 8080),
new RemoteHostEndPoint("localhost", 80));
Console.WriteLine($"Forwarding on {forward.ListenEndPoint}");
The local endpoint can be an IPEndPoint or a UnixDomainSocketEndPoint. The remote endpoint can be a RemoteHostEndPoint, RemoteIPEndPoint, or RemoteUnixEndPoint.
StartRemoteForwardAsync binds a remote endpoint and forwards incoming connections to a local endpoint.
using var forward = await sshClient.StartRemoteForwardAsync(
new RemoteIPListenEndPoint("localhost", 8080),
new IPEndPoint(IPAddress.Loopback, 80));
The remote endpoint can be a RemoteIPListenEndPoint or a RemoteUnixEndPoint. The local endpoint can be a DnsEndPoint, IPEndPoint, or UnixDomainSocketEndPoint.
The returned LocalForward and RemoteForward implement IDisposable — disposing stops the forward. They also expose a Stopped cancellation token that triggers when the forward stops (for example, when the SSH connection drops), and ThrowIfStopped to check that the forward is still running.
Proxy via SOCKS
StartSocksForwardAsync starts a local SOCKS proxy that routes traffic through the SSH server.
using var proxy = await sshClient.StartSocksForwardAsync(
new IPEndPoint(IPAddress.Loopback, 1080));
Console.WriteLine($"SOCKS proxy on {proxy.ListenEndPoint}");
The returned SocksForward implements IDisposable — disposing it stops the forward. It also exposes a Stopped cancellation token that triggers when the forward stops (for example, when the SSH connection drops), and ThrowIfStopped() to check that the forward is still running.
Open a TCP/Unix connection
OpenTcpConnectionAsync opens a direct TCP connection to a host through the SSH server:
using var stream = await sshClient.OpenTcpConnectionAsync("database-server", 5432);
OpenUnixConnectionAsync connects to a Unix domain socket on the remote server:
using var stream = await sshClient.OpenUnixConnectionAsync("/var/run/postgresql/.s.PGSQL.5432");
Listen for TCP/Unix connections
ListenTcpAsync creates a TCP listener on the remote server. Incoming connections are accepted through the SSH tunnel as RemoteConnection types:
using var listener = await sshClient.ListenTcpAsync("0.0.0.0", 8080);
while (true)
{
using var connection = await listener.AcceptAsync();
if (!connection.HasStream)
break; // Listener was stopped.
using var stream = connection.MoveStream();
// Handle the incoming connection...
}
When the listener is stopped via Stop(), AcceptAsync returns a RemoteConnection with HasStream set to false. Use MoveStream() to take ownership of the connection stream.
Pass port 0 to let the server assign a port. The actual assigned port is available from ListenEndPoint.
ListenUnixAsync does the same for Unix domain sockets:
using var listener = await sshClient.ListenUnixAsync("/tmp/my.sock");
SSH File Transfer Protocol (SFTP)
The SftpClient provides methods for performing filesystem operations on remote servers using the SSH File Transfer Protocol (SFTP).
The following example uploads a file to the server and downloads it back:
using Tmds.Ssh;
using var sftpClient = new SftpClient("user@example.com");
await sftpClient.UploadFileAsync("/local/path/file.txt", "/remote/path/file.txt");
await sftpClient.DownloadFileAsync("/remote/path/file.txt", "/local/path/downloaded.txt");
All SFTP file operations are defined on the ISftpDirectory interface. SftpClient implements this interface using the server's working directory as the base for relative paths. To scope operations to a specific directory, use GetDirectory(string). The returned SftpDirectory implements the same interface, resolving relative paths against the specified directory:
SftpDirectory uploads = sftpClient.GetDirectory("/remote/uploads");
await uploads.UploadFileAsync("/local/file.txt", "file.txt");
Uploading files
To upload a file from disk, pass the local and remote paths:
await sftpClient.UploadFileAsync("/local/file.txt", "/remote/file.txt");
You can also upload from a Stream:
using var stream = File.OpenRead("/local/report.csv");
await sftpClient.UploadFileAsync(stream, "/remote/report.csv");
By default, UploadFileAsync will not overwrite an existing file. Pass overwrite: true to replace it. The createPermissions parameter sets the file permissions on the remote server — the server applies a umask on top of these:
await sftpClient.UploadFileAsync("/local/file.txt", "/remote/file.txt", overwrite: true,
createPermissions: UnixFilePermissions.UserRead | UnixFilePermissions.UserWrite);
UploadDirectoryEntriesAsync uploads all entries from a local directory to a remote directory:
await sftpClient.UploadDirectoryEntriesAsync("/local/dir", "/remote/dir");
UploadEntriesOptions controls overwriting, subdirectory recursion, link following, filtering, and concurrency:
await sftpClient.UploadDirectoryEntriesAsync("/local/dir", "/remote/dir",
new UploadEntriesOptions
{
Overwrite = true,
ShouldInclude = (ref LocalFileEntry entry) => !entry.ToFullPath().EndsWith(".tmp")
});
The TargetDirectoryCreation property controls whether the target directory is created automatically. The default is CreateWithParents. Create creates only the target directory without parents. Set it to None if the target directory must already exist, or CreateNew to fail if it already exists.
Downloading files
To download a file to disk:
await sftpClient.DownloadFileAsync("/remote/file.txt", "/local/file.txt");
To download into a Stream:
using var stream = File.Create("/local/file.txt");
await sftpClient.DownloadFileAsync("/remote/file.txt", stream);
DownloadDirectoryEntriesAsync downloads all entries from a remote directory to a local directory:
await sftpClient.DownloadDirectoryEntriesAsync("/remote/dir", "/local/dir");
DownloadEntriesOptions provides the same controls as UploadEntriesOptions. Download options use SftpFileEntryPredicate for filtering remote entries, while upload options use LocalFileEntryPredicate for filtering local entries:
await sftpClient.DownloadDirectoryEntriesAsync("/remote/dir", "/local/dir",
new DownloadEntriesOptions
{
FileTypeFilter = UnixFileTypeFilter.RegularFile,
ShouldInclude = (ref SftpFileEntry entry) => entry.Length > 0
});
Copying files
CopyFileAsync copies a file on the remote server:
await sftpClient.CopyFileAsync("/remote/source.txt", "/remote/destination.txt");
When the server supports the copy-data SFTP extension, the copy is performed entirely server-side. Otherwise, the data is read from the source and written to the destination through the client.
Renaming files and directories
RenameAsync renames a file or directory:
await sftpClient.RenameAsync("/remote/old.txt", "/remote/new.txt");
Working with directories
CreateDirectoryAsync creates a directory. Pass createParents: true to create intermediate directories:
await sftpClient.CreateDirectoryAsync("/remote/path/to/newdir", createParents: true);
CreateDirectoryAsync succeeds if the directory already exists. If you want the operation to fail if the directory already exists, you can call CreateNewDirectoryAsync.
DeleteDirectoryAsync removes a directory. Pass recursive: true to delete all contents:
await sftpClient.DeleteDirectoryAsync("/remote/olddir", recursive: true);
DeleteDirectoryAsync also succeeds when the directory did not exist.
GetDirectoryEntriesAsync lists the contents of a directory. It takes a transform delegate that selects which data to extract from each entry:
await foreach (var (path, length) in sftpClient.GetDirectoryEntriesAsync(
"/remote/dir",
(ref SftpFileEntry entry) => (entry.ToPath(), entry.Length)))
{
Console.WriteLine($"{path} ({length} bytes)");
}
Pass EnumerationOptions to recurse into subdirectories, follow links, or filter by file type:
await foreach (string path in sftpClient.GetDirectoryEntriesAsync(
"/remote/dir",
(ref SftpFileEntry entry) => entry.ToPath(),
new Tmds.Ssh.EnumerationOptions
{
RecurseSubdirectories = true,
FileTypeFilter = UnixFileTypeFilter.RegularFile
}))
{
Console.WriteLine(path);
}
The ShouldInclude and ShouldRecurse predicates provide fine-grained control over which entries are returned and which subdirectories are traversed. ExtendedAttributes specifies which extended attributes to request from the server — pass null to request all.
Working with files
To open a remote file, you can call the OpenFileAsync method. If the file does not exist, the method returns null. If you want to create the file when it doesn't exist yet, you can call OpenOrCreateFileAsync instead. Or, if you want to ensure a new file is created, you can call CreateNewFileAsync which will throw if the file already exists.
using SftpFile file = await sftpClient.OpenOrCreateFileAsync("/remote/data.bin", FileAccess.ReadWrite);
The SftpFile type that is returned derives from Stream. It provides additional methods to read and write at an offset:
int bytesRead = await file.ReadAtAsync(buffer, offset: 0);
The behavior of the open can be controlled using FileOpenOptions.
using SftpFile file = await sftpClient.OpenOrCreateFileAsync("/remote/log.txt", FileAccess.Write,
new FileOpenOptions { OpenMode = OpenMode.Append });
OpenMode.Truncate clears the file on open. Set CacheLength = true to enable Stream.Length and Stream.Seek.
To delete a file, you can call DeleteFileAsync. The method also succeeds when the file did not exist.
await sftpClient.DeleteFileAsync("/remote/file.txt");
Working with symbolic links
CreateSymbolicLinkAsync creates a symbolic link:
await sftpClient.CreateSymbolicLinkAsync("/remote/link", "/remote/target");
GetLinkTargetAsync reads the target of a symbolic link, and GetRealPathAsync resolves the canonical path:
string target = await sftpClient.GetLinkTargetAsync("/remote/link");
string realPath = await sftpClient.GetRealPathAsync("/remote/link");
To delete a symbolic link, you can call DeleteFileAsync. The method also succeeds when the link did not exist.
await sftpClient.DeleteFileAsync("/remote/link");
Getting and changing attributes
GetAttributesAsync retrieves file metadata such as size and permissions:
var attributes = await sftpClient.GetAttributesAsync("/remote/file.txt", followLinks: true);
if (attributes is not null)
{
Console.WriteLine($"Length: {attributes.Length}");
Console.WriteLine($"Permissions: {attributes.Permissions}");
}
The method returns null when the path does not exist. The returned FileEntryAttributes includes Length, FileType, Permissions, Uid, Gid, LastAccessTime, LastWriteTime, and optionally ExtendedAttributes.
SetAttributesAsync modifies file metadata:
await sftpClient.SetAttributesAsync("/remote/file.txt",
permissions: UnixFilePermissions.UserRead | UnixFilePermissions.UserWrite);
SetAttributesAsync accepts optional parameters — only the values you pass are changed:
await sftpClient.SetAttributesAsync("/remote/file.txt",
times: (DateTime.UtcNow, DateTime.UtcNow),
ids: (Uid: 1000, Gid: 1000));
Monitor progress
SFTP operations that transfer data accept an optional SftpProgressHandler for monitoring progress. Subclass it and override the methods you are interested in:
class MyProgressHandler : SftpProgressHandler
{
private long _startTime;
private long _endTime;
private long _totalBytesTransferred;
protected override void Start(int maxConcurrentEntries)
=> _startTime = Stopwatch.GetTimestamp();
protected override void DataTransferred(int index, long bytesTransferred, long offset)
=> Interlocked.Add(ref _totalBytesTransferred, bytesTransferred);
protected override void Completed(Exception? exception)
=> _endTime = Stopwatch.GetTimestamp();
public long TotalBytesTransferred
=> Interlocked.Read(ref _totalBytesTransferred);
public TimeSpan Elapsed
=> Stopwatch.GetElapsedTime(_startTime, _endTime != 0 ? _endTime : Stopwatch.GetTimestamp());
}
var progress = new MyProgressHandler();
await sftpClient.UploadDirectoryEntriesAsync("/local/dir", "/remote/dir", progress: progress);
Start is always called synchronously before the async method returns its ValueTask. Completed is called at the end, including when the operation fails. Callbacks are invoked from background threads — use thread-safe operations like Interlocked to aggregate data, and defer UI updates to a separate thread. See SftpProgressHandler for more information.
.NET Tools
ssh and ssh-cp are .NET tools built using Tmds.Ssh. They provide an easy way to try Tmds.Ssh against an SSH server without writing code.
ssh
ssh is an SSH client similar to OpenSSH ssh. With .NET 10+, it can be run directly:
dnx ssh user@example.com
On .NET 8+, install it as a .NET tool:
dotnet tool update -g ssh
dotnet ssh user@example.com
ssh-cp
ssh-cp copies files to and from remote hosts, similar to OpenSSH scp:
dnx ssh-cp localfile.txt user@example.com:/remote/path/
dnx ssh-cp user@example.com:/remote/file.txt ./local/
On .NET 8+, install it as a .NET tool:
dotnet tool update -g ssh-cp
dotnet ssh-cp localfile.txt user@example.com:/remote/path/