Traversio

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:

FieldMeaning
standardOutputRaw bytes received on SSH stdout
standardErrorRaw bytes received on SSH stderr
exitStatusExit status reported by the remote process, if present
exitSignalRemote exit signal details, if the process terminated because of a signal
didReceiveEOFWhether 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 disk

Each 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 SSHSession event 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 events iteration, which already attempt a best-effort channel-close on 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:

  • authenticationRejected
  • connectionFailed
  • operationFailed
  • passwordChangeRequired
  • connectionScopeEnded

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 implicit instead 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 session or sftp
  • 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.

On this page