Running Commands
Execute a remote command and interpret the current result model.
Choose A Command API
Use SSHConnection.execute(_:) when you need a one-shot remote command:
import Traversio
@available(macOS 26.0, iOS 26.0, *)
func readKernelVersion() async throws -> String {
let configuration = SSHClientConfiguration(
host: "example.com",
username: "deploy",
authentication: .password("secret"),
hostKeyPolicy: .knownHostsFile("/Users/me/.ssh/known_hosts")
)
let result = try await SSHClient.withConnection(configuration: configuration) { connection in
try await connection.execute("uname -a")
}
return String(decoding: result.standardOutput, as: UTF8.self)
}If you need incremental stdout/stderr/exit-status delivery or want to write to stdin yourself, use SSHConnection.openExec(_:) instead:
import Traversio
@available(macOS 26.0, iOS 26.0, *)
func streamCommand(configuration: SSHClientConfiguration) async throws {
try await SSHClient.withConnection(configuration: configuration) { connection in
let session = try await connection.openExec("printf 'hello\\n'; printf 'warn\\n' >&2; exit 7")
for try await event in session.events {
switch event {
case let .standardOutput(bytes):
print("stdout:", String(decoding: bytes, as: UTF8.self))
case let .standardError(bytes):
print("stderr:", String(decoding: bytes, as: UTF8.self))
case let .exitStatus(status):
print("exit:", status)
case let .exitSignal(exitSignal):
print("exit signal:", exitSignal.signal.rawValue)
case .endOfFile:
print("eof")
}
}
}
}Sending Environment Variables
Use the optional environment: parameter when the remote command expects one or more SSH env requests before exec starts:
let environment = [
SSHSessionEnvironmentVariable(name: "LANG", value: "en_US.UTF-8"),
SSHSessionEnvironmentVariable(name: "LC_ALL", value: "en_US.UTF-8"),
]
let result = try await connection.execute(
"printenv LANG",
environment: environment
)The streamed exec path supports the same input:
let session = try await connection.openExec(
"printenv TERM_PROGRAM",
environment: [
SSHSessionEnvironmentVariable(name: "TERM_PROGRAM", value: "Traversio")
]
)Traversio waits for a reply to each environment request before it sends exec. A rejected environment request fails session startup and preserves a clear startup boundary.
What You Get Back
execute(_:) returns SSHExecResult:
| Field | Meaning |
|---|---|
standardOutput | Raw bytes received on SSH stdout |
standardError | Raw bytes received on SSH stderr |
exitStatus | Exit status reported by the remote process, if present |
exitSignal | Remote exit signal details, if the process terminated because of a signal |
didReceiveEOF | Whether the channel delivered EOF before closing |
Typical decoding pattern:
let result = try await connection.execute("df -h /")
let stdout = String(decoding: result.standardOutput, as: UTF8.self)
let stderr = String(decoding: result.standardError, as: UTF8.self)
if let exitStatus = result.exitStatus, exitStatus != 0 {
print("command failed:", exitStatus)
print(stderr)
}
if let exitSignal = result.exitSignal {
print("command terminated by signal:", exitSignal.signal.rawValue)
}Good Uses For execute(_:)
- health checks
- small remote inspections
- setup commands that do not need incremental interaction
- scripts where collecting the full transcript is acceptable
Running Several Commands At Once
If you are building monitoring or telemetry features, it is reasonable to run several one-shot commands at the same time on one SSHConnection.
async let cpu = connection.execute("top -bn1 | head -n 5")
async let memory = connection.execute("free -m")
async let disk = connection.execute("df -h /")
let cpuResult = try await cpu
let memoryResult = try await memory
let diskResult = try await diskEach call opens its own exec-capable session channel.
Do not try to reuse one SSHSession for multiple remote commands.
If you also need a shell or SFTP at the same time, see Sharing One Connection.
When openExec(_:) Fits Better
- stdin needs to stay open for a while
- you want stdout, stderr, exit status, and EOF in arrival order
- you want the same
SSHSessionevent model used by the shell API - you want to keep
execute(_:)as a simple convenience but still have a lower-level streamed path when needed
Because openExec(_:) returns SSHSession, the streamed path can also use the same session-control helpers as shells:
let session = try await connection.openExec("sleep 300")
try await session.sendSignal(.terminate)
try await session.sendEOF()Exec API Limits
The current exec surface stays focused on command execution and streaming:
- working-directory helpers
- exec-specific PTY setup helpers
- a broader graceful-cancellation contract beyond transcript collectors and
eventsiteration, which already attempt a best-effortchannel-closeon cancellation
Use a shell session for long-running interactive terminal work.
Metadata Is Often Worth Logging
Connection metadata is useful in command-oriented tools because it exposes:
- the remote identification string
- the verified host-key fingerprint
- which trust method accepted the host key
Example:
try await SSHClient.withConnection(configuration: configuration) { connection in
let metadata = connection.metadata
print("remote:", metadata.remoteIdentification)
print("fingerprint:", metadata.hostKeyFingerprintSHA256)
let result = try await connection.execute("hostname")
print(String(decoding: result.standardOutput, as: UTF8.self))
}Error Surface
The stable public error type is SSHClientError, which covers:
authenticationRejectedconnectionFailedoperationFailedpasswordChangeRequiredconnectionScopeEnded
connectionFailed(SSHConnectionFailure) is the connection-setup wrapper. It preserves:
- which setup stage failed
- a stable failure code
- connection diagnostics such as the remote identification, negotiated algorithms, remote disconnect details, and remote debug messages when the server sent them
- the effective integrity algorithm for each direction, so AEAD and OpenSSH Chacha transports report
implicitinstead of looking like a separate HMAC is active
operationFailed(SSHOperationFailure) covers the stable post-auth path. Session, direct-channel, forwarded-channel, remote-listener, and SFTP operations can fail with:
- an operation scope such as
sessionorsftp - a stable failure code
- channel IDs when known
- remote disconnect/debug context
- the same negotiated-algorithm and effective-integrity view as connection failures when transport diagnostics are available
- SFTP status details when the server answered with a status packet
Some upper-layer errors can still escape unchanged, especially callback or policy errors that come from your own code.