# How to Execute Local Shell Commands with Probe's Shell Plugin

> Execute local shell commands easily with Probe's shell plugin. Learn how to run synchronous tasks background jobs custom shells and inject environment variables for powerful workflow automation.

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

---

**Probe's shell plugin enables arbitrary command execution on the host machine through workflow steps defined with `uses: shell`, supporting synchronous runs, background tasks, custom shells, and environment variable injection.**

The `linyows/probe` repository provides a built-in shell action that integrates with Probe's workflow engine to execute local commands securely. Whether you need to run build scripts, capture command output for later steps, or launch background processes, the shell plugin handles process management, validation, and result capture through a structured Go API.

## Understanding the Shell Plugin Architecture

The shell plugin implementation resides in [`shell/client.go`](https://github.com/linyows/probe/blob/main/shell/client.go) and exposes a request-response model centered around the **`shell.Req`** struct. This struct holds the command string, shell binary path, working directory, timeout duration, environment map, and a background execution flag.

When the workflow engine encounters a `uses: shell` step, it delegates to the action wrapper in [`actions/shell/main.go`](https://github.com/linyows/probe/blob/main/actions/shell/main.go). This wrapper validates the presence of the `with` parameter map, registers optional lifecycle callbacks using `shell.WithBefore` and `shell.WithAfter`, and invokes **`shell.Execute`** to process the request.

The execution flow follows these distinct phases:

1. **Parameter Parsing**: The `parseParams` function validates required fields, applies defaults via `shell.NewReq` (which sets `/bin/sh` as the default shell and `30s` as the default timeout), and verifies the shell path against an allowed whitelist.
2. **Path Validation**: The `validateShellPath` function restricts execution to known shells including `/bin/sh`, `/bin/bash`, `/bin/zsh`, `/bin/dash`, and their `/usr/bin/*` equivalents for security hardening.
3. **Command Execution**: The **`Req.Do`** method constructs an `exec.Cmd` with the specified shell and `-c` flag, applies the working directory and environment variables, then runs either synchronously or asynchronously based on the `Background` flag.
4. **Result Capture**: For synchronous runs, the method captures stdout, stderr, exit code, PID, and runtime duration. For background tasks, it returns immediately with exit code `-1` and a temporary log file path for output tracking.

## Configuring Shell Command Execution

Workflow steps using the shell plugin accept several parameters within the `with` map. All parameters are optional except `cmd`, which specifies the command string to execute.

- **`cmd`**: The shell command string (required).
- **`shell`**: Absolute path to the shell binary (default: `/bin/sh`).
- **`workdir`**: Working directory for command execution (must exist).
- **`timeout`**: Duration string like `10s` or `5m` (default: `30s`).
- **`env`**: Map of environment variables to inject.
- **`background`**: Boolean flag to run the command asynchronously.

The plugin enforces strict validation: the working directory must exist on the filesystem, and only whitelisted shell binaries are permitted. Environment variables do not inherit from the host process unless explicitly added to the `env` map.

## Running Commands in Workflows

Define shell execution steps in your workflow YAML using the `uses: shell` directive. The engine unmarshals the `with` map into a `shell.Req` and processes the action.

### Basic Command Execution

```yaml
name: Shell Action Demo
jobs:
  - name: Run Local Commands
    steps:
      - name: Simple echo
        uses: shell
        with:
          cmd: "echo 'Hello, Probe!'"

```

This minimal configuration uses `/bin/sh` with a 30-second timeout.

### Custom Shell and Environment

```yaml
      - name: Use custom shell and env
        uses: shell
        with:
          cmd: "echo \"Running as $USER on $PROJECT_DIR\""
          shell: "/bin/bash"
          env:
            USER: "probe_user"
            PROJECT_DIR: "/opt/project"
          workdir: "/tmp"
          timeout: "10s"

```

The `shell` parameter overrides the default, while the `env` map explicitly defines variables available to the command context.

### Background Execution

```yaml
      - name: Background task
        uses: shell
        with:
          cmd: "sleep 60 && echo 'done'"
          background: true

```

When `background` is set to `true`, the workflow continues immediately while the process runs detached. The step result includes the `pid` and a temporary log file path instead of stdout/stderr content.

## Accessing Command Output

Probe's workflow engine exposes step results through template variables, allowing subsequent steps to access the output of previous shell commands.

```yaml
jobs:
  - name: Capture and reuse output
    steps:
      - id: generate
        uses: shell
        with:
          cmd: "uuidgen"
      - id: show
        uses: shell
        with:
          cmd: "echo \"Previous UUID: {{steps.generate.result.res.stdout}}\""

```

The `{{steps.<id>.result.res.stdout}}` variable retrieves the standard output from the previous step's execution, enabling dynamic data flow between workflow stages.

## Implementing Go Integration

For direct integration outside of workflow files, import the `shell` package and construct parameter maps matching the YAML `with` structure.

```go
package main

import (
	"fmt"
	"github.com/linyows/probe/shell"
)

func main() {
	params := map[string]any{
		"cmd":     "date; echo $MY_VAR",
		"env":     map[string]string{"MY_VAR": "probe-demo"},
		"shell":   "/bin/bash",
		"workdir": "/tmp",
		"timeout": "10s",
	}

	before := shell.WithBefore(func(cmd, sh, wd string) {
		fmt.Printf("About to run: %s (shell=%s, workdir=%s)\n", cmd, sh, wd)
	})
	
	after := shell.WithAfter(func(r *shell.Result) {
		fmt.Printf("Finished (code=%d, rt=%s)\n", r.Res.Code, r.RT)
		fmt.Println("stdout:", r.Res.Stdout)
	})

	out, err := shell.Execute(params, before, after)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Result map: %+v\n", out)
}

```

The `shell.Execute` function accepts the parameter map and optional callback options. **Callbacks** provide hooks into the execution lifecycle: `WithBefore` receives the resolved command, shell, and workdir before execution, while `WithAfter` receives the complete `Result` struct containing stdout, stderr, exit code, PID, runtime, and status.

## Security and Validation

The shell plugin implements multiple security controls in [`shell/client.go`](https://github.com/linyows/probe/blob/main/shell/client.go) to prevent arbitrary code execution vulnerabilities:

- **Shell Whitelist**: The `validateShellPath` function restricts the `shell` parameter to known interpreters (`/bin/sh`, `/bin/bash`, `/bin/zsh`, `/bin/dash`, and `/usr/bin/*` variants).
- **Directory Validation**: The `validateWorkdir` function ensures the specified working directory exists before execution begins.
- **Environment Isolation**: The plugin passes only explicitly defined environment variables; it does not inherit the complete host environment.
- **Timeout Enforcement**: All commands respect the configured timeout to prevent hanging processes.

## Summary

- The shell plugin in `linyows/probe` executes commands through the `shell.Req` struct and `Req.Do` method, wrapped by the action handler in [`actions/shell/main.go`](https://github.com/linyows/probe/blob/main/actions/shell/main.go).
- Configure commands via the `with` map using `cmd`, `shell`, `workdir`, `timeout`, `env`, and `background` parameters.
- Background execution returns immediately with a PID and log file path, while synchronous execution captures stdout, stderr, and exit codes.
- Access previous step outputs using `{{steps.<id>.result.res.stdout}}` in workflow templates.
- Security controls include shell path whitelisting, working directory validation, and isolated environment variable handling.

## Frequently Asked Questions

### What shells are supported by Probe's shell plugin?

The plugin maintains a whitelist of allowed shells including `/bin/sh`, `/bin/bash`, `/bin/zsh`, `/bin/dash`, and their `/usr/bin/*` equivalents. The `validateShellPath` function in [`shell/client.go`](https://github.com/linyows/probe/blob/main/shell/client.go) rejects any shell binary outside this list to prevent execution of arbitrary interpreters.

### How do I capture the output of a shell command for use in later steps?

Assign an `id` to your shell step, then reference the output using the template variable `{{steps.<id>.result.res.stdout}}` in subsequent steps. The workflow engine stores the complete `Result` struct, including `stdout`, `stderr`, `Code`, `Pid`, and `RT` (runtime), accessible through the `result` field.

### Can I run long-running commands without blocking the workflow?

Yes. Set `background: true` in the `with` map. When enabled, `Req.Do` launches the process detached from the workflow execution, returns immediately with exit code `-1`, and provides the process ID and a temporary log file path in the result. The workflow continues to subsequent steps while the command runs independently.

### What is the default timeout for shell commands, and how do I change it?

The default timeout is `30s`, set by the `shell.NewReq` function. Override this by specifying a duration string in the `timeout` parameter, such as `10s` for ten seconds or `5m` for five minutes. The `parseTimeout` function in [`shell/client.go`](https://github.com/linyows/probe/blob/main/shell/client.go) parses these strings into `time.Duration` values.