Skip to main content

on

Execute commands on SSH hosts.

Synopsis

xec on [options] <hosts> [command...]

Description

The on command executes commands, scripts, or tasks on remote SSH hosts. It provides unified SSH execution with support for parallel operations, pattern matching, and direct SSH connections.

Arguments

  • <hosts> - SSH host specification (patterns supported)
  • [command...] - Command to execute (optional for REPL/interactive modes)

Host Specification

Hosts can be specified as:

  • hosts.name - Configured SSH host
  • hosts.* - All configured SSH hosts
  • hosts.web-* - Pattern matching
  • {hosts.web1,hosts.web2} - Multiple specific hosts
  • user@hostname - Direct SSH connection
  • hostname - Direct connection (uses current user)

Options

Common Options

  • -o, --output <format> - Output format: text (default), json, yaml, csv
  • -c, --config <path> - Path to configuration file

General Options

  • -p, --profile <profile> - Configuration profile to use
  • -i, --interactive - Interactive mode for selecting hosts and options
  • --task <task> - Execute a configured task on the hosts
  • --repl - Start a REPL session with $target available
  • -t, --timeout <duration> - Command timeout (e.g., 30s, 5m)
  • -e, --env <key=value> - Environment variables (can be used multiple times)
  • -d, --cwd <path> - Working directory on remote host
  • -u, --user <user> - User to run command as (overrides config)
  • --parallel - Execute on multiple hosts in parallel
  • --max-concurrent <n> - Maximum concurrent executions (default: 10)
  • --fail-fast - Stop on first failure in parallel mode
  • --max-failures <n> - Stop after n failures, or a share of the fleet (20%)
  • -v, --verbose - Enable verbose output
  • -q, --quiet - Suppress output
  • --dry-run - Preview execution without running

Examples

Basic Command Execution

Execute simple commands on SSH hosts:

# Single host
xec on hosts.web-1 "uptime"

# Multiple hosts with pattern
xec on hosts.web-* "df -h"

# All SSH hosts
xec on hosts.* "date"

# Direct SSH connection
xec on deploy@server.com "ls -la"

Parallel Execution

Run commands on multiple hosts simultaneously:

# Check service status on all web servers
xec on hosts.web-* "systemctl status nginx" --parallel

# Deploy to multiple servers
xec on hosts.app-* "./deploy.sh" --parallel --max-concurrent 5

# Stop on first failure
xec on hosts.* "npm test" --parallel --fail-fast

# Or tolerate a few: stop once a fifth of the fleet has failed
xec on hosts.* "./deploy.sh" --parallel --max-failures 20%

Identical output is grouped, so the machine that disagrees is the one you see rather than the one you have to find:

$ xec on hosts.web-* "cat /etc/os-release | head -1" --parallel
web-1, web-2, web-4, web-5
PRETTY_NAME="Ubuntu 24.04.1 LTS"
web-3
PRETTY_NAME="Ubuntu 22.04.5 LTS"
✓ 5/5 in 1.2s

Both limits stop new hosts from starting. Hosts already running are allowed to finish, so their results are reported rather than discarded, and any host that never started is named.

Script Execution

Run scripts with SSH context:

# Execute TypeScript script
xec on hosts.db-master ./scripts/backup.ts

# Execute JavaScript file
xec on hosts.worker ./migrate.js

# Script with $target context
xec on hosts.app ./deploy-script.js

Task Execution

Run configured tasks on hosts:

# Run deploy task
xec on hosts.production --task deploy

# Run backup task on all databases
xec on hosts.db-* --task backup --parallel

# Run health check task
xec on hosts.* --task health-check

REPL Mode

Start REPL with SSH context:

# Start REPL for single host
xec on hosts.dev --repl

# Access host in REPL
# > await $target`ls -la`
# > const mem = await $target`free -m`

Interactive Mode

Use interactive prompts for configuration:

# Interactive host selection and execution
xec on --interactive

# Prompts for:
# - Host selection (single/multiple)
# - Execution type (command/script/task/REPL)
# - Execution options (parallel, timeout, etc.)

Environment Variables

Set environment variables for remote execution:

# Single variable
xec on hosts.app -e NODE_ENV=production "node server.js"

# Multiple variables
xec on hosts.worker -e DB_HOST=localhost -e DB_PORT=5432 "./process.sh"

# Override PATH
xec on hosts.build -e PATH=/custom/bin:$PATH "which node"

Working Directory

Set remote working directory:

# Change to specific directory
xec on hosts.app -d /var/www/html "git pull"

# Run from user home
xec on hosts.backup -d ~ "./backup.sh"

# Temporary directory
xec on hosts.temp -d /tmp "rm -rf old-files-*"

Timeout Control

Set execution timeouts:

# 30 second timeout
xec on hosts.test -t 30s "npm test"

# 5 minute timeout for longer operations
xec on hosts.build -t 5m "make all"

# 1 hour for backup operations
xec on hosts.backup -t 1h "./full-backup.sh"

User Override

Execute as different user:

# Run as root
xec on hosts.app -u root "apt-get update"

# Run as deploy user
xec on hosts.production -u deploy "./deploy.sh"

# Override configured user
xec on hosts.db -u postgres "psql -c 'SELECT version()'"

Advanced Usage

Pattern Matching

Use patterns to select multiple hosts:

# Wildcard matching
xec on hosts.web-* "nginx -t"

# Brace expansion
xec on {hosts.web1,hosts.web2,hosts.db1} "uptime"

# All hosts
xec on hosts.* "uname -a"

# Complex patterns
xec on "hosts.{prod,staging}-web-*" "systemctl status"

Direct SSH Connections

Connect to hosts not in configuration:

# User@host format
xec on admin@192.168.1.100 "ls -la"

# Hostname only (uses current user)
xec on example.com "date"

# Multiple direct connections
xec on user@host1,user@host2 "uptime" --parallel

Parallel Execution Control

Fine-tune parallel execution:

# Limited concurrency
xec on hosts.* "heavy-task.sh" --parallel --max-concurrent 3

# Fail fast on error
xec on hosts.prod-* "test.sh" --parallel --fail-fast

# Progress reporting
xec on hosts.* "backup.sh" --parallel -v

Command Chaining

Execute multiple commands:

# Sequential commands
xec on hosts.app "cd /app && git pull && npm install && npm run build"

# Conditional execution
xec on hosts.db "pg_isready && pg_dump mydb > backup.sql"

# Pipe operations
xec on hosts.log "tail -f /var/log/app.log | grep ERROR"

Script Context

Scripts have access to SSH context:

// deploy.js
const hostname = await $target`hostname`.text();
console.log(`Deploying to ${hostname}`);

await $target`cd /app && git pull`;
await $target`npm install`;
await $target`npm run build`;

Configuration

Configure SSH hosts in .xec/config.yaml:

targets:
hosts:
web-1:
type: ssh
host: 192.168.1.10
username: deploy
port: 22
privateKey: ~/.ssh/id_rsa

db-master:
type: ssh
host: db.example.com
username: admin
password: $SSH_PASSWORD # From environment

bastion:
type: ssh
host: bastion.example.com
username: jump
# SSH config for ProxyJump handled

commands:
on:
parallel: true
maxConcurrent: 10
timeout: "1m"

SSH Features

Connection Options

The command respects SSH configuration:

# Uses SSH config file
xec on hosts.configured "date"

# Direct connection with defaults
xec on new-host.com "uptime"

# Connection pooling for efficiency
xec on hosts.* "quick-check" --parallel

Authentication

Supports multiple authentication methods:

  • SSH keys (preferred)
  • SSH agent
  • Password authentication
  • SSH config ProxyJump

Connection Pooling

Connections are pooled for efficiency:

  • Reuses existing connections
  • Parallel commands share connections
  • Automatic cleanup on exit

Error Handling

The command handles SSH errors gracefully:

  • Connection refused
  • Authentication failed
  • Host unreachable
  • Command not found
  • Permission denied
  • Timeout exceeded

Use -v, --verbose for detailed error information.

Performance Considerations

  • Connection pooling reduces overhead
  • Parallel execution for multiple hosts
  • Streaming for large outputs
  • Efficient for batch operations

Security Notes

  • SSH keys preferred over passwords
  • Passwords read from environment variables
  • Sensitive data not logged
  • Respects SSH known_hosts

Troubleshooting

Connection Issues

# Test SSH connection
ssh -v user@host

# Check SSH config
cat ~/.ssh/config

# Verify host key
ssh-keyscan hostname

Authentication Problems

# Check SSH agent
ssh-add -l

# Test with specific key
xec on hosts.server "date"
# Check config for correct key path

Timeout Issues

# Increase timeout for slow connections
xec on hosts.remote -t 5m "long-running-task"

# Check network latency
ping hostname
  • in - Execute in containers/pods
  • run - Run scripts and tasks
  • forward - SSH port forwarding

Output

stdout carries the answer; stderr carries everything said about producing it. Progress, warnings, per-host failures and the final tally are all diagnostics, so xec on web-1 "cat /etc/hosts" > hosts writes the file and nothing else.

With one host, its stdout is passed through byte for byte — no trimming, no prefix. With several, output is grouped as shown above, and the grouped report is itself the answer, so it goes to stdout.

A command's own stderr always reaches stderr. It does not need --verbose.

-o json replaces all of it with one document:

{
"command": "uptime",
"ok": false,
"total": 3,
"succeeded": 2,
"failed": 1,
"durationMs": 812,
"targets": [
{ "target": "web-1", "ok": true, "exitCode": 0, "stdout": "…", "stderr": "", "durationMs": 210 }
],
"skipped": []
}

Every field is always present, including the empty ones.

Exit Codes

  • 0 - Every host succeeded
  • The command's own exit code, when exactly one host ran and exited non-zero — so xec on web-1 "test -f /etc/nginx.conf" can be used in an if
  • 1 - Anything else: a host that could not be reached, a pattern that matched nothing, several hosts failing with no single code to report