# How to Run Remote Commands Using SSH with Probe's ssh Plugin: A Complete Technical Guide

> Learn to run remote commands via SSH using Probe's ssh plugin. This guide details defining SSH steps with host, user, and commands for efficient remote execution and output management.

- Repository: [Tomohisa Oda/probe](https://github.com/linyows/probe)
- Tags: how-to-guide
- Published: 2026-03-06

---

**To run remote commands via SSH in Probe, define a workflow step with `uses: ssh` and provide the `host`, `user`, `cmd`, and either a `password` or `key_file` in the `with` block; the plugin handles connection, execution, timeout management, and returns structured output including exit code, stdout, and stderr.**

Probe treats every built-in action as a **plugin** that receives a map of parameters, builds the appropriate client, executes the work, and returns a structured result. The SSH plugin, implemented in [`ssh/client.go`](https://github.com/linyows/probe/blob/main/ssh/client.go), provides a self-contained, reproducible way to execute commands on remote servers within your Probe workflows.

## Understanding the SSH Plugin Architecture

The SSH plugin follows a clear request-response pattern defined in [`ssh/client.go`](https://github.com/linyows/probe/blob/main/ssh/client.go). Every workflow invocation maps YAML parameters to Go structs, validates the input, establishes an SSH connection, and captures the command output.

### Parameter Definition with Req and Res Structs

The plugin defines its input contract through the `Req` struct (lines 26-38) and output through the `Res` struct (lines 42-46):

```go
type Req struct {
    Host            string            `map:"host" validate:"required"`
    Port            int               `map:"port"`
    User            string            `map:"user" validate:"required"`
    Cmd             string            `map:"cmd" validate:"required"`
    Password        string            `map:"password"`
    KeyFile         string            `map:"key_file"`
    KeyPassphrase   string            `map:"key_passphrase"`
    Timeout         string            `map:"timeout"`
    Workdir         string            `map:"workdir"`
    Env             map[string]string `map:"env"`
    StrictHostCheck bool              `map:"strict_host_check"`
    KnownHosts      string            `map:"known_hosts"`
    cb              *Callback
}

type Res struct {
    Code   int    `map:"code"`
    Stdout string `map:"stdout"`
    Stderr string `map:"stderr"`
}

```

The `map` struct tags allow Probe to automatically populate these fields from your workflow YAML's `with` block, while the `validate:"required"` tags enforce that `host`, `user`, and `cmd` must be present.

### Parsing, Validation, and Defaults

The `parseParams` function (lines 86-115) normalizes your workflow input before execution:

- **Port defaults to 22** if not specified
- **Timeout defaults to 30s** if not specified  
- **Expands tilde (`~`) paths** for `key_file` and `known_hosts` to absolute paths
- **Validates authentication**: Ensures either `password` or `key_file` is provided
- **Checks required fields**: Verifies `host`, `user`, and `cmd` are present

This validation layer prevents connection failures by catching configuration errors before establishing the SSH session.

## Configuring SSH Authentication

The `createSSHConfig` function (lines 91-77 in [`ssh/client.go`](https://github.com/linyows/probe/blob/main/ssh/client.go)) builds the `ssh.ClientConfig` with multiple authentication methods and host key verification strategies.

### Password-Based Authentication

Provide the `password` parameter in your workflow:

```yaml
uses: ssh
with:
  host: "prod.example.com"
  user: "deploy"
  password: "${{ secrets.SSH_PASSWORD }}"
  cmd: "whoami"

```

### Public Key Authentication with Passphrase Support

For key-based auth, specify `key_file` and optionally `key_passphrase`:

```yaml
uses: ssh
with:
  host: "prod.example.com"
  user: "deploy"
  key_file: "~/.ssh/deploy_key"
  key_passphrase: "${{ secrets.KEY_PASS }}"
  cmd: "git pull origin main"

```

The plugin handles encrypted private keys by decrypting them with the provided passphrase before use.

### Host Key Verification

Control security strictness with `strict_host_check` and `known_hosts`:

- **Strict mode**: Set `strict_host_check: true` to verify the remote host against known hosts
- **Custom known hosts**: Use `known_hosts: "~/.ssh/known_hosts"` (defaults to `~/.ssh/known_hosts` and `/etc/ssh/ssh_known_hosts` if not specified)
- **Lenient mode**: Omit `strict_host_check` or set to `false` to skip verification (not recommended for production)

## Executing Remote Commands

The `Req.Do()` method (lines 79-52) orchestrates the actual command execution with robust timeout and signal handling.

### Connection and Session Management

When `Do()` executes, it performs the following sequence:

1. Opens the SSH connection via `ssh.Dial` using the configured `host` and `port`
2. Starts a new session and applies environment variables from the `env` map
3. Optionally prepends a `cd` command if `workdir` is specified
4. Launches the command via `session.Start`
5. Captures `stdout` and `stderr` through separate pipes concurrently

### Timeout Handling and Signal Management

The plugin respects the user-defined `timeout` parameter via Go contexts:

- **Graceful termination**: On timeout, sends `SIGTERM` to the remote process
- **Force kill**: Falls back to `SIGKILL` if the process doesn't terminate gracefully
- **Context cancellation**: Uses Go's `context.WithTimeout` for precise deadline management

### Return Value Structure

The method returns a `Result` struct containing:

- **Original request**: The `Req` that was executed
- **Response data**: Exit code (`res.code`), stdout (`res.stdout`), stderr (`res.stderr`)
- **Runtime metrics**: Execution duration (`RT`)
- **Status**: High-level status code (`0` for success, `1` for failure)

## Writing SSH Workflows in YAML

Define remote command execution in your Probe workflow files using the `ssh` action. The `with` block maps 1-to-1 to the fields of the `Req` struct.

### Complete Workflow Example

```yaml
name: Deploy Application
jobs:
  - name: SSH Deploy
    steps:
      - name: Run remote script
        uses: ssh
        with:
          host: "prod.example.com"
          port: 22
          user: "deploy"
          key_file: "~/.ssh/deploy_key"
          cmd: |
            cd /opt/myapp
            git pull origin main
            systemctl restart myapp
          timeout: "2m"
          workdir: "/opt/myapp"
          env:
            DEPLOY_ENV: production
            GIT_SSH_COMMAND: "ssh -o StrictHostKeyChecking=yes"
          strict_host_check: true
          known_hosts: "~/.ssh/known_hosts"
        test: res.code == 0 && !contains(res.stderr, "error")

```

**Key parameters explained:**
- **host** (required): Target server hostname or IP address
- **user** (required): SSH username for authentication
- **cmd** (required): The command string to execute remotely
- **timeout**: Maximum execution time (e.g., `30s`, `5m`, `1h`)
- **workdir**: Remote directory to change into before executing the command
- **env**: Map of environment variables to set in the remote session

## Using the SSH Plugin Programmatically

For Go applications using Probe as a library, import the SSH plugin and invoke it through the generic `Execute` function (lines 80-31 in [`ssh/client.go`](https://github.com/linyows/probe/blob/main/ssh/client.go)).

### Basic Programmatic Usage

```go
package main

import (
	"fmt"
	"github.com/linyows/probe"
	_ "github.com/linyows/probe/actions/ssh" // registers the ssh plugin
)

func main() {
	workflow := map[string]any{
		"name": "Deploy",
		"jobs": []any{
			map[string]any{
				"name": "ssh-deploy",
				"steps": []any{
					map[string]any{
						"name": "Run remote",
						"uses": "ssh",
						"with": map[string]any{
							"host":     "prod.example.com",
							"user":     "deploy",
							"key_file": "~/.ssh/deploy_key",
							"cmd": `cd /opt/myapp && git pull origin main`,
							"timeout": "120s",
							"env": map[string]string{
								"DEPLOY_ENV": "production",
							},
						},
						"test": "res.code == 0",
					},
				},
			},
		},
	}

	out, err := probe.Execute(workflow)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Result: %+v\n", out)
}

```

The `probe.Execute` function parses the map, detects the `ssh` action, converts the parameters to a `Req` struct, calls `Do()`, and returns the `Result` as a generic `map[string]any`.

### Implementing Callback Hooks

The SSH plugin supports `Before` and `After` callbacks for logging or side effects via `WithBefore` and `WithAfter`:

```go
import "github.com/linyows/probe/ssh"

// Register a callback before connection
ssh.WithBefore(func(host string, port int, user string, cmd string) {
    fmt.Printf("[SSH] Connecting to %s@%s:%d → %s\n", user, host, port, cmd)
})

// Register a callback after execution
ssh.WithAfter(func(r *ssh.Result) {
    fmt.Printf("[SSH] Completed: exit=%d elapsed=%s\n", r.Res.Code, r.RT)
})

```

These hooks allow you to inject custom telemetry, audit logging, or debugging without modifying the core plugin implementation in [`ssh/client.go`](https://github.com/linyows/probe/blob/main/ssh/client.go).

## Summary

- **The SSH plugin** in Probe is implemented in [`ssh/client.go`](https://github.com/linyows/probe/blob/main/ssh/client.go) and provides a structured way to execute remote commands through the `Req` and `Res` structs.
- **Authentication flexibility** supports both password-based and public-key authentication (including encrypted keys with passphrases), configured through the workflow YAML or Go code.
- **Robust execution handling** includes configurable timeouts with SIGTERM/SIGKILL signal management, concurrent stdout/stderr capture, and working directory/environment variable support.
- **Integration options** include declarative YAML workflows for CI/CD pipelines or programmatic Go integration using `probe.Execute` with optional callback hooks for observability.

## Frequently Asked Questions

### How do I handle SSH host key verification in Probe?

Set `strict_host_check: true` in your workflow and provide a `known_hosts` file path (defaults to `~/.ssh/known_hosts`). According to the source code in [`ssh/client.go`](https://github.com/linyows/probe/blob/main/ssh/client.go), the plugin reads this file to verify the remote server's identity before establishing the connection, falling back to system defaults at `/etc/ssh/ssh_known_hosts` if not specified.

### What happens if my remote command exceeds the timeout?

The plugin uses Go's context-based timeout management. When the timeout expires, it first sends `SIGTERM` to the remote process; if the process doesn't exit gracefully, it escalates to `SIGKILL`. The `Res` struct will contain the exit code and any output captured before termination.

### Can I use environment variables in my remote commands?

Yes. The `Req` struct includes an `Env` field of type `map[string]string` that accepts arbitrary environment variables. In YAML, define them under the `env` key in the `with` block. The plugin sets these variables in the remote session before executing your command, making them available to scripts and applications run via the `cmd` parameter.

### How do I authenticate with an encrypted SSH private key?

Provide both the `key_file` path and the `key_passphrase` in your workflow configuration. The `createSSHConfig` function decrypts the private key using the passphrase before authentication, supporting standard OpenSSH encrypted key formats without requiring you to decrypt keys on disk.