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