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 Squeezed 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). It deliberately does not fall back to partial or substring label matching — an automation lookup either matches a saved connection exactly or fails. 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 (unknown) hosts are rejected during automation — there is no UI to prompt for fingerprint verification, so the engine refuses to trust a host it has never seen. You must connect to the host interactively at least once (via the hosts grid) so its key is enrolled in known-hosts; after that, automation trusts it. Changed host keys are also 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), connections are executed concurrently through a bounded worker pool — at most 4 SSH connections run at once, not all simultaneously. 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 opens a quick-connect prompt for the host. It is a connection shortcut, not a key-gated automation call, so it takes no 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 with an escalating lockout: 5 consecutive failed validations lock all API key validation for 1 minute, 10 for 5 minutes, and 15 or more for 30 minutes (the cap). 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. Squeezed entitlement: automation requires an active Squeezed (Pro) entitlement. Juiced is a separate purchase and does not include Squeezed. Free-tier 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.