Execute remote commands over SSH securely. Platform-agnostic — configure once, use anywhere.
Use forDon't use forConnecting to Linux servers via alias, IP, user, portStoring/displaying credentials in chat or logsInspection commands (read-only)Running destructive commands without user confirmationMaintenance with explicit --confirm-dangerousBypassing host-key verification (strict by default)Any credential backend: vault, env vars, key files, password promptsExposing private key contents anywhere
The skill works with any credential source. Configure what you have — no mandatory vault dependency.
TierSourceHowExample1. VaultAny vault that outputs to stdout--vault-key <name> + backend scriptBitwarden CLI, 1Password CLI, vault-resolver, HashiCorp Vault2. Env varsEnvironment variablesSSH_PASS, SSH_SUDO_PASS, SSH_KEYCI/CD pipelines, containerized agents3. DirectKey files, interactive prompts--key <path>, --ssh-pass-ask, --sudo-pass-askLocal development, one-off access
The default vault backend is vault-resolver (Hermes-native, Vaultwarden API). To use another vault:
bash
# 1Password CLI
export VAULT_RESOLVER_BIN="op"
# Bitwarden CLI
export VAULT_RESOLVER_BIN="bw"
# Custom script (must accept JSON on stdin, output JSON on stdout)
export VAULT_RESOLVER_BIN="/path/to/your/vault-wrapper"
See references/vault-backends.md for setup guides for each platform.
bash
# vault-resolver is auto-detected at /opt/data/bin/vault-resolver
# Store a key:
ssh-keys.sh store my-server ~/.ssh/id_rsa
# Use:
ssh-run.sh --host my-server --vault-key my-server -- 'uptime'
bash
# No vault — use env vars or key files:
export SSH_KEY="$(cat ~/.ssh/id_rsa)"
ssh-run.sh --host my-server --user ubuntu --key ~/.ssh/id_rsa -- 'uptime'
# Or with password (requires SSH_EXECUTOR_ALLOW_DANGEROUS=1):
SSH_EXECUTOR_ALLOW_DANGEROUS=1 SSH_PASS="<your-password>" ssh-run.sh --host my-server --user ubuntu -- 'uptime'
bash
ssh-run.sh --host ${{ secrets.SSH_HOST }} \
--user ${{ secrets.SSH_USER }} \
--key <(echo "${{ secrets.SSH_KEY }}") \
-- 'deploy.sh'
The remote server inventory (aliases, IPs, ports, users) lives in the wiki at /opt/data/wiki/entities/remote-servers.md. When the user mentions an alias not in ~/.ssh/config, check this file first — it contains the full table of all known servers.
Warning: The actual ~/.ssh/config may differ from what the wiki documents. In particular, the User directive is often not present for individual hosts. Always pass --user <user> explicitly in that case. The wiki contains the per-host user table.
Do not use this skill to:
restore-to-file unnecessarily — prefer ssh-agentscripts/ssh-run.sh and the correct parameters.ssh-run.sh and its backends follow a strict output contract:
StreamContentFormatstdoutPure JSON with command result{"success": bool, "exit_code": int, "stdout": "...", "stderr": "...", ...}stderrStatus messages (vault, warnings, progress)Free text
Rule: stdout is always parseable by json.load(). It never contains vault messages, ssh warnings, or any non-JSON text. This enables pipelines like:
bash
result=$(ssh-run.sh --host <your-host> --user <user> --vault-key id-rsa -- 'df -h' 2>/dev/null)
echo "$result" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['stdout'])"
--confirm-dangerous and explicit user approval.--sudo execution requires explicit user approval, even if the password is already in the vault. This includes consecutive commands on the same host — never automate sudo.ssh-run.sh JSON output intentionally omits key paths.ssh-agent, ssh-keys.sh restore writes decrypted key material to a temp file ( /tmp/ssh-vault-* or /dev/shm/ssh-vault-*) that is cleaned up by trap handlers. restore-to-file writes to a caller-specified path. Interruption by kill -9 prevents cleanup — prefer ssh-agent to avoid disk exposure entirely./var/log/auth.log, /var/log/secure, or similar security logs exposes usernames, source IPs, and authentication patterns — treat as privileged telemetry. Always get explicit user approval and consider redacting PII (usernames, IPs) before sharing results./dev/shm over /tmp for temporary key material. /dev/shm is RAM-backed tmpfs and survives reboots less often than /tmp. The scripts use /dev/shm when available and fall back to a secure temp directory otherwise. Multiplexing sockets (not key material) in /tmp are acceptable.This skill requires the following capabilities. Deploy with least-privilege toolset restrictions:
CapabilityScopeJustificationterminalBash scripts ( ssh-run.sh, ssh-keys.sh, ssh-run-native.sh)SSH command executionfileRead ~/.ssh/config, wiki server inventory, temp filesConfiguration resolutionenv``SSH_PASS, SSH_SUDO_PASS, SSH_AUTH_SOCKPassword/env-var auth pathswebvault-resolver API calls (localhost only)Credential resolution
Restricted: Never grant delegation or cronjob to this skill — SSH execution must always be directly supervised by the user.
Password-based SSH authentication and sudo are gated behind the SSH_EXECUTOR_ALLOW_DANGEROUS environment variable. Without it set to 1, these operations return exit code 98:
bash
# Enable dangerous operations (password auth + sudo):
export SSH_EXECUTOR_ALLOW_DANGEROUS=1
ssh-run.sh --host <host> --sudo --sudo-pass-vault <name> -- 'systemctl restart nginx'
# Without the env var → exit 98 "requires SSH_EXECUTOR_ALLOW_DANGEROUS=1"
This gate ensures the operator explicitly acknowledges the elevated risk at the environment level before any password or sudo operation is possible. Key-based auth (without sudo) does not require this gate.
For new server investigations or infrastructure mapping, follow this discovery order — each step answers one question before moving on:
StepQuestionTypical commands1. IdentityWho is this host? What role?hostname, cat /etc/os-release, uname -a2. RuntimeWhat is running?docker ps, systemctl list-units --type=service, ps aux, docker compose ps3. OriginWhere did it come from? Which repo/tag/volumes?docker inspect --format '{{.Config.Image}}' <container>, docker inspect --format '{{.Mounts}}' <container>, docker compose config4. NetworkWhat is listening?ss -tlnp, docker port <container>, iptables -L -n (with approval)5. DataWhere is persistent data?df -h, lsblk, docker volume ls, mount6. RegisterWhat do I know for sure vs. what am I inferring?Structured report (see below)
Narrow commands: each command should answer one question. Avoid docker inspect without a field filter — prefer --format '{{.Config.Image}}' or '{{.Mounts}}'. Avoid find / or grep -r /.
If at any step you encounter a path with .env, keystore/, secrets/, or credential files — do not read the contents, only register its existence.
When finishing a discovery or mapping, report in this format:
text
Host: <hostname>
Role: <role description>
Runtime:
- <container/service 1> → image:tag, compose: <path>
- <container/service 2> → image:tag, compose: <path>
Network:
- <port>: <protocol> → <description>
Data:
- <mount>: <host path>
Confirmed facts:
- ...
Inference / Assumptions:
- ... (mark explicitly)
Open questions:
- ...
Required approvals:
- ...
This separates evidence from interpretation — any downstream operator can reproduce the facts and question the conclusions.
--vault-key#As of 2026-07-22: --vault-key no longer forces the Python/Paramiko backend. The native backend ( ssh-run-native.sh) handles vault keys via ssh-keys.sh restore → ssh-agent. The Python/Paramiko backend ( ssh-client.py) is used only as a fallback when openssh-client is unavailable.
To avoid audit log spam ( auth.log) — where multiple SSH connections in a few seconds look like a brute-force attack — the native backend now uses OpenSSH connection multiplexing:
bash
# First command: opens connection + authenticates (1 entry in auth.log)
ssh-run.sh --host <your-host> -- 'hostname'
# Subsequent commands (within 180s): reuse socket (ZERO new entries)
ssh-run.sh --host <your-host> -- 'df -h'
ssh-run.sh --host <your-host> -- 'docker ps'
Parameters:
--control-persist <seconds> — how long the master socket stays alive after the last command (default: 180s)--control-close — close the master socket immediately after the command (for clean audit session termination)Socket path: /tmp/ssh-mux-%r@%h:%p (e.g. /tmp/ssh-mux-root@10.0.0.5:22)
Important: Multiplexing only works when all commands to the same host use the same user and port. If alternating between root@host and user@host, each combination gets its own socket.
Active multiplexing verification: references/multiplexing-verification.md.
For commands requiring sudo without configuring NOPASSWD on the server, the skill supports three password sources, resolved in priority order:
bash
# Store once:
vault-resolver write sudo-<your-host> sudo_password="my-password"
# Use:
SSH_EXECUTOR_ALLOW_DANGEROUS=1 ssh-run.sh --host <your-host> --user <user> --vault-key <key-name> \
--sudo --sudo-pass-vault <your-host> --confirm-dangerous \
-- 'systemctl restart nginx'
bash
SSH_EXECUTOR_ALLOW_DANGEROUS=1 SSH_SUDO_PASS="my-password" ssh-run.sh --host <your-host> --user <user> --vault-key <key-name> \
--sudo --confirm-dangerous -- 'systemctl restart nginx'
bash
# LLM calls with --sudo-pass-ask, script fails with instructions
SSH_EXECUTOR_ALLOW_DANGEROUS=1 ssh-run.sh --host <your-host> --user <user> --vault-key <key-name> \
--sudo --sudo-pass-ask --confirm-dangerous -- 'systemctl restart nginx'
# → exit 97: "Ask the user for the password, then retry with SSH_SUDO_PASS"
# LLM asks user, then retries:
SSH_EXECUTOR_ALLOW_DANGEROUS=1 SSH_SUDO_PASS="<user-provided>" ssh-run.sh ... --sudo --confirm-dangerous -- 'systemctl restart nginx'
Priority: vault → SSH_SUDO_PASS env var → --sudo-pass-ask
Security:
echo '<pass>' | sudo -S -p '' — password never appears on the command line ( ps)-p '' suppresses sudo password prompt (avoids stderr pollution)--sudo automatically enables --confirm-dangerous (no extra flag needed)--sudo execution must be re-confirmed by the user. Even with the password in the vault, the LLM must never run sudo commands without explicit approval on each call. This includes consecutive commands on the same host — each --sudo requires fresh confirmation.Vault format: item sudo-<name> with field sudo_password (plain text, not base64).
⚠️ Security tradeoff: Password-based SSH is weaker than key-based auth. Use only when keys are not available and the user has explicitly approved. Prefer key-based auth ( --vault-key or --key) whenever possible.
When no SSH key is available, the skill supports password-based authentication via sshpass, with the same three-source pattern:
bash
# Store once:
vault-resolver write ssh-<your-host> ssh_password="my-password"
# Use:
SSH_EXECUTOR_ALLOW_DANGEROUS=1 ssh-run.sh --host <your-host> --user <user> --ssh-pass-vault <your-host> -- 'df -h'
bash
SSH_EXECUTOR_ALLOW_DANGEROUS=1 SSH_PASS="my-password" ssh-run.sh --host <your-host> --user <user> -- 'df -h'
bash
SSH_EXECUTOR_ALLOW_DANGEROUS=1 ssh-run.sh --host <your-host> --user <user> --ssh-pass-ask -- 'df -h'
# → exit 96: "Ask the user for the password, then retry with SSH_PASS"
Priority: vault → SSH_PASS env var → --ssh-pass-ask
When key-based auth is available ( --vault-key or --key), it takes precedence over password auth. Password auth is only attempted when no key is configured.
Requires: sshpass ( apt install sshpass / brew install sshpass).
Vault format: item ssh-<name> with field ssh_password (plain text).
The skill supports pluggable vault backends via the VAULT_RESOLVER_BIN environment variable. The default backend is vault-resolver (Vaultwarden API, Hermes-native), but you can point it at any CLI that accepts JSON on stdin and returns JSON on stdout.
PlatformVault backendSetupHermes Agentvault-resolver (built-in)Zero config — auto-detectedBitwardenbw CLIexport VAULT_RESOLVER_BIN="bw"1Passwordop CLIexport VAULT_RESOLVER_BIN="op"HashiCorp Vaultvault CLIexport VAULT_RESOLVER_BIN="/path/to/vault-wrapper"NoneKey files onlySkip --vault-key, use --key or env vars
Path: /opt/data/bin/vault-resolver (hardcoded in scripts; override with VAULT_RESOLVER_BIN).
Key storage convention:
ssh-<name> with field ssh_private_key (base64)ssh-<name> with field ssh_password (plain text)sudo-<name> with field sudo_password (plain text)For self-signed certificate environments: vault-resolver uses SSL_CTX.check_hostname=False. Only safe on trusted local networks (LAN, VPN, localhost).
See references/vault-backends.md for custom backend integration and API contract.
bash
# ALWAYS pass --vault-key to connect with a vault key
scripts/ssh-run.sh --host <your-host> --vault-key id-rsa -- 'command'
# Does NOT work without --vault-key — IdentityFile from SSH config may not exist
If --vault-key fails because the SSH key was never stored in Vaultwarden ( Expecting value error), use direct SSH with the local key on disk:
bash
ssh -o StrictHostKeyChecking=yes -i ~/.ssh/id_rsa <user>@<host> '<command>'
If this is first contact with the host, verify its fingerprint manually before connecting. Add it to ~/.ssh/known_hosts via ssh-keyscan or accept interactively. Never use StrictHostKeyChecking=no for unknown hosts.
This bypasses the vault-resolver dependency. The native backend with ssh-agent is the current default. Use this fallback only for read-only commands (df, ps, cat, ls). After confirming it works, consider storing the key in the vault:
bash
ssh-keys.sh store id-rsa ~/.ssh/id_rsa
This way, --vault-key will work normally in future sessions.
Warning: The ssh-run.sh wrapper flags commands with 2>/dev/null, |, &&, || as dangerous (exit 99). When using direct SSH, these redirections work without blocking.
ProblemCauseSolutionPermission denied (publickey)``--vault-key not passed; tries IdentityFile from SSH configAdd --vault-key <name>``'ssh-id-rsa' not foundvault-resolver looks in login.password, but key is in notesUse ssh-keys.sh store to recreate with custom field ssh_private_key (see references/vault-key-format.md)Failed to resolve vault key <name>: Expecting valueThe SSH key does not exist in Vaultwarden — item ssh-<name> was never created, is empty, or has invalid formatFall back to direct SSH with local key (see "Fallback without vault-key" below), then store the key in vault with ssh-keys.sh store <name> ~/.ssh/id_rsa``No module named 'paramiko'Python/Paramiko backend (fallback, used only when openssh-client is unavailable on the system) but paramiko not installeduv pip install --target ~/.openclaw/workspace/.ssh-pylib paramiko``timed out (SSH connection)Wrong port or hostname — Python backend wasn't resolving SSH configNow resolves HostName/Port/User from ~/.ssh/config automaticallyauthentication failed(a) SSH key was never deployed to server's authorized_keys — Paramiko rejects during key exchange; (b) vault-resolver returned key from wrong field (notes vs field)(a) Fall back to direct SSH with local key (see "Fallback without vault-key" below) and confirm the key works; then optionally store with ssh-keys.sh store <name> <path>; (b) Use ssh-keys.sh store to recreate in correct formatssh-keys.sh timeout (~30s)vault-resolver authentication via bw CLI was slow (~18s)Direct API rewrite reduces to ~2s with session cachePermissionError: /tmp/.bw_hermes/api_session.jsonvault-resolver cache created by different UID (e.g. 1000 vs 10000)Set BITWARDENCLI_APPDATA_DIR to a writable directory, e.g. BITWARDENCLI_APPDATA_DIR=/opt/data/tmp/bw-cache before calling vault-resolver or ssh-run.sh with --vault-keycat with redirections or short pipes blocked as "dangerous" (exit 99)ssh-run.sh heuristic detects 2>/dev/null, pipes `or&& /``find command with pipe blocked as "dangerous" (exit 99)False positive from heuristic: `find ...while read d; do ts=$(stat -c %Y "$d"); ... contains stat` + pipes that trigger the dangerous command detector, even though it's read-onlyControlSocket /tmp/ssh-mux-... already existsMaster socket from a previous session is still active (within ControlPersist)Normal and expected — means multiplexing is working. To force a new socket, use --control-close on the previous command or wait for TTL to expire**Host key verification failed despite correct known_hosts**SSH config has UserKnownHostsFile /dev/null which discards all host keysThe script now forces UserKnownHostsFile when StrictHostKeyChecking=yes. Ensure host keys are in ${HOME}/.ssh/known_hosts**Group membership not updated (e.g. sudo, docker)**ControlMaster socket caches the group list from the initial connectionClose the socket first: ssh -O exit <host> or --control-close, then reconnect for a fresh session with updated groupsControlPersist doesn't work with different hosts even if same IPControlPath uses %h (hostname), not IPUse the same alias/hostname in all calls. If you need to switch between alias and IP, standardize on the ~/.ssh/config aliassudo: a terminal is required even with --pty``--pty allocates a PTY, but if sudo asks for a password, the command hangs waiting for inputConfigure NOPASSWD in sudoers or use --sudo with a password source: --sudo-pass-vault, SSH_SUDO_PASS env var, or --sudo-pass-ask``--sudo requires a password source (exit 97)No vault item, no env var, and no --sudo-pass-ask flag providedProvide one of: --sudo-pass-vault <name>, SSH_SUDO_PASS env var, or --sudo-pass-ask``Failed to resolve sudo password from vault: sudo-<name> (exit 97)The item sudo-<name>/sudo_password does not exist in VaultwardenCreate the item: vault-resolver write sudo-<name> sudo_password="password"``SSH password auth requires SSH_EXECUTOR_ALLOW_DANGEROUS=1 (exit 98)Password-based SSH auth attempted without environment opt-inSet export SSH_EXECUTOR_ALLOW_DANGEROUS=1 to acknowledge the elevated risk, then retry. Key-based auth does not require this gate.sudo requires SSH_EXECUTOR_ALLOW_DANGEROUS=1 (exit 98)sudo attempted without environment opt-inSet export SSH_EXECUTOR_ALLOW_DANGEROUS=1 to acknowledge the elevated risk, then retry with --confirm-dangerous.ssh-run.sh JSON output contains vault messages ("Retrieving SSH key...")Old version of ssh-keys.sh (< 2026-07-22) wrote info messages to stdoutUpdate to the current version: all cmd_restore() messages now go to stderr. Stdout contains only JSON--vault-key loads key but falls back to Python backend (no multiplexing)ssh-agent is not running. ssh-keys.sh restore detects the absence and creates a temp file for the Python backend, which lacks ControlMasterStart ssh-agent first: eval "$(ssh-agent -s)". Without an active agent, each --vault-key generates a new TCP connection = auth.log spamPermission denied (publickey) with wrong resolved_user~/.ssh/config has no User directive for the host, and the alias doesn't specify a user. SSH uses the local system userPass --user <user> explicitly: ssh-run.sh --host <your-host> --user <user> --vault-key <key-name> -- 'command'. Or add User <user> to the Host entry in ~/.ssh/config``sshpass: command not found (exit 127) when using --ssh-pass-*``sshpass is not installed on the systemInstall: apt install sshpass (Debian/Ubuntu) or brew install sshpass (macOS). sshpass is only needed for password-based SSH auth; key-based auth does not require itPermission denied (password) with --ssh-pass-*SSH server does not allow password authentication ( PasswordAuthentication no in sshd_config)Enable PasswordAuthentication yes on the server, or use key-based auth insteadHost key verification failed (exit 255) on first contactDefault is strict ( StrictHostKeyChecking=yes). Host not in ~/.ssh/known_hosts.Manually verify the host fingerprint, then run: ssh-keyscan <host> >> ~/.ssh/known_hosts. Do NOT use --host-key-checking no as a shortcut.paramiko.ssh_exception.SSHException: Server ... not found in known_hosts (Python backend)Python backend uses RejectPolicy by default — same strict behaviour as nativeUse native backend if possible (has multiplexing). If Python is required, verify host fingerprint and add to ~/.ssh/known_hosts, or use --host-key-checking no only with explicit user approval.restore-to-file leaves key on disk after crashssh-keys.sh restore-to-file writes decrypted key to a caller-specified path. Interruption (kill -9) prevents cleanup.Prefer ssh-keys.sh restore (loads into ssh-agent, no disk). Use restore-to-file only as last resort. Verify cleanup after use.shred: command not found (macOS) during key cleanupmacOS does not ship shred. The fallback rm -f is used but does not securely overwrite blocks.Acceptable on macOS (APFS encryption at rest mitigates). On Linux, ensure coreutils is installed.restore-to-file fails with "Refusing to write private key to system directory"Path validation rejects system dirs: /etc, /boot, /sys, /proc, /dev, /runUse a path under /tmp/ or your home directory.restore-to-file warns about non-tmp pathOutput path not under /tmp or /dev/shm — key will persist across rebootsUse /tmp/ for temporary key material.⚠️ SECURITY WARNING: Host-key checking disabled in stderr--host-key-checking no was passed — connection is MITM-vulnerableConfirm with the user that they understand and accept the risk. Prefer adding the host key to known_hosts instead.Host key verification failed despite host key in known_hosts~/.ssh/config has UserKnownHostsFile /dev/null — SSH ignores known hosts. Strict checking has no trust store.Native backend now forces -o UserKnownHostsFile=${HOME}/.ssh/known_hosts when strict. Grep config for UserKnownHostsFile directives pointing to /dev/null. See references/testing-pitfalls-2026-07-23.md.ssh-keys.sh restore exits 1 but prints "✓ loaded" (ghost failure)set -e + trap with shred -u on already-deleted temp file. EXIT trap fires after success path deleted file.Fixed: trap uses rm -f. Explicit shred -u runs in code paths; trap is safety net. See references/testing-pitfalls-2026-07-23.md.Temp key survives kill -9``trap cannot catch SIGKILL. If the process is hard-killed during ssh-keys.sh restore, the temp file at /dev/shm/ssh-vault-* (or /tmp/ssh-vault-* as fallback) remains.Use ssh-keys.sh cleanup to list stale keys and ssh-keys.sh cleanup --force to shred them. The files are also cleaned on next normal invocation when the same key name is restored.
ssh-executor scripts use /opt/data/bin/vault-resolver (hardcoded path).
bash
export PATH="/opt/data/bin:$PATH"
⚠️ DEPRECATED PATTERN — see
references/server-to-server-rsync.mdfor security warning and preferred alternatives.
When you need to copy files between two remote servers and there is no ssh-agent running locally (making ForwardAgent impossible), prefer the SSH tunneled variant (ForwardAgent) or pull-via-jump approach. Copying a private key to a remote server is a last resort — use only with a dedicated single-purpose key and explicit user approval.
scripts/ssh-run.sh#Main remote execution script. Automatically selects backend:
ssh/ ssh-agent/ ssh-add are available (includes vault-key via ssh-agent)bash
scripts/ssh-run.sh --host <host> [--user <user>] [--port <port>] \
[--key <path>] [--vault-key <name>] \
[--timeout <sec>] [--host-key-checking <yes|no>] \
[--confirm-dangerous] \
[--control-persist <sec>] [--control-close] \
[--sudo --sudo-pass-vault <name>] \
-- '<remote command>'
scripts/ssh-run.sh --list-aliases # list aliases from ~/.ssh/config
--vault-key <name> retrieves the SSH key from Vaultwarden (item ssh-<name>) and loads it into ssh-agent. With an active agent, the key stays in memory. Without ssh-agent, a temp file in RAM-backed /dev/shm is used as fallback (cleaned by trap handlers; see security caveats).
--control-persist <sec> how long the multiplexing socket stays alive (default: 180).
--control-close closes the master socket immediately after the command.
--pty allocates a pseudo-terminal (needed for sudo in non-interactive sessions).
--sudo runs the command with sudo using password from vault, env var, or user prompt.
--sudo-pass-vault <name> Vaultwarden item name with the sudo password ( sudo-<name>/sudo_password).
--sudo-pass-ask signals the LLM to ask the user for the password (retry with SSH_SUDO_PASS env var).
--ssh-pass-vault <name> SSH password from Vaultwarden item ssh-<name>/ssh_password (used when no key is configured).
--ssh-pass-ask signals the LLM to ask the user for the SSH password (retry with SSH_PASS env var).
scripts/ssh-keys.sh#Manages SSH keys in Vaultwarden.
bash
scripts/ssh-keys.sh store <name> <path> # store local key in vault
scripts/ssh-keys.sh restore <name> # load from vault into agent (or temp file)
scripts/ssh-keys.sh restore-to-file <name> <out> # write from vault to file
scripts/ssh-keys.sh cleanup # list stale temp key files
scripts/ssh-keys.sh cleanup --force # shred stale temp key files
scripts/ssh-keys.sh agent-status # show keys loaded in agent
references/multiplexing-verification.md#Verification procedure to confirm ControlMaster multiplexing is active: 6-step checklist (socket timestamp, mux PID, ss, auth.log), failure signals, and quick diagnostic command.
references/vault-backends.md#API contract and setup guides for pluggable vault backends: vault-resolver (Hermes), Bitwarden CLI, 1Password CLI, HashiCorp Vault wrapper, and no-vault mode.
references/vault-ssh-integration.md#Detailed architecture of how SSH keys are stored in Vaultwarden and loaded into memory. Read when you need to understand the security model or full flow.
references/docker-diagnostics-without-cli.md#Techniques for diagnosing Docker containers remotely without access to the
Docker CLI (user without docker group, without sudo NOPASSWD). Lists 10 command
techniques ( cgroup, /proc/PID, ip neigh, ss, /dev/tcp, docker-compose,
config files) and a real-world MariaDB case with network_mode: host that wasn't
listening on any port.
references/safety.md#Detailed security rules for remote execution. Read when in doubt about what is considered destructive or how to handle sensitive hosts.
references/security-audit-2026-07-clawhub.md#Complete ClawHub SkillSpector audit history across versions. Documents all findings, fixes, and false-positive classifications from v2.1.0 (47 findings) through v2.3.6.
The skill is split into two editions, built from the same source via build.sh:
EditionPackageAuthSudoLinesBasessh-executor-<ver>.zipKey onlyNo270Fullssh-executor-full-<ver>.zipKey + passwordYes481
Base is the default. No sudo, no password auth, no sshpass — minimal attack surface.
Full adds sudo + SSH password auth behind SSH_EXECUTOR_ALLOW_DANGEROUS=1.
text
scripts/
├── ssh-run-native.sh ← full (sudo + password)
├── ssh-run-native-base.sh ← stripped (key-only)
├── ssh-run.sh / ssh-run-base.sh
├── ssh-keys.sh / ssh-client.py ← shared
SKILL.md / SKILL.base.md ← full / stripped docs
references/
├── safety.md / safety.base.md
└── ...
build.sh ← bash build.sh <version>
Design: separate files over marker-based stripping. Simpler to maintain and audit. ClawHub SkillSpector security audit results (47 findings). Documents all high-confidence vulnerabilities found and how they were fixed in v2.2.0. Read when maintaining or extending the skill to avoid reintroducing known issues: host-key bypass, credential mismanagement, key exfiltration, undeclared MCP capabilities.
references/remote-backup-cleanup.md#Remote backup cleanup pattern with find -mtime +N -delete. Covers the permission model (write access to directory vs. file ownership), solutions for Permission denied, and diagnostic commands with namei. Refer to when deleting old files in remote backup directories.
These phrases require explicit user invocation — the skill should NOT auto-trigger from casual conversation about servers.
The skill triggers on phrases that explicitly reference ssh-executor or the ssh-run.sh / ssh-keys.sh scripts. Casual mentions of "server", "deploy", or "SSH" alone should NOT activate this skill.