Exceptions and Return Values
Version scope: The base command API is available in 3.1.5. Managed deadlines and jobs described below are features available in 5.0.0.
This contract describes the source API in both normal Python and python -O. Managed-command additions are marked 4.0; the base result/check API was introduced in 3.1.5.
Managed commands (4.0 source API)
Session.start(cmd, *, shell=None, shell_executable=None, timeout=None, stop_timeout=3, capture_limit=1048576, check=False, input=None, env=None, get_pty=False) returns a CommandJob with wait(), stop(), iter_stdout(), done, and a context manager. Timeout is a total local/SSH command budget; None means no execution deadline. Local get_pty=True is unsupported.
exec_command(..., command_timeout=...) opts into the same managed execution; legacy timeout remains unchanged and cannot be combined with command_timeout. Managed calls return JobResult, a CommandResult subclass with stop_reason, termination_status and stdout/stderr truncation flags. Missing status is None. Deadline expiry raises CommandTimeoutError with a partial result and text output, even when check=False. start() does not alter Session.last_result.
These APIs are not in published 3.1.5. The legacy result/buffer rules below apply when command_timeout is omitted. See the timeout and cancellation guide for bounded output, cleanup guarantees, and a remote tcpdump/Ctrl-C example.
Exception matrix
| Failure | Exception |
|---|---|
| Wrong argument type | TypeError |
| Invalid value, content, or combination | ValueError |
| Invalid Session/console/channel lifecycle state | RuntimeError |
| Operation on a closed channel/transport | BrokenPipeError |
| Channel ends while waiting | EOFError |
| SSH command closes without an exit-status message (5.0.0; introduced in 4.0) | EOFError; no completed command result |
| Legacy one-shot timeout | subprocess.TimeoutExpired locally; original Paramiko/socket timeout remotely |
| Managed command deadline | CommandTimeoutError, a TimeoutError subclass, with partial JobResult |
| Managed streaming queue overflow | BufferError |
| su/sudo rejected authentication, requested another password, or required an unsupplied password (4.0) | PermissionError |
| su/sudo entry or return deadline expired (4.0) | TimeoutError |
| su/sudo early command exit, identity mismatch, or reuse of a single-use context (4.0) | RuntimeError |
| Disconnected SFTP access or transfer | SSHScriptException |
| Filesystem failure | Appropriate OSError subclass |
| SSH/authentication/host-key failure | Original Paramiko exception and traceback |
| Internal AST/data-structure invariant | Descriptive RuntimeError |
| Session-stack index outside its bounds | IndexError, following deque |
AssertionError was never a supported SSHScript API contract. Do not catch it as input validation. User-written .spy assertions remain ordinary Python assertions and are removed by python -O.
Missing SSH exit status is a protocol failure even with check=False. Paramiko’s internal -1 sentinel is not a command exit code. Transport errors retain their original types rather than becoming CommandTimeoutError, and an SSH failure never causes local execution fallback. See the missing-status and transport-loss contract.
Authenticated console entry (4.0)
A failed su/sudo entry never silently falls back to the original account or resends the same password. I/O errors propagate; invalid enter_timeout types raise TypeError, and invalid numeric values raise ValueError. A separate two-second recovery attempt verifies return to the original parent shell. Unconfirmed recovery makes the channel reject subsequent commands. Handle entry errors outside the with statement and create a new context for retries. See the authenticated console contract.
Results and failure handling
Session.exec_command() returns an immutable CommandResult. Unpack three values: stdout, stderr, exitcode = result. Output is captured text. The result also has host, duration, and command; session.last_result retains it. Saved results survive subsequent commands and Session closure.
With check=False (the default), nonzero status remains data. With check=True, local and remote one-shot commands raise subprocess.CalledProcessError after saving the result. The exception carries text stdout/stderr and result.
from contextlib import closing
from sshscript import Session
with closing(Session()) as session:
stdout, stderr, exitcode = session.exec_command(["false"])
if exitcode != 0:
raise RuntimeError(f"command failed: exitcode={exitcode}")
In 4.0 source, completed shell/su/sudo commands also return immutable CommandResult snapshots, and check failures carry .result. Published 3.1.5 console calls still return two buffers. Interactive input reports prompt/exit/silence instead of a command result. Transfers return paths without updating command status. close() returns a success bool and records close_errors; close(strict=True) raises after attempting cleanup. Do not depend on assert for production failure handling.
See Results and Error Model for field semantics and migration from live output to text snapshots.
Validation guarantees
One-shot commands accept nonempty strings or nonempty lists/tuples of strings. Argv disallows an empty executable, NUL, and shell mode. Persistent commands must be one-line strings; get_pty must be None/bool, and internal for_with must be bool. Compiled bytes regexes are rejected by text matching. Appended output must be str. Invalid inputs are rejected before command execution or affected buffer mutation. Failed listener removal, duplicate hijack/release, and last-layer removal do not alter their associated state.
See Session and Console API Reference for operation signatures and cleanup details.
Last Updated: 2026-10-10 19:43:07