Skip to content

Automation Engine

ZestSSH includes a full automation engine that lets external apps trigger SSH commands, snippets, batch operations, and workflows without opening a terminal session. The engine connects to your saved servers headlessly, runs the command, captures stdout/stderr, and returns the result — all in the background.

Automation is a Pro feature. All automation actions require a valid API key.

The automation engine (AutomationService) establishes a temporary SSH connection to the target server, executes a command via SSHClient.execute() (no PTY allocated), collects stdout and stderr, waits for the exit code, and disconnects. Each execution is isolated: a fresh connection is opened and closed for every action.

Connection credentials are resolved from ZestSSH’s saved connections database. The engine looks up the connection by exact ID first, then by exact label (case-insensitive), and finally by partial label match. Identity credentials (password, private key, passphrase) are loaded from FlutterSecureStorage at execution time.

Host key verification follows the same known-hosts database as interactive sessions. New hosts are auto-trusted during automation (there is no UI to prompt), but changed host keys are rejected as a potential MITM attack.

Run a raw shell command on a saved connection.

  • Deep link: zestssh://execute?connection=prod-web&command=uptime&key=API_KEY
  • Intent: com.affluentlabs.zestssh.EXECUTE with extras connection_id, command, key
  • Parameters: connection (label or ID), command, timeout (seconds, default 30), key

Run a saved snippet by name. Snippet variables can be passed as var_ prefixed parameters.

  • Deep link: zestssh://snippet?connection=prod-web&name=Disk+Usage&key=API_KEY
  • Intent: com.affluentlabs.zestssh.SNIPPET with extras connection_id, snippet_name, key
  • Variables: pass var_HOST=example.com to set the HOST variable in the snippet template

The engine looks up the snippet by label (case-insensitive). If variables are provided, {{key}} placeholders in the snippet command are replaced with the corresponding values.

Execute the same command across multiple connections simultaneously or sequentially.

  • Deep link: zestssh://batch?connections=web1,web2,db1&command=uptime&parallel=true&key=API_KEY
  • Parameters: connections (comma-separated labels/IDs), command, timeout, parallel (default true), key

When parallel=true (default), all connections are executed concurrently via Future.wait. When false, they execute sequentially in list order. The combined result includes per-connection output sections separated by headers.

Trigger a saved multi-step workflow by name or ID.

  • Deep link: zestssh://workflow?name=Deploy+Production&key=API_KEY
  • Intent: com.affluentlabs.zestssh.WORKFLOW with extras workflow_name or workflow_id, key

See the Workflows documentation for details on workflow structure and execution.

Verify that a connection is reachable without running a command.

  • Deep link: zestssh://connect?host=prod-web&key=API_KEY
  • Intent: com.affluentlabs.zestssh.CONNECT with extras connection_id, key

Internally, the engine runs echo "ZestSSH: connected successfully" to verify the connection. The result returns success or failure.

Every automation request must include a valid API key. Keys are 48-character alphanumeric strings generated from Random.secure(), providing approximately 285 bits of entropy.

Key validation uses constant-time SHA-256 comparison: the input key and each stored key are independently hashed with SHA-256, and the resulting byte arrays are compared using XOR with OR accumulation. This prevents timing side-channel attacks that could leak information about valid keys.

Rate limiting protects against brute-force attempts: after 5 consecutive failed validation attempts, all API key validation is locked out for 1 minute. The failed-attempt counter and lockout timestamp are persisted to FlutterSecureStorage, so restarting the app does not reset the lockout.

Expired keys are treated as invalid and count toward the failed-attempt counter.

See API Keys for full details on key management.

Execute and batch actions support optional output filters applied in this order:

  1. Grep (grep parameter): keep only lines matching a pattern (case-insensitive regex). Patterns longer than 256 characters are treated as literal string matches to prevent ReDoS. Invalid regex patterns fall back to literal matching.
  2. Head (head parameter): take only the first N lines.
  3. Tail (tail parameter): take only the last N lines.

Filters are applied after the command completes, before returning results to the caller.

Example: zestssh://execute?connection=web1&command=journalctl+-n+100&grep=error&tail=10&key=API_KEY — runs journalctl -n 100, filters to lines containing “error”, then returns only the last 10 matching lines.

Execute and batch actions support a callback parameter with an HTTPS URL. After the command completes, ZestSSH POSTs a JSON payload to the webhook:

{
"connection": "prod-web",
"command": "uptime",
"stdout": "14:23:01 up 42 days...",
"stderr": "",
"exit_code": 0,
"elapsed_ms": 1523,
"timestamp": "2026-01-15T14:23:02Z"
}

Webhook calls are fire-and-forget: they run asynchronously and failures are logged but do not affect the automation result. Only HTTPS URLs are accepted; HTTP URLs are rejected for security.

All automation runs are logged to a local database table with timestamp, action type, connection, command, stdout, stderr, exit code, success status, and elapsed time. The log is capped at 500 entries (oldest entries are trimmed automatically).

View the log from Settings > Automation > History.

On Android, automation results are returned to the calling app via the intent result mechanism. The result includes:

  • stdout: the command output (or error message, after any filters are applied).
  • exit_code: the process exit code (0 for success).

This allows apps like Tasker to read the command output and make decisions based on the exit code.

Before any automation action is processed, the engine checks three gates in order:

  1. Pro subscription: automation requires an active Pro or Bundle entitlement. Non-Pro users receive an error.
  2. Automation enabled: the user can toggle automation on/off in Settings. Disabled automation returns an error.
  3. API key validation: the provided key must be valid, non-expired, and pass rate limiting. Invalid keys return an error.

Only after all three gates pass does the engine proceed to resolve the connection and execute the action.

Deep link URIs are validated before processing:

  • Maximum URI length: 2048 bytes.
  • Maximum per-parameter length: 256 characters.
  • The zestssh:// scheme is required.
  • Connection identifiers are checked for control characters.
  • Connection-type deep links (zestssh://connect) require explicit user confirmation via a callback before proceeding.