SSHX Documentation
SSH is the channel. X is execution.
sshx is an agent-native remote host execution tool. It uses SSH/SFTP to reach existing hosts and brings target resolution, execution preview, safety checks, command and file actions, structured results, and audit evidence into one CLI invocation.
It keeps the operating model simple: one command opens one connection, performs one explicit action, returns a decidable result, writes a local audit event, and exits. Nothing needs to be installed on the remote host and no long-running control plane is introduced.
The documentation starts in English by default. Use the language switch in the top navigation bar to open the matching Chinese page.
What SSHX Is Good At
- Run a remote command with predictable stdout, stderr, and exit-code behavior.
- Save sudo passwords in the operating system keyring instead of plaintext files.
- Use short host names from
~/.sshx/settings.jsoninstead of repeating IP, port, user, and key paths. - Perform small SFTP tasks without opening an interactive client.
- Replace one remote regular file with a hash, backup, and atomic write.
- Produce JSON output that scripts and AI agents can branch on.
- Preview local execution plans with
--dry-runbefore connecting, reading secrets, mutatingknown_hosts, or writing host config. - Keep a local JSONL audit trail without recording plaintext passwords, private keys, stdout, or stderr.
- Inspect system/network state in one call and create reusable application plugins under the sshx runtime root.
Mental Model
Think of sshx as a remote execution primitive in an agent’s toolbox, not an interactive shell replacement and not a desired-state or workflow orchestration platform.
agent, automation, or human operator
|
v
agent contract: CLI / JSON / exit code / dry-run
|
v
X execution: target / safety / action / audit
|
v
SSH channel: auth / host key / SSH exec / SFTP
|
v
remote host
Common First Commands
# See available flags and examples
sshx --help
# Run a simple command
sshx -h=192.168.1.100 -u=root "uptime"
# Run against a named host
sshx -h=prod-web "systemctl is-active nginx"
# Preview what would happen before connecting
sshx -h=prod-web --dry-run --json "sudo systemctl restart nginx"
# Get machine-readable output for automation
sshx -h=prod-web --json "systemctl is-active nginx"
# Inspect a complete system/network baseline in one call
sshx inspect -h=prod-web system.baseline --json
Safety First
Remote access tools can cause real damage. The safe default path in sshx is strict:
- Host keys are checked through
known_hosts. - Passwords belong in the OS keyring, not in shell history or config files.
- Sudo passwords are sent through stdin, never interpolated into the command string.
- Obvious destructive commands are blocked unless the user explicitly bypasses checks.
- Safety checks are guardrails against mistakes; they are not a sandbox for untrusted commands.
Read Security Guidelines before using sshx in production or agent-driven workflows.
Where To Go Next
- Project Profile and Direction defines the product position, hard non-goals, and acceptance matrix (currently maintained in Chinese).
- Getting Started gets one host working.
- Host Management explains named hosts and key selection.
- Usage Scenarios gives practical examples for daily operations.
- Agent and Script Mode explains JSON output, exit codes, timeouts, and audit logs.
- Inspection Capabilities and Local Plugins explains built-ins,
plugin create, trust, and observations. - SFTP Workflows covers upload, download, list, mkdir, and remove.
- Guarded File Apply replaces one remote file with backup and hash checks.
Getting Started
This guide walks through a first safe setup. The examples use prod-web as the host name; replace it with your own server name or IP address.
Install
If Go is already installed:
go install github.com/talkincode/sshx/cmd/sshx@latest
sshx --version
sshx skill install
sshx skill install writes the canonical skill embedded in the binary to
~/.agents/skills/sshx/SKILL.md. The same command should be run after a
Homebrew install or upgrade; prior sshx-managed versions update automatically.
Use --force only after reviewing a locally modified existing copy.
You can also run a specific version without installing:
go run github.com/talkincode/sshx/cmd/sshx@latest --help
Verify SSH Trust First
sshx checks host keys by default. Add the server to known_hosts before the first connection:
ssh-keyscan -H prod-web >> ~/.ssh/known_hosts
If you deliberately want first-use trust, use:
sshx --accept-unknown-host -h=prod-web "uptime"
Avoid --insecure-hostkey except in short-lived controlled labs. It disables the trust check that protects against man-in-the-middle attacks.
Run The First Command
sshx -h=prod-web -u=deploy "uptime"
Useful variations:
# Non-standard SSH port
sshx -h=prod-web -p=2222 -u=deploy "uptime"
# Specific SSH key
sshx -h=prod-web -u=deploy -i=~/.ssh/prod-web.pem "uptime"
# Bound a slow command
sshx -h=prod-web --timeout=30s "apt-get update"
Add A Named Host
Named hosts keep connection details in one local file:
sshx --host-add --host-name=prod-web -h=192.168.1.100 -u=deploy -i=~/.ssh/prod-web.pem --host-desc="Production web node"
After that:
sshx --host-list
sshx --host-test=prod-web
sshx -h=prod-web "uname -a"
The settings file is ~/.sshx/settings.json and is written with 0600 permissions.
Save A Sudo Password
For commands that start with sudo, sshx can read a password from the OS keyring and feed it to sudo through stdin.
sshx --password-set=prod-web-sudo
sshx -h=prod-web -pk=prod-web-sudo "sudo systemctl status nginx"
Use interactive input. Avoid inline values such as --password-set=key:password; they can leak through shell history or process lists.
Preview Before Running
--dry-run explains how sshx interpreted the command without connecting, executing, reading keyring secrets, changing known_hosts, or writing host config.
sshx -h=prod-web --dry-run --json "sudo systemctl restart nginx"
Dry-run proves the local plan. It does not prove the remote command would succeed.
Host Management
Named hosts turn repetitive SSH details into a short, readable alias. They are useful when you manage several servers, when each server uses a different SSH key, or when an agent needs a stable host inventory.
Add Hosts
Interactive setup:
sshx --host-add
Command-line setup:
sshx --host-add \
--host-name=prod-web \
-h=192.168.1.100 \
-p=22 \
-u=deploy \
-i=~/.ssh/prod-web.pem \
-pk=prod-web-sudo \
--host-desc="Production web node" \
--host-type=linux
Then run commands by alias:
sshx -h=prod-web "hostname && uptime"
Import from ~/.ssh/config
If you already maintain hosts in the OpenSSH client config, import them selectively instead of retyping them. Import is never all-or-nothing by default — you choose what enters ~/.sshx/settings.json.
Interactive selection:
sshx --host-import
sshx lists importable entries (with the resolved user@host:port and key) plus everything it skipped and why, then asks which entries to import (numbers, names, or all).
Non-interactive, by name (script/agent friendly, all-or-nothing):
sshx --host-import=web1,db1
Import from a different config file:
sshx --host-import --ssh-config=~/work/ssh_config
Preview without writing anything:
sshx --host-import=web1 --dry-run --json
What is imported per entry: HostName (or the alias itself when absent), Port, User, and IdentityFile (as the per-host key).
Pollution guards — the importer always skips:
- wildcard or negated patterns (
Host *,web-?,!pattern) — they are rules, not hosts; - aliases that already exist in settings;
- entries whose
host:portalready exists in settings (or duplicates an earlier entry in the same file); - options sshx does not support (
ProxyJump,ForwardAgent, …) — shown asignored:so nothing disappears silently; - options from other blocks:
Host *defaults are never merged into imported entries; IdentityFilevalues containing%tokens (reported in a note).
Match blocks are ignored and Include directives are not followed; import from included files directly via --ssh-config=<path>.
Settings File
Host definitions live in ~/.sshx/settings.json.
{
"key": "/Users/alice/.ssh/id_rsa",
"hosts": [
{
"name": "prod-web",
"description": "Production web node",
"host": "192.168.1.100",
"port": "22",
"user": "deploy",
"key": "/Users/alice/.ssh/prod-web.pem",
"password_key": "prod-web-sudo",
"type": "linux"
}
]
}
The top-level key is the default SSH private key. A per-host key overrides it for that host only.
Daily Host Commands
# List configured hosts
sshx --host-list
# Test one host
sshx --host-test=prod-web
# Test every host with a per-host dial timeout
sshx --host-test-all
# Update a host
sshx --host-update --host-name=prod-web -u=deploy -i=~/.ssh/prod-web-2026.pem
# Remove a host
sshx --host-remove=old-lab
Practical Naming Patterns
Use names that explain both role and environment:
prod-web-1
prod-db-primary
staging-api
lab-router
customer-a-jump
Use password keys that do not expose sensitive topology in public logs. For shared runbooks, prefer placeholders:
sshx -h=prod-web -pk=<sudo-key> "sudo systemctl reload nginx"
Team And Agent Use
For human operators, named hosts reduce typing errors. For automation agents, they create a stable boundary:
- The agent receives
prod-web, not a raw IP and key path. - The operator can review
~/.sshx/settings.json. --dry-run --jsoncan confirm which address, port, user, key, and sudo key would be used.- Audit events can record the resolved host without storing secrets.
SFTP Workflows
sshx supports one-shot SFTP actions for common file tasks. It is not an interactive file manager; each invocation performs one clear upload, download, list, mkdir, or remove operation.
Upload A File
sshx -h=prod-web --upload=./deploy/nginx.conf --to=/tmp/nginx.conf
To overwrite an existing remote file with a backup and hash precondition, use Guarded File Apply instead of assembling upload + install yourself:
sshx apply -h=prod-web --path=/etc/nginx/nginx.conf --from=./deploy/nginx.conf --sudo --json
sshx run --target=prod-web --json -- "sudo nginx -t"
Download A File
sshx -h=prod-web --download=/var/log/nginx/error.log --to=./error.log
Incident collection example:
mkdir -p incident-2026-07-01/prod-web
sshx -h=prod-web --download=/var/log/nginx/error.log --to=incident-2026-07-01/prod-web/error.log
sshx -h=prod-web --download=/etc/os-release --to=incident-2026-07-01/prod-web/os-release
List And Create Directories
sshx -h=prod-web --list=/var/log
sshx -h=prod-web --mkdir=/tmp/sshx-upload
Remove A Remote File
sshx -h=prod-web --rm=/tmp/old-upload.txt
Treat remote deletion as production change. Prefer listing the parent directory first:
sshx -h=prod-web --list=/tmp
sshx -h=prod-web --rm=/tmp/old-upload.txt
Path Boundary
Local paths use your local operating system rules. Remote paths are SFTP paths and should be written as slash-separated remote paths, even when sshx is run from Windows.
# Local Windows path, remote POSIX path
sshx -h=prod-web --upload=C:\Users\alice\release.zip --to=/tmp/release.zip
When To Use Plain SSH Instead
Use an SSH command when the operation needs remote validation or privilege changes:
sshx -h=prod-web "sudo ls -l /etc/nginx"
sshx -h=prod-web "sudo install -m 0644 /tmp/nginx.conf /etc/nginx/nginx.conf"
Use SFTP for direct file movement. Use remote commands for checks, ownership changes, service reloads, and cleanup that requires sudo.
Guarded File Apply
sshx apply replaces one remote regular file. It is the file equivalent of sshx sql: classify the target, check a hash precondition, write a backup, then atomically replace the file. Reload and restart stay outside this command.
sshx apply -h=prod-web --path=/etc/nginx/nginx.conf --from=./nginx.conf \
--expect-sha256=<current> --sudo --json
What Apply Does
- Refuse anything that is not a clean absolute regular-file path.
- Block
/etc/passwd,/etc/shadow, and/etc/sudoersunless--force --bypass-reason=is explicit. - Read the current file (if it exists) and compare
--expect-sha256when provided. - Copy the current file to
~/.sshx/file-backups/unless--no-backup --forceis set. - Write a same-directory temp file, preserve mode and owner, then rename over the target.
- Return
changed,before_sha256,after_sha256,backup.path, andcompletion.
If the remote content already matches the payload, apply succeeds with changed=false and does not write a backup.
Privileged Paths
SFTP runs as the SSH user. Use --sudo when the target is not writable by that user. sshx stages the payload under the remote home directory, then runs a privileged stdin script to install it. The script is never left on the host.
sshx apply --target=prod-web --path=/etc/nginx/nginx.conf \
--from=./nginx.conf --sudo --json
Validation and service reload are separate sshx run invocations:
sshx run --target=prod-web --json -- "sudo nginx -t"
sshx run --target=prod-web --json -- "sudo systemctl reload nginx"
Preview
sshx apply -h=prod-web --path=/etc/nginx/nginx.conf --from=./nginx.conf --dry-run --json
Dry-run hashes the local file and prints the local plan. It does not connect or mutate the remote file.
When To Keep Using SFTP
Use --upload / --download for moving bytes without a backup contract. Use apply when an existing remote file may be overwritten and the caller needs a hash, a backup, and a decidable changed result.
Agent And Script Mode
sshx is designed to be called by scripts and AI agents. The contract is intentionally simple: predictable streams, predictable exit codes, optional JSON, and optional local audit events.
Canonical Run Contract
Prefer sshx run for strict alias selection, complex scripts, and multi-host fan-out:
sshx run --target=prod-web --json -- "systemctl is-active nginx"
sshx run --group=prod-web --tag=env=prod --concurrency=4 --jsonl -- "uptime"
sshx run --target=prod-web --script-file=./check.sh --dry-run --json
cat ./check.sh | sshx run --target=prod-web --script-stdin --json
- Selectors resolve configured hosts only. Use
--address=for one literal address. - Script payloads are streamed on SSH stdin and are not reconstructed through shell joining.
- Dry-run and results expose payload SHA-256 and byte length, not raw script contents.
- Multi-target
--jsonlstreamsrun_started, per-target events, andrun_finished. - Multi-target exit codes:
0all succeeded,1partial/failed/skipped/uncertain,255request-level failure. - High-risk bypasses require explicit flags; command mode and
sshx runrequire--bypass-reason=with--force/--no-safety-check. - Working-directory
.envfiles are not loaded. InheritedSSH_FORCE/SSH_NO_SAFETY_CHECK/ host-key env switches do not authorize trust relaxation.
Default Stream Behavior
By default sshx does not request a PTY. That keeps stdout and stderr separate and avoids terminal control characters in script output.
sshx -h=prod-web "systemctl is-active nginx"
The remote command exit code becomes the sshx process exit code when the remote command runs.
Exit Codes
| Code | Meaning |
|---|---|
0 | Remote command succeeded. |
1..254 | Remote command failed with that exit code. |
255 | sshx failed before or around execution, such as connect, auth, host-key, timeout, blocked command, config, or other local error. |
In JSON mode, sshx-level failures use exit_code: -1 and a non-empty error_kind, so automation can distinguish them from a remote command that exits 255.
JSON Output
sshx -h=prod-web --json "systemctl is-active nginx"
Example shape:
{
"host": "192.168.1.100",
"port": "22",
"user": "deploy",
"command": "systemctl is-active nginx",
"exit_code": 0,
"success": true,
"stdout": "active\n",
"stderr": "",
"duration_ms": 142,
"auth_method": "key"
}
Agent branching example:
result="$(sshx -h=prod-web --json "systemctl is-active nginx")"
if printf '%s' "$result" | jq -e '.success == true' >/dev/null; then
echo "nginx is active"
else
printf '%s\n' "$result" | jq '{exit_code, error_kind, stderr}'
fi
Guarded File Apply
Prefer sshx apply when replacing one remote regular file. Branch on
changed, completion, and error_kind. A precondition failure means the
file was not written.
sshx apply --target=prod-web --path=/etc/nginx/nginx.conf \
--from=./nginx.conf --expect-sha256="$current" --sudo --json
Reload stays a separate sshx run. See Guarded File Apply.
Reusable Host Inspection
Before repeating a chain of discovery commands, list and run a bounded inspection capability:
sshx plugin list --json
sshx inspect -h=prod-web system.baseline --json
Application-specific collectors are sshx runtime assets, not skill assets. An Agent can scaffold one without inventing its file layout:
sshx plugin create docker.environment --template=docker --privilege=optional --json
sshx plugin test docker.environment --fixture=complete --json
sshx plugin trust docker.environment --json
sshx inspect -h=prod-web docker.environment --json
Branch on observation status (complete, partial, unsupported, or
failed) and typed errors. Do not interpret permission-limited partial as
service absence. New or edited plugins must be trusted by digest before sshx
connects to the target.
Remote reuse is explicit:
sshx inspect -h=prod-web docker.environment \
--cache=remote-prefer --max-age=10m --json
The remote cache stores only normalized, redacted observation JSON. It is host-scoped and freshness-bounded, not an authoritative inventory. See Inspection Capabilities And Local Plugins for the full manifest, trust, redaction, and invalidation contract.
Dry-Run For Change Review
Before a script performs a privileged operation, ask for the plan:
sshx -h=prod-web --dry-run --json "sudo systemctl restart nginx"
Use dry-run to verify host resolution, selected sudo key, safety status, and whether the command would mutate state. Do not treat it as proof that the remote service can restart successfully.
Timeouts
Always set timeouts for unattended workflows. sshx run defaults the command
timeout to 60s when --timeout / SSH_TIMEOUT are unset; compatibility
sshx -h=... command mode still has no command timeout unless you set one.
The SSH dial timeout is independent (30s).
sshx -h=prod-web --timeout=30s --json "systemctl is-active nginx"
sshx -h=prod-web --timeout=2m --json "sudo apt-get update"
sshx run --target=prod-web --json -- "uptime" # command timeout defaults to 60s
Audit Events
Non-dry-run invocations write local JSONL audit events by default:
~/.sshx/audit/sshx-YYYY-MM-DD.jsonl
Store audit events next to a project or incident directory:
sshx -h=prod-web --audit-output=./.sshx-audit "systemctl reload nginx"
Audit events are for provenance. They record metadata and outcomes, but they do not record plaintext passwords, private key contents, stdout, or stderr.
PTY Is Explicit
Some commands need terminal behavior:
sshx -h=prod-web --pty "top -b -n1"
Do not combine --pty with --json. A PTY merges stderr into stdout and makes structured automation less reliable.
Inspection Capabilities And Local Plugins
Repeated host discovery is expensive for an Agent: checking whether Docker is
available, locating Compose projects, reading routes and DNS state, and deciding
whether a missing result means “absent” or “permission denied” otherwise takes
many independent commands. sshx inspect turns that work into one versioned,
structured invocation.
Built-in system capabilities
The stable operating-system layer is compiled into sshx:
system.identitysystem.resourcessystem.baselinenetwork.interfacesnetwork.routesnetwork.dnsnetwork.listenersnetwork.firewall
Run the complete baseline:
sshx inspect -h=prod-web system.baseline --json
The result is an sshx.observation/v1 JSON document. status is one of
complete, partial, unsupported, or failed; permission-limited evidence
is never normalized to an absent service.
Plugins belong to the sshx runtime
Application collectors are local sshx assets. They do not belong in an Agent skill and are not installed on the target:
~/.sshx/
├── settings.json
├── audit/
├── plugins/
│ └── <plugin-id>/
│ ├── manifest.json
│ ├── collectors/
│ ├── result.schema.json
│ ├── README.md
│ └── fixtures/
├── plugin-lock.json
└── observations/
Set SSHX_HOME to replace ~/.sshx for an isolated project, Agent, or CI run.
Existing settings, audit, plugin, and lock paths all follow the same runtime
root.
Create a custom plugin
plugin create produces a complete, editable scaffold:
sshx plugin create private.environment \
--runner=sh \
--platform=linux \
--privilege=optional \
--template=generic \
--json
Available templates are generic, docker, and nginx. The Docker template
collects installation/daemon state, versions, Docker root/storage/cgroup data,
containers, images, ports, networks, mounts, and Compose project metadata. It
does not collect container environment values, registry auth, .env contents,
Secret values, or raw Compose files.
Plugin API v1 uses the sh runner on Linux or Darwin targets. The sshx
controller itself remains cross-platform; a future Windows-target runner needs
an explicit execution and test contract rather than silently treating
PowerShell as POSIX shell.
Use --replace only when replacement is intentional. sshx moves the previous
directory under ~/.sshx/plugin-backups/ before installing the new scaffold.
plugin remove is recoverable for the same reason.
Validate, test, and trust
sshx plugin validate private.environment --json
sshx plugin test private.environment --fixture=complete --json
sshx plugin test private.environment --json
sshx plugin trust private.environment --json
sshx plugin show private.environment --json
sshx plugin list --json
validate checks the manifest contract, paths, file types/permissions, entrypoint,
JSON Schema, timeouts, privilege declaration, cache policy, and declared effects.
test validates a fixture or explicitly runs the local collector with bounded
stdout/stderr and a minimal environment.
A new or changed local plugin is untrusted. plugin trust records the digest of
the manifest, entrypoint, and schema in plugin-lock.json. Any later edit changes
the digest, and inspect refuses to open an SSH connection until the new digest
is explicitly trusted. Trust is admission and audit metadata, not a sandbox: a
trusted collector can do anything allowed to the SSH identity, so review it first.
Execute without installing remote code
sshx inspect -h=prod-web private.environment --json
sshx resolves and verifies the plugin locally before connecting. It streams the
collector to a fixed sh -s -- session over SSH stdin, validates exactly one
JSON document against the plugin schema, applies field redaction, wraps the facts
with target/provenance/freshness metadata, and exits. The collector is never
installed persistently on the target and never receives SSH or keyring secrets.
Privilege is declared by the manifest:
never:--sudois rejected.optional: normal user by default; use--sudoexplicitly when necessary.required: sshx resolves the selected sudo key and feeds it to sudo separately from the collector payload.
Preview every boundary without connecting:
sshx inspect -h=prod-web private.environment \
--cache=remote-prefer \
--dry-run \
--json
The plan includes the plugin path, digest, trust state, host resolution, privilege, secret-read decision, execution decision, known-host impact, and observation write.
Freshness-bounded remote observations
Remote caching is opt-in:
sshx inspect -h=prod-web private.environment \
--cache=remote-prefer \
--max-age=10m \
--json
Only the normalized, redacted JSON observation is stored under the remote user’s
~/.sshx/observations/v1/. Plugin code remains local. Files are owner-only and
replaced atomically.
A snapshot is reusable only when its capability ID/version/digest, result schema,
parameters, target host-key fingerprint, platform, authenticated UID, boot ID,
and privilege scope still match. TTL expiry or --refresh runs the collector again. --allow-stale
is an explicit instruction to return a matching expired snapshot; it never makes
the snapshot appear fresh.
Cached files are untrusted input. sshx rejects symlinked path components, unsafe permissions, wrong ownership, oversized files, malformed JSON, mismatched schemas, and identity drift rather than silently treating them as current facts.
This is an observation cache, not a CMDB: it has no fleet search, ownership, desired state, reconciliation, or claim of authoritative inventory.
MCP Server (stdio)
sshx mcp serves the sshx execution contract over the Model Context Protocol
so MCP-capable agents (Claude Desktop, IDE agents, custom clients) can call
sshx as native tools instead of shelling out.
sshx mcp
The server speaks MCP over stdio only. It is spawned and owned by the MCP client, holds no SSH connections, keeps no state, and exits when the client closes the stream. Every tool call re-enters the sshx binary as a one-shot child process — the same process model, safety gates, keyring access, and audit trail as the CLI.
Client Configuration
Claude Desktop / generic MCP client entry:
{
"mcpServers": {
"sshx": {
"command": "sshx",
"args": ["mcp"]
}
}
}
Tools
| Tool | Maps to | Notes |
|---|---|---|
sshx_run | sshx run --json | Selectors, command or byte-preserving script, bounded fan-out, dry-run, force + bypass_reason |
sshx_sql | sshx sql --json | Guarded single-statement SQL via remote psql/sqlite3 |
sshx_apply | sshx apply --json | Guarded single-file replace; accepts from_path or inline content |
sshx_inspect | sshx inspect --json | Built-in capabilities and trusted local plugins |
sshx_sftp | SFTP flags | upload / download / list / mkdir / remove |
sshx_transfer | --transfer | Server-to-server streaming through the local machine |
sshx_host_list | --host-list --json | Read-only sshx.hosts.v1 inventory |
Tool results contain the CLI’s versioned JSON verbatim (for example
sshx.result.v1 from sshx_run), so success, error_kind, completion,
and retry guidance keep exactly the semantics documented for the CLI. A
non-zero child exit marks the MCP result as a tool error while preserving the
structured payload.
Security Model
- Same gates, same evidence. Safety checks, host-key verification, keyring
credential roles, and audit all run in the child process exactly as in
direct CLI use.
force/no_safety_checkrequire an explicitbypass_reasonargument. - Audit attribution. Child invocations carry
entry: "mcp"in their audit events, so MCP-originated executions are distinguishable from interactive CLI use. The marker is metadata only — it never changes trust or safety decisions. - No secret surface. Password management (
--password-setand friends) is deliberately not exposed as a tool. Configure credentials with the CLI first; MCP tools only ever reference keyring keys. - No trust relaxations by omission. Accepting unknown host keys is not a
tool parameter. Trust hosts explicitly beforehand (for example with
sshx --host-testor one supervised CLI run). - stdio only. There is no HTTP/SSE transport, no listening socket, and no resident service; this boundary is documented in AGENT.md §3.
Typical Flow
- Configure and trust hosts with the CLI (
--host-add,--host-import,--host-test). - Store credentials in the OS keyring (
--password-set=...). - Point the MCP client at
sshx mcp. - The agent discovers inventory (
sshx_host_list), previews withdry_run: true, executes, and branches on the structured result.
Usage Scenarios
This page is intentionally example-heavy. Treat the host names as placeholders and adapt the commands to your own runbooks.
Scenario 1: First Health Check
You just received access to a server and want a low-risk check.
ssh-keyscan -H prod-web >> ~/.ssh/known_hosts
sshx -h=prod-web -u=deploy "hostname && uptime && whoami"
Why this is useful: it verifies host trust, authentication, the remote user, and basic reachability without changing the server.
Scenario 2: Add A Production Host Once
sshx --host-add \
--host-name=prod-web \
-h=192.168.1.100 \
-u=deploy \
-i=~/.ssh/prod-web.pem \
-pk=prod-web-sudo \
--host-desc="Production web node"
sshx --host-test=prod-web
sshx -h=prod-web "hostname"
Why this is useful: future commands no longer repeat IP, user, key path, and sudo key.
Scenario 3: Check A Service Without Changing It
sshx -h=prod-web "systemctl is-active nginx"
sshx -h=prod-web "systemctl status nginx --no-pager"
For automation:
sshx -h=prod-web --json "systemctl is-active nginx"
Scenario 4: Restart A Service With Review
sshx -h=prod-web --dry-run --json "sudo systemctl restart nginx"
sshx -h=prod-web -pk=prod-web-sudo "sudo systemctl restart nginx"
sshx -h=prod-web "systemctl is-active nginx"
Why this is useful: the dry-run confirms local interpretation before a privileged change.
Scenario 5: Check Disk Pressure On Several Servers
for host in prod-web prod-api prod-db; do
echo "== $host =="
sshx -h="$host" --timeout=15s "df -h / /var /data"
done
Agent-friendly version:
for host in prod-web prod-api prod-db; do
sshx -h="$host" --timeout=15s --json "df -h / /var /data"
done
Scenario 6: Collect Logs For An Incident
mkdir -p incident-2026-07-01/prod-web
sshx -h=prod-web --download=/var/log/nginx/error.log --to=incident-2026-07-01/prod-web/error.log
sshx -h=prod-web --download=/var/log/nginx/access.log --to=incident-2026-07-01/prod-web/access.log
sshx -h=prod-web --audit-output=incident-2026-07-01/audit "journalctl -u nginx --since '30 min ago' --no-pager"
Why this is useful: downloaded evidence and local audit metadata stay next to the incident folder.
Scenario 7: Upload A Config Safely
sshx -h=prod-web --upload=./nginx.conf --to=/tmp/nginx.conf
sshx -h=prod-web "sudo nginx -t -c /tmp/nginx.conf"
sshx -h=prod-web "sudo install -m 0644 /tmp/nginx.conf /etc/nginx/nginx.conf"
sshx -h=prod-web "sudo nginx -t"
sshx -h=prod-web "sudo systemctl reload nginx"
Why this is useful: the file is staged and validated before replacing the production config.
Scenario 8: Use Different Sudo Keys Per Host
sshx --password-set=prod-web-sudo
sshx --password-set=prod-db-sudo
sshx -h=prod-web -pk=prod-web-sudo "sudo systemctl reload nginx"
sshx -h=prod-db -pk=prod-db-sudo "sudo systemctl status postgresql"
Why this is useful: one operator can manage several servers without reusing one global sudo key.
Scenario 9: Validate Every Configured Host
sshx --host-test-all
Run this after rotating keys, changing VPN access, or importing a new settings.json.
Scenario 10: Script A Safe Status Report
for host in prod-web prod-api prod-db; do
sshx -h="$host" --timeout=20s --json "hostname && uptime" \
| jq --arg host "$host" '{alias: $host, success, exit_code, error_kind, stdout}'
done
Why this is useful: scripts read JSON fields instead of scraping terminal prose.
Scenario 11: Bound A Risky Long-Running Command
sshx -h=prod-web --timeout=2m "sudo apt-get update"
Why this is useful: unattended commands should not hang forever.
Scenario 12: Diagnose A Host-Key Failure
If a host key changed, do not bypass it first. Check why it changed.
ssh-keygen -F prod-web
ssh-keyscan -H prod-web
Only update known_hosts after confirming the machine was rebuilt, reinstalled, or intentionally rotated.
Scenario 13: Avoid Shell-Pipe Installers
This command pattern is intentionally high risk:
sshx -h=prod-web "curl -fsSL https://example.invalid/install.sh | sh"
Safer pattern:
sshx -h=prod-web "curl -fsSL https://example.invalid/install.sh -o /tmp/install.sh"
sshx -h=prod-web "less /tmp/install.sh"
sshx -h=prod-web "sha256sum /tmp/install.sh"
sshx -h=prod-web "sh /tmp/install.sh"
Scenario 14: Use PTY Only When Needed
sshx -h=prod-web --pty "sudo visudo -c"
Prefer non-PTY for scripts because it preserves stdout and stderr separation.
Scenario 15: Disable Audit For A Single Sensitive Run
If command text itself would reveal sensitive context, disable audit for that invocation and record the reason in your own runbook.
SSHX_NO_AUDIT=true sshx -h=prod-web "echo redacted"
Do not use this as a default. Audit events are useful for explaining what happened.
Scenario 16: Check Docker Without Opening A Shell
sshx -h=prod-web --json "docker ps --format '{{json .}}' | head -20"
sshx -h=prod-web "docker inspect nginx --format '{{.State.Status}} {{.RestartCount}}'"
Why this is useful: operators can collect container state without starting an interactive SSH session or copying broad logs.
Scenario 17: Verify A Deployment Artifact Before Releasing
sshx -h=prod-web --upload=./dist/app.tar.gz --to=/tmp/app.tar.gz
sshx -h=prod-web "sha256sum /tmp/app.tar.gz"
sshx -h=prod-web "tar -tzf /tmp/app.tar.gz | head"
Only install the artifact after the checksum and archive contents match the release note.
Scenario 18: Rotate A Service Config With Rollback
sshx -h=prod-web --upload=./service.env --to=/tmp/service.env.new
sshx -h=prod-web "sudo cp /etc/myapp/service.env /etc/myapp/service.env.bak.\$(date +%Y%m%d%H%M%S)"
sshx -h=prod-web "sudo install -m 0600 /tmp/service.env.new /etc/myapp/service.env"
sshx -h=prod-web "sudo systemctl restart myapp"
sshx -h=prod-web --json "systemctl is-active myapp"
Why this is useful: the backup, install mode, restart, and health check are separate visible steps.
Scenario 19: Collect A Minimal Support Bundle
mkdir -p support/prod-web
sshx -h=prod-web --download=/etc/os-release --to=support/prod-web/os-release
sshx -h=prod-web --audit-output=support/audit "uname -a"
sshx -h=prod-web --audit-output=support/audit "df -h"
sshx -h=prod-web --audit-output=support/audit "free -m"
Do not download private application data unless the support case explicitly needs it.
Scenario 20: Use -- When Remote Flags Look Like Local Flags
sshx -h=prod-web -- docker run --rm alpine:3.20 sh -c 'echo hello'
sshx -h=prod-web -- echo --force belongs-to-the-remote-command
Why this is useful: -- makes the boundary between local sshx flags and remote command arguments obvious.
Scenario 21: Test A New Host Entry Before Sharing It
sshx --host-add --host-name=staging-api -h=10.0.8.21 -u=deploy -i=~/.ssh/staging.pem -pk=staging-api-sudo
sshx --host-test=staging-api
sshx -h=staging-api --dry-run --json "sudo systemctl reload api"
Only commit or share a runbook after the named host resolves, authenticates, and selects the expected sudo key.
Scenario 22: Keep A Migration Run Bounded
sshx -h=prod-db --timeout=10s --json "pg_isready"
sshx -h=prod-db --timeout=5m --dry-run --json "sudo systemctl restart postgresql"
sshx -h=prod-db --timeout=5m -pk=prod-db-sudo "sudo systemctl restart postgresql"
sshx -h=prod-db --timeout=30s --json "pg_isready"
Why this is useful: every step has a time budget and a machine-readable result.
Scenario 23: Remove A Temporary File With Evidence
sshx -h=prod-web --list=/tmp
sshx -h=prod-web --rm=/tmp/app.tar.gz
sshx -h=prod-web --list=/tmp
Deletion should be visible before and after. For high-risk paths, prefer a remote mv into a dated quarantine directory before permanent removal.
Scenario 24: Fail Closed In CI
result="$(sshx -h=prod-web --timeout=20s --json "systemctl is-active nginx")"
printf '%s\n' "$result" | jq .
printf '%s\n' "$result" | jq -e '.success == true and .stdout == "active\n"'
Why this is useful: CI fails when the structured result is missing, the command fails, or the service state is not exactly what the runbook expects.
Security Guidelines
Remote execution is high impact. These rules are strict because a small mistake can change production systems, leak credentials, or hide the real cause of an incident.
Non-Negotiable Rules
- Keep host-key verification strict.
- Store passwords in the OS keyring, not in files, shell history, tickets, or chat.
- Send sudo passwords through stdin only; never place them in command strings.
- Treat
--force,--no-safety-check, and--insecure-hostkeyas exceptional break-glass choices. - Use
--dry-runbefore privileged or destructive operations. - Use
--jsonand explicit exit-code checks for automation. - Remember that command safety checks are not a sandbox.
Production Policy
For production, shared runbooks, CI jobs, and agent-driven operations, treat these as policy instead of suggestions:
- Use named hosts so reviewers can see the intended target.
- Set
--timeouton every unattended command. - Use
--audit-outputfor project, migration, release, and incident work. - Require
--dry-run --jsonbefore a privileged mutation. - Keep
--forceand--no-safety-checkout of reusable scripts. - Keep
--insecure-hostkeyout of reusable scripts and CI. - Do not run commands copied from chat, tickets, or web pages until they are reviewed against the target host and rollback plan.
- Prefer staged file writes: upload to
/tmp, validate, then install with explicit mode and ownership. - Prefer one visible step per irreversible action; avoid chaining many privileged changes with
&&. - Record the maintenance window, operator, command, result, and rollback decision in your own runbook when the action affects production.
Never make a weak security flag global through shell profiles, CI variables, or shared .env files. A break-glass override must be local to one command and easy to remove.
Host-Key Trust
Default behavior protects against unknown or changed host keys. Use one of these safe paths:
# Recommended: explicitly add the host key after reviewing the target
ssh-keyscan -H prod-web >> ~/.ssh/known_hosts
# Accept first use for a known controlled host
sshx --accept-unknown-host -h=prod-web "uptime"
Avoid:
sshx --insecure-hostkey -h=prod-web "uptime"
Use insecure host-key mode only in short-lived controlled labs where the risk is understood and recorded. Never make it the default in scripts or shared runbooks.
Secret Handling
Use interactive keyring storage:
sshx --password-set=prod-web-sudo
Avoid inline secrets:
sshx --password-set=prod-web-sudo:plain-text-password
Inline values can leak through shell history, terminal scrollback, process listings, logs, or copied commands.
Keyring password keys are for sudo auto-fill. SSH_PASSWORD is an SSH login password and should be treated as a high-risk fallback, not a normal operating mode.
Sudo Rules
sshx auto-fills sudo only when the remote command starts with sudo:
sshx -h=prod-web -pk=prod-web-sudo "sudo systemctl reload nginx"
These commands do not trigger sudo auto-fill:
sshx -h=prod-web "sh -c 'sudo whoami'"
sshx -h=prod-web "echo sudo"
This boundary keeps password lookup, stdin injection, and audit metadata aligned to one clear rule.
Safety Checks Are Guardrails
sshx blocks common destructive patterns such as root deletion, disk formatting, shutdown or reboot commands, critical system file edits, fork bombs, and curl | sh style pipelines.
That does not make untrusted commands safe. A command validator cannot understand every script, shell expansion, application-specific migration, or data-destruction path.
Before bypassing checks:
sshx -h=prod-web --dry-run --json "sudo systemctl reboot"
sshx -h=prod-web --force "sudo systemctl reboot"
Ask:
- Is the target host correct?
- Is the command reviewed?
- Is there a maintenance window?
- Is rollback possible?
- Is the bypass reason recorded?
If any answer is “no”, stop and fix the runbook first. --force should mean “I reviewed this exact command for this exact target”, not “make the tool stop complaining”.
Agent And Automation Rules
Automation should be more conservative than a human terminal:
- Always set
--timeout. - Prefer
--json. - Parse
success,exit_code, anderror_kind. - Run
--dry-run --jsonbefore privileged changes. - Do not set
SSH_INSECURE_HOST_KEY=1globally. - Do not pass plaintext passwords through environment variables unless there is no safer path and the lifetime is tightly controlled.
- Store audit events with
--audit-outputwhen a run belongs to a project, migration, or incident.
Audit Trail Boundaries
Audit events are local JSONL records for provenance. They record metadata such as mode, action, host resolution, sudo/keyring decisions, safety status, authentication method, exit code, error kind, and duration.
They intentionally do not record:
- Plaintext passwords.
- Private key contents.
- stdout.
- stderr.
Command text is included for provenance and redacted for common password or token-style arguments, but do not treat redaction as a reason to place secrets in commands.
Inspection Plugin Trust And Cache Safety
Custom inspection plugins are executable code owned by the local sshx runtime,
normally ~/.sshx/plugins/. Agent skills may explain how to use them but must
not embed or maintain their collector scripts.
- New and edited plugins are untrusted. Review them, run
plugin validateandplugin test, then useplugin trustto admit the exact current digest. - Digest trust is not a sandbox. A trusted collector runs with the selected remote user or sudo identity and should be treated like any reviewed script.
- Plugin code and sshx credentials are never persisted on the remote host. sshx streams the collector through the one-shot SSH session.
--cache=remote-preferis opt-in and stores only normalized, redacted JSON under the authenticated user’s~/.sshx/observations/v1/.- Cached observations are untrusted input. sshx rejects malformed, oversized, symlinked, broadly accessible, wrong-owner, identity-mismatched, and stale entries unless stale reuse was explicitly requested within the hard limit.
- Do not place environment dumps, raw Compose/Nginx configuration, registry authentication, cookies, tokens, private keys, or secret values in facts or evidence. Redaction is defense in depth, not permission to collect secrets.
SFTP Safety
For uploads to privileged paths, stage the file first:
sshx -h=prod-web --upload=./service.conf --to=/tmp/service.conf
sshx -h=prod-web "sudo install -m 0644 /tmp/service.conf /etc/service/service.conf"
For removals, list before deleting:
sshx -h=prod-web --list=/tmp
sshx -h=prod-web --rm=/tmp/old-file
Remote SFTP paths are remote paths. Do not rely on local OS path rules for remote targets.
Incident Response Checklist
When something looks wrong:
- Stop retrying with weaker security flags.
- Capture the exact command, exit code, and
error_kind. - Check audit events under
~/.sshx/auditor the configured--audit-output. - Verify host-key state with
ssh-keygen -F <host>. - Check whether the failure happened before SSH, during auth, during safety validation, during command execution, or during output collection.
- Rotate exposed credentials if a secret may have entered shell history, CI logs, issue text, or chat.
Good Defaults For Shared Runbooks
sshx -h=<named-host> \
--timeout=30s \
--audit-output=./.sshx-audit \
--dry-run \
--json \
"sudo systemctl reload <service>"
Then run the real command only after the plan is reviewed:
sshx -h=<named-host> \
--timeout=30s \
--audit-output=./.sshx-audit \
-pk=<sudo-key> \
"sudo systemctl reload <service>"
Troubleshooting
Use the failure boundary first: did sshx fail before the remote command ran, or did the remote command run and exit non-zero?
Get Structured Error Details
sshx -h=prod-web --json "systemctl is-active nginx"
Look at:
successexit_codeerror_kindstderrauth_method
An sshx-level failure in JSON mode has exit_code: -1 and a non-empty error_kind.
Host Key Errors
Symptoms:
- Unknown host key.
- Changed host key.
- Connection aborts before authentication.
Checks:
ssh-keygen -F prod-web
ssh-keyscan -H prod-web
Fix only after confirming the host is expected. Do not jump straight to --insecure-hostkey.
Authentication Errors
Check the resolved host and selected key:
sshx -h=prod-web --dry-run --json "whoami"
Common causes:
- Wrong user in
~/.sshx/settings.json. - Wrong per-host key path.
- Key file has bad permissions.
- Server does not allow the selected authentication method.
- You expected keyring sudo password to act as an SSH login password.
Keyring passwords are for sudo auto-fill. They are not silently used as SSH login passwords.
Sudo Does Not Auto-Fill
sshx only auto-fills sudo when the command starts with sudo.
Works:
sshx -h=prod-web -pk=prod-web-sudo "sudo whoami"
Does not trigger auto-fill:
sshx -h=prod-web "sh -c 'sudo whoami'"
Check that the password key exists:
sshx --password-check=prod-web-sudo
A Command Is Blocked
Blocked commands are usually safety-check failures.
sshx -h=prod-web --dry-run --json "sudo rm -rf /"
If a privileged or destructive command is genuinely intended, review it, record the reason, and use --force only for that invocation.
Script Hangs
Set a timeout:
sshx -h=prod-web --timeout=30s --json "long-running-command"
If the command requires terminal behavior, use --pty, but remember that PTY mode is less suitable for structured automation.
JSON Output Is Not Parseable
In normal JSON mode, stdout should contain one JSON object and diagnostics should stay on stderr. Check for these issues:
- The command was run with
--pty. - A wrapper script printed extra text around the
sshxcall. - The caller mixed stdout and stderr.
SFTP Path Problems
Use local path rules only for local files. Use slash-separated remote paths for remote targets:
sshx -h=prod-web --upload=./file.txt --to=/tmp/file.txt
Audit Events Are Missing
Check whether audit was disabled:
env | grep SSHX_NO_AUDIT
Check the output location:
ls ~/.sshx/audit
If using a project-local location:
sshx -h=prod-web --audit-output=./.sshx-audit "uptime"
ls ./.sshx-audit
Command Not Found
Check installation:
command -v sshx
sshx --version
If installed with Go, confirm ~/go/bin or your GOPATH/bin is in PATH.
macOS Keychain Prompts During Development
Symptoms (contributors building sshx from source on macOS):
- Every rebuilt binary triggers a Keychain authorization dialog when it reads a stored password.
- Real-keyring E2E runs interrupt with GUI prompts.
Cause: Keychain item ACLs are bound to the binary’s code signature. Each rebuild produces a new ad-hoc signature, so previously granted access no longer matches. macOS has no global per-app allowlist; the two supported mechanisms are a stable signing identity or an ephemeral test keychain.
Fix 1 — ephemeral test keychain for E2E runs (recommended for tests):
make test-keychain-macos
This mirrors CI: it creates a throwaway keychain, makes it the user default,
sets the key partition list so command-line tools need no GUI approval, runs
the E2E suite with SSHX_E2E_REAL_KEYRING=1, and always restores your
original keychain configuration afterwards.
Fix 2 — stable self-signed identity for day-to-day manual use:
-
Open Keychain Access → Certificate Assistant → Create a Certificate. Name it
sshx-dev, set Certificate Type toCode Signing. -
Sign every dev build with it:
codesign -f -s sshx-dev ./bin/sshx -
On the next Keychain prompt choose “Always Allow”. Because the signing identity now stays constant across rebuilds, the approval persists.
Note: routine unit tests never touch the real Keychain — the sshx_e2e build
tag swaps in a file-backed isolated keyring, and the E2E harness only uses the
OS keyring when SSHX_E2E_REAL_KEYRING=1 is set.
SSHX 文档
SSH 是通道,X 代表执行。
sshx 是一个面向 Agent 的远程主机执行工具。它通过 SSH/SFTP 连接现有主机,把目标解析、执行预览、安全检查、命令与文件动作、结构化结果和审计留痕收敛到一次 CLI 调用中。
它保持一个简单的模型:一次命令建立一次连接,完成一个明确动作,返回可判断的结果,写入本地审计事件,然后退出;无需在远端安装常驻 Agent,也不引入长期控制面。
文档默认首页是英文。可以使用顶部导航栏里的语言切换入口打开对应中文页面。
SSHX 擅长什么
- 用稳定的 stdout、stderr 和退出码执行远程命令。
- 把 sudo 密码保存到操作系统密钥链,而不是明文文件。
- 用
~/.sshx/settings.json里的主机短名称代替重复输入 IP、端口、用户和 key 路径。 - 不打开交互式 SFTP 客户端,也能完成常见文件上传、下载和目录操作。
- 用哈希前置条件、备份和原子替换安全地改一个远程文件。
- 输出适合脚本和 AI agent 判断分支的 JSON。
- 用
--dry-run在连接、读取 secret、修改known_hosts或写配置前预览本地执行计划。 - 写入本地 JSONL 审计日志,同时不记录明文密码、私钥、stdout 或 stderr。
- 一次调用探测系统/网络状态,并在 sshx 运行目录创建可复用的应用插件。
心智模型
把 sshx 理解成 Agent 工具箱里的远程执行基本件,而不是交互式 shell 的替代品,也不是期望状态或工作流编排平台。
Agent、自动化或人类运维者
|
v
Agent 契约:CLI / JSON / 退出码 / dry-run
|
v
X 执行:目标解析 / 安全检查 / 动作 / 审计
|
v
SSH 通道:认证 / host-key / SSH exec / SFTP
|
v
远程主机
最常用的第一组命令
# 查看参数和示例
sshx --help
# 执行简单命令
sshx -h=192.168.1.100 -u=root "uptime"
# 使用命名主机
sshx -h=prod-web "systemctl is-active nginx"
# 连接前预览执行计划
sshx -h=prod-web --dry-run --json "sudo systemctl restart nginx"
# 给自动化输出机器可读结果
sshx -h=prod-web --json "systemctl is-active nginx"
# 一次采集完整系统/网络基线
sshx inspect -h=prod-web system.baseline --json
安全优先
远程操作工具可能造成真实破坏。sshx 的默认安全路径是严格的:
- 通过
known_hosts校验主机密钥。 - 密码应进入 OS keyring,而不是 shell history 或配置文件。
- sudo 密码通过 stdin 传入,绝不拼进命令字符串。
- 明显危险的破坏性命令默认会被阻止,除非用户显式绕过。
- 安全检查只是防误操作护栏,不是不可信命令的沙箱。
在生产环境或 agent 驱动工作流中使用前,请先阅读安全准则。
下一步
- 项目画像与方向定义产品定位、非目标铁律和验收矩阵。
- 快速开始帮助你让第一台主机跑通。
- 主机管理说明命名主机和密钥选择。
- 使用场景提供大量日常运维例子。
- Agent 与脚本模式说明 JSON、退出码、timeout 和审计日志。
- 主机探测能力与本地插件说明内置能力、
plugin create、信任和观察快照。 - SFTP 工作流覆盖上传、下载、列目录、创建目录和删除。
- 受控文件 Apply用备份和哈希检查替换一个远程文件。
快速开始
这份指南走一遍安全的首次配置。示例里使用 prod-web 作为主机名,请替换成你自己的服务器名称或 IP。
安装
如果已经安装 Go:
go install github.com/talkincode/sshx/cmd/sshx@latest
sshx --version
sshx skill install
sshx skill install 会把二进制内嵌的官方 Skill 写入
~/.agents/skills/sshx/SKILL.md。使用 Homebrew 安装或升级后也运行同一条命令;
由 sshx 管理的旧版本会自动升级,只有在审阅过本地修改副本后,才使用
--force 覆盖。
也可以不安装,直接运行指定版本:
go run github.com/talkincode/sshx/cmd/sshx@latest --help
先确认 SSH 信任
sshx 默认校验 host key。首次连接前建议先把服务器写入 known_hosts:
ssh-keyscan -H prod-web >> ~/.ssh/known_hosts
如果你明确接受首次连接信任,可以使用:
sshx --accept-unknown-host -h=prod-web "uptime"
除短期受控实验环境外,不要使用 --insecure-hostkey。它会关闭防中间人攻击的主机信任校验。
执行第一条命令
sshx -h=prod-web -u=deploy "uptime"
常见变体:
# 非标准 SSH 端口
sshx -h=prod-web -p=2222 -u=deploy "uptime"
# 指定 SSH key
sshx -h=prod-web -u=deploy -i=~/.ssh/prod-web.pem "uptime"
# 给慢命令设置上限
sshx -h=prod-web --timeout=30s "apt-get update"
添加命名主机
命名主机把连接信息集中保存在一个本地文件里:
sshx --host-add --host-name=prod-web -h=192.168.1.100 -u=deploy -i=~/.ssh/prod-web.pem --host-desc="Production web node"
之后可以这样使用:
sshx --host-list
sshx --host-test=prod-web
sshx -h=prod-web "uname -a"
配置文件是 ~/.sshx/settings.json,写入权限为 0600。
保存 sudo 密码
对于以 sudo 开头的命令,sshx 可以从 OS keyring 读取密码,并通过 stdin 传给 sudo。
sshx --password-set=prod-web-sudo
sshx -h=prod-web -pk=prod-web-sudo "sudo systemctl status nginx"
建议使用交互式输入。不要使用 --password-set=key:password 这种内联值,它可能泄露到 shell history 或进程列表。
执行前预览
--dry-run 会说明 sshx 如何解释这条命令,但不会连接、执行、读取 keyring secret、修改 known_hosts 或写主机配置。
sshx -h=prod-web --dry-run --json "sudo systemctl restart nginx"
dry-run 证明本地执行计划,不证明远程命令一定会成功。
主机管理
命名主机把重复的 SSH 信息变成短名称。它适合管理多台服务器、每台服务器使用不同 SSH key、或者 agent 需要稳定主机清单的场景。
添加主机
交互式添加:
sshx --host-add
命令行添加:
sshx --host-add \
--host-name=prod-web \
-h=192.168.1.100 \
-p=22 \
-u=deploy \
-i=~/.ssh/prod-web.pem \
-pk=prod-web-sudo \
--host-desc="Production web node" \
--host-type=linux
之后用别名执行命令:
sshx -h=prod-web "hostname && uptime"
从 ~/.ssh/config 导入
如果你已经在 OpenSSH 客户端配置里维护主机,可以选择性导入,而不是重新输入。导入默认不会“一键全导“——写入 ~/.sshx/settings.json 的内容由你决定。
交互式选择:
sshx --host-import
sshx 会列出可导入条目(含解析后的 user@host:port 与 key)以及所有被跳过的条目和原因,然后由你按编号、名称或 all 选择。
按名称非交互导入(适合脚本 / agent,全部成功或全部失败):
sshx --host-import=web1,db1
从其他配置文件导入:
sshx --host-import --ssh-config=~/work/ssh_config
预览而不写入:
sshx --host-import=web1 --dry-run --json
每个条目导入的字段:HostName(缺省时使用别名本身)、Port、User、IdentityFile(作为该主机的 key)。
防污染规则——导入器始终跳过:
- 通配或否定模式(
Host *、web-?、!pattern)——它们是规则,不是主机; - 与 settings 中已有主机同名的条目;
host:port已存在于 settings(或与同文件中更早条目重复)的条目;- sshx 不支持的选项(
ProxyJump、ForwardAgent等)——以ignored:显示,不会静默丢失; - 其他块的选项:
Host *的默认值绝不会合并进导入条目; - 含
%令牌的IdentityFile(在提示中说明)。
Match 块会被忽略,Include 指令不会被跟随;被包含的文件请用 --ssh-config=<path> 直接导入。
配置文件
主机定义保存在 ~/.sshx/settings.json。
{
"key": "/Users/alice/.ssh/id_rsa",
"hosts": [
{
"name": "prod-web",
"description": "Production web node",
"host": "192.168.1.100",
"port": "22",
"user": "deploy",
"key": "/Users/alice/.ssh/prod-web.pem",
"password_key": "prod-web-sudo",
"type": "linux"
}
]
}
顶层 key 是默认 SSH 私钥。单个 host 的 key 只覆盖这一台主机。
日常主机命令
# 列出已配置主机
sshx --host-list
# 测试单台主机
sshx --host-test=prod-web
# 测试所有主机,每台使用独立拨号超时
sshx --host-test-all
# 更新主机
sshx --host-update --host-name=prod-web -u=deploy -i=~/.ssh/prod-web-2026.pem
# 删除主机
sshx --host-remove=old-lab
实用命名方式
主机名最好同时说明环境和角色:
prod-web-1
prod-db-primary
staging-api
lab-router
customer-a-jump
password key 不要暴露敏感拓扑。共享 runbook 中尽量使用占位符:
sshx -h=prod-web -pk=<sudo-key> "sudo systemctl reload nginx"
团队和 agent 使用
对人类来说,命名主机减少输入错误。对自动化 agent 来说,它提供稳定边界:
- agent 收到的是
prod-web,不是裸 IP 和 key 路径。 - 操作者可以审阅
~/.sshx/settings.json。 --dry-run --json可以确认真实会使用哪个地址、端口、用户、key 和 sudo key。- 审计事件可以记录解析后的主机,但不保存 secret。
SFTP 工作流
sshx 支持常见的一次性 SFTP 操作。它不是交互式文件管理器;每次调用只做一个明确的上传、下载、列目录、创建目录或删除操作。
上传文件
sshx -h=prod-web --upload=./deploy/nginx.conf --to=/tmp/nginx.conf
覆盖已有远程文件并需要备份/哈希前置条件时,用 受控文件 Apply,不要自己拼 upload + install:
sshx apply -h=prod-web --path=/etc/nginx/nginx.conf --from=./deploy/nginx.conf --sudo --json
sshx run --target=prod-web --json -- "sudo nginx -t"
下载文件
sshx -h=prod-web --download=/var/log/nginx/error.log --to=./error.log
事故材料采集示例:
mkdir -p incident-2026-07-01/prod-web
sshx -h=prod-web --download=/var/log/nginx/error.log --to=incident-2026-07-01/prod-web/error.log
sshx -h=prod-web --download=/etc/os-release --to=incident-2026-07-01/prod-web/os-release
列目录与创建目录
sshx -h=prod-web --list=/var/log
sshx -h=prod-web --mkdir=/tmp/sshx-upload
删除远程文件
sshx -h=prod-web --rm=/tmp/old-upload.txt
把远程删除当成生产变更。建议先列出父目录:
sshx -h=prod-web --list=/tmp
sshx -h=prod-web --rm=/tmp/old-upload.txt
路径边界
本地路径遵循本地操作系统规则。远程路径是 SFTP 路径,应使用斜杠分隔;即使 sshx 在 Windows 上运行也一样。
# 本地 Windows 路径,远程 POSIX 路径
sshx -h=prod-web --upload=C:\Users\alice\release.zip --to=/tmp/release.zip
什么时候改用 SSH 命令
当操作需要远程校验或权限变更时,使用 SSH 命令:
sshx -h=prod-web "sudo ls -l /etc/nginx"
sshx -h=prod-web "sudo install -m 0644 /tmp/nginx.conf /etc/nginx/nginx.conf"
SFTP 负责文件移动。远程命令负责检查、改属主、reload 服务和需要 sudo 的清理。
受控文件 Apply
sshx apply 替换一个远程正则文件。它是文件版的 sshx sql:判断目标、检查哈希前置条件、写备份,然后原子替换。服务校验和 reload 不属于这条命令。
sshx apply -h=prod-web --path=/etc/nginx/nginx.conf --from=./nginx.conf \
--expect-sha256=<current> --sudo --json
Apply 做什么
- 拒绝非干净绝对路径、目录、符号链接和设备节点。
- 默认阻断
/etc/passwd、/etc/shadow、/etc/sudoers,除非显式--force --bypass-reason=。 - 读取现有文件,并在提供
--expect-sha256时做前置校验。 - 除非
--no-backup --force,否则把原文复制到~/.sshx/file-backups/。 - 在同目录写临时文件,保留权限和所有者,再 rename 覆盖目标。
- 返回
changed、before_sha256、after_sha256、backup.path和completion。
如果远程内容已经与 payload 一致,apply 以 changed=false 成功,且不写备份。
特权路径
SFTP 以 SSH 用户身份运行。目标对该用户不可写时使用 --sudo。sshx 先把 payload 暂存到远端 home,再通过 stdin 执行特权安装脚本;脚本不会留在主机上。
校验和 reload 用另一次 sshx run:
sshx run --target=prod-web --json -- "sudo nginx -t"
sshx run --target=prod-web --json -- "sudo systemctl reload nginx"
预览
sshx apply -h=prod-web --path=/etc/nginx/nginx.conf --from=./nginx.conf --dry-run --json
dry-run 只哈希本地文件并打印本地计划,不连接、不改远程文件。
什么时候继续用 SFTP
只搬字节、不需要备份合同时用 --upload / --download。会覆盖已有远程文件、需要哈希、备份和可判定的 changed 时用 apply。
Agent 与脚本模式
sshx 设计上可以被脚本和 AI agent 调用。契约很简单:稳定的 stdout/stderr、稳定退出码、可选 JSON、可选本地审计事件。
规范执行契约 sshx run
复杂脚本、严格别名选择和有界多主机执行请优先使用:
sshx run --target=prod-web --json -- "systemctl is-active nginx"
sshx run --group=prod-web --tag=env=prod --concurrency=4 --jsonl -- "uptime"
sshx run --target=prod-web --script-file=./check.sh --dry-run --json
- 选择器只解析已配置主机;字面地址用
--address=,不能进入 group/tag 扩散。 - 脚本经 SSH stdin 原样传输,不经本地
strings.Join拼装。 - dry-run/结果暴露 payload SHA-256 与字节数,默认不回传脚本全文。
- 多主机
--jsonl输出run_started/target_*/run_finished。 - 多主机退出码:
0全成功,1部分失败/跳过/不确定,255请求级失败。 - 高风险绕过需显式 CLI;
sshx run还要求--bypass-reason=。 - 不再隐式加载工作目录
.env;SSH_FORCE等环境变量不能授权信任降级。
默认输出流
默认不请求 PTY,这样 stdout 和 stderr 会保持分离,也不会把终端控制字符混进脚本输出。
sshx -h=prod-web "systemctl is-active nginx"
当远程命令成功运行后,远程退出码会成为 sshx 进程退出码。
退出码
| 退出码 | 含义 |
|---|---|
0 | 远程命令成功。 |
1..254 | 远程命令以该退出码失败。 |
255 | sshx 层面失败,例如连接、认证、host-key、timeout、命令被阻止、配置或其他本地错误。 |
在 JSON 模式下,sshx 层面的失败使用 exit_code: -1 和非空 error_kind,因此自动化可以把它和远程命令退出 255 区分开。
JSON 输出
sshx -h=prod-web --json "systemctl is-active nginx"
示例结构:
{
"host": "192.168.1.100",
"port": "22",
"user": "deploy",
"command": "systemctl is-active nginx",
"exit_code": 0,
"success": true,
"stdout": "active\n",
"stderr": "",
"duration_ms": 142,
"auth_method": "key"
}
agent 分支示例:
result="$(sshx -h=prod-web --json "systemctl is-active nginx")"
if printf '%s' "$result" | jq -e '.success == true' >/dev/null; then
echo "nginx is active"
else
printf '%s\n' "$result" | jq '{exit_code, error_kind, stderr}'
fi
受控文件 Apply
覆盖一个远程正则文件时优先用 sshx apply。根据 changed、completion 和
error_kind 分支。precondition 表示文件没有被写入。
sshx apply --target=prod-web --path=/etc/nginx/nginx.conf \
--from=./nginx.conf --expect-sha256="$current" --sudo --json
reload 仍是另一次 sshx run。详见 受控文件 Apply。
可复用主机探测
在重复执行一串环境发现命令前,先列出并调用有界探测能力:
sshx plugin list --json
sshx inspect -h=prod-web system.baseline --json
应用级采集器属于 sshx 运行资产,不属于 skill。Agent 可以直接生成完整骨架:
sshx plugin create docker.environment --template=docker --privilege=optional --json
sshx plugin test docker.environment --fixture=complete --json
sshx plugin trust docker.environment --json
sshx inspect -h=prod-web docker.environment --json
应根据观察结果的 status(complete、partial、unsupported、failed)
和 typed errors 分支,不要把权限受限的 partial 解释成服务不存在。新建或
修改后的插件必须先按当前摘要显式信任,sshx 才会建立远端连接。
远端复用必须显式开启:
sshx inspect -h=prod-web docker.environment \
--cache=remote-prefer --max-age=10m --json
远端缓存只保存规范化、已脱敏的观察 JSON;它有主机边界和有效期,不是权威 资产库。完整 manifest、信任、脱敏与失效规则见 主机探测能力与本地插件。
用 dry-run 审核变更
在脚本执行特权操作前,先看计划:
sshx -h=prod-web --dry-run --json "sudo systemctl restart nginx"
用 dry-run 核对主机解析、sudo key、安全检查结果,以及真实执行是否会修改状态。不要把 dry-run 当成远程服务一定能重启成功的证明。
超时
无人值守工作流应总是设置 timeout:
sshx -h=prod-web --timeout=30s --json "systemctl is-active nginx"
sshx -h=prod-web --timeout=2m --json "sudo apt-get update"
审计事件
非 dry-run 调用默认写入本地 JSONL 审计事件:
~/.sshx/audit/sshx-YYYY-MM-DD.jsonl
把审计事件保存到项目或事故目录旁边:
sshx -h=prod-web --audit-output=./.sshx-audit "systemctl reload nginx"
审计事件用于溯源。它记录元数据和结果,但不记录明文密码、私钥内容、stdout 或 stderr。
PTY 需要显式启用
某些命令需要终端语义:
sshx -h=prod-web --pty "top -b -n1"
不要把 --pty 和 --json 混用。PTY 会把 stderr 合并进 stdout,让结构化自动化变得不稳定。
主机探测能力与本地插件
Agent 初次接触服务器时,往往需要反复检查 Docker、Compose、路由、DNS、
网卡、防火墙和资源状态,还需要判断“没有结果”究竟是服务不存在,还是权限
不足。sshx inspect 将这些探索收敛为一次有版本、可校验的结构化调用。
内置系统能力
稳定的操作系统能力直接内置在 sshx 中:
system.identitysystem.resourcessystem.baselinenetwork.interfacesnetwork.routesnetwork.dnsnetwork.listenersnetwork.firewall
一次采集完整基线:
sshx inspect -h=prod-web system.baseline --json
结果是 sshx.observation/v1 JSON 文档。status 明确区分 complete、
partial、unsupported 和 failed;权限不足不会被误报成服务不存在。
插件属于 sshx 运行目录
应用级采集器是 sshx 的本地运行资产,不放在 Agent skill 中,也不会安装到 远端服务器:
~/.sshx/
├── settings.json
├── audit/
├── plugins/
│ └── <plugin-id>/
│ ├── manifest.json
│ ├── collectors/
│ ├── result.schema.json
│ ├── README.md
│ └── fixtures/
├── plugin-lock.json
└── observations/
可通过 SSHX_HOME 替换默认的 ~/.sshx,用于项目、Agent 或 CI 隔离;
settings、audit、plugins 和 lock 都跟随同一个运行根目录。
创建自定义插件
plugin create 会生成可直接编辑和验证的完整骨架:
sshx plugin create private.environment \
--runner=sh \
--platform=linux \
--privilege=optional \
--template=generic \
--json
模板包括 generic、docker 和 nginx。Docker 模板采集安装与 daemon
状态、版本、Docker 根目录、存储/cgroup 驱动、容器、镜像、端口、网络、
挂载以及 Compose project/工作目录/配置路径。默认不采集容器环境变量、
registry auth、.env 内容、Secret 值或 Compose 文件正文。
插件 API v1 使用面向 Linux 或 Darwin 目标的 sh runner。sshx 控制端仍保持
跨平台;未来的 Windows 目标 runner 必须先补充明确执行与测试契约,不能把
PowerShell 静默当成 POSIX shell。
--replace 会先把旧插件移动到 ~/.sshx/plugin-backups/;plugin remove
同样采用可恢复移动,而不是直接永久删除。
校验、测试与信任
sshx plugin validate private.environment --json
sshx plugin test private.environment --fixture=complete --json
sshx plugin test private.environment --json
sshx plugin trust private.environment --json
sshx plugin show private.environment --json
sshx plugin list --json
validate 检查 manifest、路径、文件类型与权限、入口、JSON Schema、超时、
权限声明、缓存策略和副作用声明。test 可以校验 fixture,也可以显式在本地
最小环境中执行采集器,并限制 stdout/stderr 大小。
新建或修改后的本地插件默认不可信。plugin trust 将 manifest、入口和 schema
摘要写入 plugin-lock.json;以后任何修改都会改变摘要,inspect 会在建立 SSH
连接前拒绝执行,直到新摘要再次得到显式信任。信任不是沙箱:可信插件仍拥有
SSH 身份允许的权限,因此信任前必须审查内容。
远端临时执行,不安装脚本
sshx inspect -h=prod-web private.environment --json
sshx 在本地解析并校验插件,然后通过固定的 sh -s -- SSH 会话把采集器送入
stdin。stdout 必须是唯一一份符合 schema 的 JSON;sshx 随后脱敏并补充目标、
来源和新鲜度信息。采集器不会持久安装到远端,也不会获得 SSH 或 keyring 密钥。
manifest 的权限策略为:
never:禁止--sudo。optional:默认普通用户,确有需要时显式增加--sudo。required:sshx 解析对应 sudo key,并把密码和采集器内容分离送入 stdin。
执行前可以预览完整边界:
sshx inspect -h=prod-web private.environment \
--cache=remote-prefer \
--dry-run \
--json
预览包含插件路径、摘要、信任状态、目标解析、权限、是否读取 secret、是否执行、 known_hosts 影响以及是否写观察快照,全程不连接远端。
有有效期的远端观察快照
缓存必须显式启用:
sshx inspect -h=prod-web private.environment \
--cache=remote-prefer \
--max-age=10m \
--json
远端只在当前用户的 ~/.sshx/observations/v1/ 保存规范化、已脱敏 JSON,
插件代码仍然只在本地。目录和文件仅属主可访问,并使用原子替换。
只有 capability ID/版本/摘要、schema、参数、host-key 指纹、平台、认证 UID、
boot ID 和权限范围全部一致时,快照才可复用。TTL 到期或 --refresh 会重新采集;
--allow-stale 只是显式允许返回匹配但过期的快照,不会伪装成新鲜数据。
缓存始终按不可信输入处理:路径中的软链接、宽松权限、属主不符、超大文件、 畸形 JSON、schema 不匹配和身份漂移都会失败关闭,而不是被当成当前事实。
这只是观察缓存,不是 CMDB:不提供跨主机搜索、资产归属、期望状态、持续 收敛,也不声称它是权威资产库。
使用场景
这一页故意放了很多例子。请把主机名当成占位符,并按你的 runbook 调整命令。
场景 1:第一次健康检查
刚拿到服务器访问权限,先做低风险检查:
ssh-keyscan -H prod-web >> ~/.ssh/known_hosts
sshx -h=prod-web -u=deploy "hostname && uptime && whoami"
这能同时验证 host trust、认证、远程用户和基本连通性,而且不会修改服务器。
场景 2:一次性添加生产主机
sshx --host-add \
--host-name=prod-web \
-h=192.168.1.100 \
-u=deploy \
-i=~/.ssh/prod-web.pem \
-pk=prod-web-sudo \
--host-desc="Production web node"
sshx --host-test=prod-web
sshx -h=prod-web "hostname"
这样后续命令不再重复 IP、用户、key 路径和 sudo key。
场景 3:只检查服务,不做变更
sshx -h=prod-web "systemctl is-active nginx"
sshx -h=prod-web "systemctl status nginx --no-pager"
自动化版本:
sshx -h=prod-web --json "systemctl is-active nginx"
场景 4:带审核地重启服务
sshx -h=prod-web --dry-run --json "sudo systemctl restart nginx"
sshx -h=prod-web -pk=prod-web-sudo "sudo systemctl restart nginx"
sshx -h=prod-web "systemctl is-active nginx"
dry-run 可以在特权变更前确认本地解释是否正确。
场景 5:检查多台机器磁盘压力
for host in prod-web prod-api prod-db; do
echo "== $host =="
sshx -h="$host" --timeout=15s "df -h / /var /data"
done
agent 友好版本:
for host in prod-web prod-api prod-db; do
sshx -h="$host" --timeout=15s --json "df -h / /var /data"
done
场景 6:为事故收集日志
mkdir -p incident-2026-07-01/prod-web
sshx -h=prod-web --download=/var/log/nginx/error.log --to=incident-2026-07-01/prod-web/error.log
sshx -h=prod-web --download=/var/log/nginx/access.log --to=incident-2026-07-01/prod-web/access.log
sshx -h=prod-web --audit-output=incident-2026-07-01/audit "journalctl -u nginx --since '30 min ago' --no-pager"
下载的证据和本地审计元数据会放在同一个事故目录附近。
场景 7:安全上传配置
sshx -h=prod-web --upload=./nginx.conf --to=/tmp/nginx.conf
sshx -h=prod-web "sudo nginx -t -c /tmp/nginx.conf"
sshx -h=prod-web "sudo install -m 0644 /tmp/nginx.conf /etc/nginx/nginx.conf"
sshx -h=prod-web "sudo nginx -t"
sshx -h=prod-web "sudo systemctl reload nginx"
先暂存并验证文件,再替换生产配置。
场景 8:不同主机使用不同 sudo key
sshx --password-set=prod-web-sudo
sshx --password-set=prod-db-sudo
sshx -h=prod-web -pk=prod-web-sudo "sudo systemctl reload nginx"
sshx -h=prod-db -pk=prod-db-sudo "sudo systemctl status postgresql"
这样一个操作者可以管理多台服务器,而不需要复用一个全局 sudo key。
场景 9:验证所有已配置主机
sshx --host-test-all
轮换 key、调整 VPN、导入新的 settings.json 后,可以先跑这条命令。
场景 10:生成安全状态报告
for host in prod-web prod-api prod-db; do
sshx -h="$host" --timeout=20s --json "hostname && uptime" \
| jq --arg host "$host" '{alias: $host, success, exit_code, error_kind, stdout}'
done
脚本读取 JSON 字段,而不是解析自然语言终端输出。
场景 11:限制长命令运行时间
sshx -h=prod-web --timeout=2m "sudo apt-get update"
无人值守命令不应该无限挂住。
场景 12:诊断 host-key 失败
如果 host key 发生变化,不要先绕过。先确认为什么变化:
ssh-keygen -F prod-web
ssh-keyscan -H prod-web
只有确认机器被重建、重装或按计划轮换后,才更新 known_hosts。
场景 13:避免管道安装脚本
这种模式风险很高:
sshx -h=prod-web "curl -fsSL https://example.invalid/install.sh | sh"
更安全的模式:
sshx -h=prod-web "curl -fsSL https://example.invalid/install.sh -o /tmp/install.sh"
sshx -h=prod-web "less /tmp/install.sh"
sshx -h=prod-web "sha256sum /tmp/install.sh"
sshx -h=prod-web "sh /tmp/install.sh"
场景 14:只在必要时使用 PTY
sshx -h=prod-web --pty "sudo visudo -c"
脚本中优先使用非 PTY,因为它能保持 stdout 和 stderr 分离。
场景 15:单次敏感运行禁用审计
如果命令文本本身会暴露敏感上下文,可以只对这一次禁用审计,并在自己的 runbook 中记录原因。
SSHX_NO_AUDIT=true sshx -h=prod-web "echo redacted"
不要把它作为默认值。审计事件对事后解释很有用。
场景 16:不打开 Shell 也能检查 Docker
sshx -h=prod-web --json "docker ps --format '{{json .}}' | head -20"
sshx -h=prod-web "docker inspect nginx --format '{{.State.Status}} {{.RestartCount}}'"
这样可以收集容器状态,而不需要进入交互式 SSH,也不需要复制大量日志。
场景 17:发布前校验部署产物
sshx -h=prod-web --upload=./dist/app.tar.gz --to=/tmp/app.tar.gz
sshx -h=prod-web "sha256sum /tmp/app.tar.gz"
sshx -h=prod-web "tar -tzf /tmp/app.tar.gz | head"
只有 checksum 和压缩包内容都符合发布说明后,才继续安装。
场景 18:带回滚点地轮换服务配置
sshx -h=prod-web --upload=./service.env --to=/tmp/service.env.new
sshx -h=prod-web "sudo cp /etc/myapp/service.env /etc/myapp/service.env.bak.\$(date +%Y%m%d%H%M%S)"
sshx -h=prod-web "sudo install -m 0600 /tmp/service.env.new /etc/myapp/service.env"
sshx -h=prod-web "sudo systemctl restart myapp"
sshx -h=prod-web --json "systemctl is-active myapp"
备份、权限安装、重启和健康检查是分开的可见步骤,出错时更容易定位。
场景 19:收集最小支持包
mkdir -p support/prod-web
sshx -h=prod-web --download=/etc/os-release --to=support/prod-web/os-release
sshx -h=prod-web --audit-output=support/audit "uname -a"
sshx -h=prod-web --audit-output=support/audit "df -h"
sshx -h=prod-web --audit-output=support/audit "free -m"
除非支持工单明确需要,不要下载应用私有数据。
场景 20:远程参数像本地参数时使用 --
sshx -h=prod-web -- docker run --rm alpine:3.20 sh -c 'echo hello'
sshx -h=prod-web -- echo --force belongs-to-the-remote-command
-- 可以明确区分本地 sshx 参数和远程命令参数,避免把远程参数误当成本地开关。
场景 21:共享前先测试新的主机条目
sshx --host-add --host-name=staging-api -h=10.0.8.21 -u=deploy -i=~/.ssh/staging.pem -pk=staging-api-sudo
sshx --host-test=staging-api
sshx -h=staging-api --dry-run --json "sudo systemctl reload api"
只有命名主机能解析、能认证,并且选择了预期 sudo key 后,才把 runbook 共享出去。
场景 22:给迁移操作设置边界
sshx -h=prod-db --timeout=10s --json "pg_isready"
sshx -h=prod-db --timeout=5m --dry-run --json "sudo systemctl restart postgresql"
sshx -h=prod-db --timeout=5m -pk=prod-db-sudo "sudo systemctl restart postgresql"
sshx -h=prod-db --timeout=30s --json "pg_isready"
每一步都有时间上限,也都有机器可读结果。
场景 23:带证据地删除临时文件
sshx -h=prod-web --list=/tmp
sshx -h=prod-web --rm=/tmp/app.tar.gz
sshx -h=prod-web --list=/tmp
删除前后都应该可见。高风险路径优先把文件移动到带日期的隔离目录,而不是直接永久删除。
场景 24:让 CI 失败时默认关闭
result="$(sshx -h=prod-web --timeout=20s --json "systemctl is-active nginx")"
printf '%s\n' "$result" | jq .
printf '%s\n' "$result" | jq -e '.success == true and .stdout == "active\n"'
当结构化结果缺失、命令失败或服务状态不符合 runbook 预期时,CI 会直接失败。
安全准则
远程执行影响很大。这些规则必须严格执行,因为一个小错误就可能修改生产系统、泄露凭据,或掩盖事故的真实原因。
不可妥协的规则
- 保持严格 host-key 校验。
- 密码保存到 OS keyring,不放进文件、shell history、工单或聊天记录。
- sudo 密码只通过 stdin 传入,绝不拼进命令字符串。
--force、--no-safety-check、--insecure-hostkey都是例外的 break-glass 选择。- 对特权或破坏性操作先跑
--dry-run。 - 自动化使用
--json和明确的退出码判断。 - 记住命令安全检查不是沙箱。
生产环境策略
对生产环境、共享 runbook、CI 作业和 agent 驱动操作,把下面这些当成策略,而不是建议:
- 使用命名主机,让审阅者能看清目标。
- 每个无人值守命令都设置
--timeout。 - 项目、迁移、发布和事故操作使用
--audit-output。 - 特权变更前必须先跑
--dry-run --json。 - 不要把
--force和--no-safety-check写进可复用脚本。 - 不要把
--insecure-hostkey写进可复用脚本或 CI。 - 从聊天、工单或网页复制来的命令,必须先结合目标主机和回滚方案审阅。
- 优先使用分阶段写入:上传到
/tmp,验证后再用明确权限和属主安装。 - 不可逆动作尽量一条命令一个可见步骤,避免用
&&串起大量特权变更。 - 影响生产时,在自己的 runbook 中记录维护窗口、操作者、命令、结果和回滚判断。
不要通过 shell profile、CI 变量或共享 .env 把弱安全参数变成全局默认值。break-glass 绕过必须只作用于单次命令,并且容易移除。
Host-Key 信任
默认行为会防止未知或变更的 host key。使用下面的安全路径:
# 推荐:审阅目标后显式加入 host key
ssh-keyscan -H prod-web >> ~/.ssh/known_hosts
# 对受控主机接受首次信任
sshx --accept-unknown-host -h=prod-web "uptime"
避免:
sshx --insecure-hostkey -h=prod-web "uptime"
不安全 host-key 模式只适合短期受控实验环境,并且要明确记录风险。不要把它写进默认脚本或共享 runbook。
Secret 处理
使用交互式 keyring 存储:
sshx --password-set=prod-web-sudo
避免内联 secret:
sshx --password-set=prod-web-sudo:plain-text-password
内联值可能泄露到 shell history、终端滚屏、进程列表、日志或复制出去的命令里。
Keyring password key 用于 sudo 自动填充。SSH_PASSWORD 是 SSH 登录密码,应视为高风险 fallback,而不是正常操作模式。
Sudo 规则
只有远程命令以 sudo 开头时,sshx 才会自动填充 sudo:
sshx -h=prod-web -pk=prod-web-sudo "sudo systemctl reload nginx"
下面这些不会触发自动填充:
sshx -h=prod-web "sh -c 'sudo whoami'"
sshx -h=prod-web "echo sudo"
这个边界让密码查询、stdin 注入和审计字段都遵循同一条清晰规则。
安全检查只是护栏
sshx 会拦截常见破坏性模式,例如删除根目录、格式化磁盘、关机重启、修改关键系统文件、fork bomb 和 curl | sh 这类管道。
这并不代表不可信命令就安全了。命令校验器不可能理解所有脚本、shell 展开、应用迁移和业务数据删除路径。
绕过检查前:
sshx -h=prod-web --dry-run --json "sudo systemctl reboot"
sshx -h=prod-web --force "sudo systemctl reboot"
先确认:
- 目标主机是否正确?
- 命令是否被审阅?
- 是否有维护窗口?
- 是否有回滚方案?
- 绕过原因是否被记录?
只要有一个答案是“否”,就先停下来修 runbook。--force 的含义应该是“我已经为这个目标审阅过这条命令”,而不是“让工具别再提醒我”。
Agent 和自动化规则
自动化应该比人类终端更保守:
- 总是设置
--timeout。 - 优先使用
--json。 - 解析
success、exit_code和error_kind。 - 特权变更前先跑
--dry-run --json。 - 不要全局设置
SSH_INSECURE_HOST_KEY=1。 - 除非没有更安全路径且生命周期严格受控,否则不要通过环境变量传明文密码。
- 对项目、迁移或事故运行,使用
--audit-output保存审计事件。
审计边界
审计事件是本地 JSONL 溯源记录。它记录模式、动作、主机解析、sudo/keyring 决策、安全状态、认证方式、退出码、错误类型和耗时等元数据。
它刻意不记录:
- 明文密码。
- 私钥内容。
- stdout。
- stderr。
命令文本会作为溯源材料写入,并对常见 password/token 类参数做脱敏,但不要因此把 secret 放进命令。
探测插件信任与缓存安全
自定义探测插件是 sshx 本地运行目录(通常为 ~/.sshx/plugins/)拥有的可执行
代码。Agent skill 可以说明如何调用,但不能嵌入或维护采集脚本。
- 新建或修改后的插件默认不可信。先审阅并执行
plugin validate、plugin test, 再用plugin trust准入当前准确摘要。 - 摘要信任不是沙箱。可信采集器会以所选远端用户或 sudo 身份运行,应按已审阅 脚本对待。
- 插件代码和 sshx 凭据不会持久化到远端;采集器只通过单次 SSH 会话流式执行。
--cache=remote-prefer必须显式开启,远端只在当前认证用户的~/.sshx/observations/v1/保存规范化、已脱敏 JSON。- 缓存属于不可信输入。格式错误、超限、符号链接、权限过宽、属主不符、身份不符 或过期的记录都会被拒绝;只有在硬上限内显式请求时才允许复用陈旧记录。
- 不要把环境变量转储、原始 Compose/Nginx 配置、镜像仓库认证、cookie、token、 私钥或 secret 值放入 facts/evidence。脱敏是纵深防御,不是采集 secret 的许可。
SFTP 安全
上传到特权路径时,先暂存文件:
sshx -h=prod-web --upload=./service.conf --to=/tmp/service.conf
sshx -h=prod-web "sudo install -m 0644 /tmp/service.conf /etc/service/service.conf"
删除前先列目录:
sshx -h=prod-web --list=/tmp
sshx -h=prod-web --rm=/tmp/old-file
远程 SFTP 路径就是远程路径,不要套用本地操作系统路径规则。
事故响应检查表
当情况不对时:
- 停止用更弱的安全参数反复重试。
- 记录准确命令、退出码和
error_kind。 - 检查
~/.sshx/audit或指定--audit-output里的审计事件。 - 用
ssh-keygen -F <host>验证 host-key 状态。 - 判断失败发生在 SSH 前、认证阶段、安全校验阶段、命令执行阶段,还是输出收集阶段。
- 如果 secret 可能进入 shell history、CI 日志、issue 文本或聊天记录,立即轮换相关凭据。
共享 Runbook 的好默认值
sshx -h=<named-host> \
--timeout=30s \
--audit-output=./.sshx-audit \
--dry-run \
--json \
"sudo systemctl reload <service>"
计划审阅后,再执行真实命令:
sshx -h=<named-host> \
--timeout=30s \
--audit-output=./.sshx-audit \
-pk=<sudo-key> \
"sudo systemctl reload <service>"
故障排查
先判断失败边界:是 sshx 在远程命令运行前失败,还是远程命令已经运行但返回非零退出码?
获取结构化错误详情
sshx -h=prod-web --json "systemctl is-active nginx"
重点看:
successexit_codeerror_kindstderrauth_method
JSON 模式下,sshx 层面的失败会有 exit_code: -1 和非空 error_kind。
Host Key 错误
症状:
- 未知 host key。
- host key 发生变化。
- 认证前连接中断。
检查:
ssh-keygen -F prod-web
ssh-keyscan -H prod-web
只有确认主机符合预期后再修复。不要直接跳到 --insecure-hostkey。
认证错误
检查解析后的主机和选择的 key:
sshx -h=prod-web --dry-run --json "whoami"
常见原因:
~/.sshx/settings.json中用户写错。- per-host key 路径错误。
- key 文件权限不正确。
- 服务端不接受选择的认证方式。
- 误以为 keyring 里的 sudo 密码会当作 SSH 登录密码。
Keyring 密码用于 sudo 自动填充,不会被静默用作 SSH 登录密码。
Sudo 没有自动填充
只有命令以 sudo 开头,sshx 才会自动填充。
可以触发:
sshx -h=prod-web -pk=prod-web-sudo "sudo whoami"
不会触发:
sshx -h=prod-web "sh -c 'sudo whoami'"
检查 password key 是否存在:
sshx --password-check=prod-web-sudo
命令被阻止
通常是安全检查失败。
sshx -h=prod-web --dry-run --json "sudo rm -rf /"
如果特权或破坏性命令确实是预期操作,先审阅、记录原因,再只对这一次使用 --force。
脚本卡住
设置 timeout:
sshx -h=prod-web --timeout=30s --json "long-running-command"
如果命令必须要终端语义,可以使用 --pty,但 PTY 模式不适合结构化自动化。
JSON 输出无法解析
普通 JSON 模式下,stdout 应该只包含一个 JSON 对象,诊断信息走 stderr。检查这些问题:
- 是否使用了
--pty。 - 外层脚本是否在
sshx前后打印了额外文本。 - 调用方是否混合了 stdout 和 stderr。
SFTP 路径问题
本地文件使用本地路径规则。远程目标使用斜杠分隔的远程路径:
sshx -h=prod-web --upload=./file.txt --to=/tmp/file.txt
审计事件缺失
检查是否禁用了审计:
env | grep SSHX_NO_AUDIT
检查默认输出位置:
ls ~/.sshx/audit
如果使用项目内目录:
sshx -h=prod-web --audit-output=./.sshx-audit "uptime"
ls ./.sshx-audit
command not found
检查安装:
command -v sshx
sshx --version
如果通过 Go 安装,确认 ~/go/bin 或 GOPATH/bin 已加入 PATH。
sshx 项目画像与方向
SSH is the channel. X is execution.
SSH 是通道,X 代表执行。
项目定位
sshx 是一个面向 Agent 的远程主机执行工具。它以 SSH/SFTP 作为已有、成熟、普遍可达的可信通道,把 Agent 的执行意图转换为一次边界清楚、结果可判断、过程可审计的远程主机操作。
这一定义刻意把产品重心从“SSH 客户端”移到“远程执行”上:
- SSH 是通道:负责连接、认证、加密、主机身份校验和文件传输,但不是 sshx 的全部产品价值。
- X 是执行:负责目标解析、执行预览、安全检查、命令或文件动作、结果结构化、失败分类和审计留痕。
- Agent 是首要调用者:CLI 不是纯人类交互界面,而是一份稳定的进程级工具契约;人类运维者与 Agent 共用同一套目标、安全和审计语义。
一句话定位:
让 Agent 通过 SSH,高效、安全、可审计地在远程主机上完成任务。
英文定位:
Agent-native remote execution over SSH.
项目概述
Agent 操作远程主机时,真正的困难通常不在“能否建立 SSH 连接”,而在于如何稳定地回答以下问题:目标是哪台主机、使用什么身份、将执行什么、是否越过安全边界、执行是否成功、失败发生在哪一层、事后能否解释这次操作。sshx 应把这些重复且高风险的细节收敛为一个短命令和一份稳定结果。
项目保持单二进制、跨平台、无远端驻留组件的形态。每次调用解析目标和约束,建立 SSH/SFTP 连接,执行一个明确动作,返回结果并退出;本地配置、系统 keyring、known_hosts 和审计记录共同构成执行所需的最小信任环境。
Agent / 自动化 / 人类运维者
|
v
Agent 契约层
- CLI / JSON / 退出码 / error_kind
- dry-run / timeout / audit context
|
v
X 执行层
- 目标发现与解析
- 动作分类与安全检查
- 命令、SFTP、主机间传输
- 结果归一化与审计留痕
|
v
SSH 通道层
- 加密连接与认证
- host-key 信任
- SSH exec / SFTP
|
v
远程主机
本地信任边界:settings.json / OS keyring / known_hosts / audit JSONL
项目画像(目标状态)
sshx 做好之后,应成为 Agent 工具箱里的“远程执行基本件”:像调用本地进程一样容易组合,又明确承认远程操作具有凭据、权限、网络和破坏性副作用。Agent 不需要模拟交互式终端,不需要从自然语言日志猜测结果,也不需要在每个任务中重新拼装 SSH 参数、sudo 注入、安全检查和审计逻辑。
效率画像
效率不是单纯缩短 SSH 握手时间,而是减少 Agent 完成一次可靠远程操作所需的决策、调用和返工:
- 意图表达短:命名主机和可复用配置把地址、端口、用户、key 与 sudo key 收敛到稳定目标名。
- 一次调用闭环:发现或解析目标、执行动作、返回结构化结果、形成审计记录,不要求 Agent 维持交互会话。
- 机器判断直接:stdout、stderr、退出码、
success与error_kind各自职责稳定,Agent 不依赖脆弱文本匹配。 - 执行前少返工:dry-run 能在连接、读取 secret 或修改状态前暴露目标解析、sudo、安全绕过和副作用意图。
- 失败后快恢复:错误能区分配置、连接、认证、host-key、超时、命令退出和安全阻断,避免盲目重试。
- 数据移动少落地:远端到远端传输可以流式中转,不要求先写入本地磁盘。
安全画像
安全不是一句“危险命令检测”,而是一组贯穿执行生命周期的边界:
- 凭据边界:secret 默认进入 OS keyring,不进入配置、命令字符串、审计记录或普通终端回显。
- 通道边界:默认严格校验 host key,未知或变更的远端身份不会被静默接受。
- 权限边界:SSH 登录身份与 sudo secret 语义分离;特权执行必须可识别、可预览、可追溯。
- 动作边界:明显破坏性操作默认阻断;绕过必须显式且进入结果与审计上下文。
- 副作用边界:Agent 在执行前能知道动作是否会连接、读取 secret、修改本地状态或修改远端状态。
- 责任边界:记录足够解释“谁通过什么入口,对哪台主机,以何种安全上下文做了什么,结果如何”,同时默认不持久化敏感输出。
安全、正确性与可审计性高于便利和吞吐;在发生冲突时,宁可要求调用者显式表达意图,也不静默降级信任边界。效率优化必须减少无意义摩擦,但不能用模糊目标、隐式凭据选择或不可解释的并发换速度。
产品边界画像
sshx 是执行工具,不是远程主机上的 Agent,也不是持续运行的控制平面。它应在现有 SSH 基础设施上提供一个清晰的 Agent 执行契约,而不是要求每台服务器安装新服务。它可以支持受控的批量执行和更强的执行描述,但不承担期望状态管理、工作流编排、资产治理或组织级审批系统的全部职责。
当前能力清单
-
单次远程命令执行
支持
sshx -h=<host> [options] <command>,默认不启用 PTY,保持 stdout/stderr 分离,并透传远程命令退出码。支持 timeout、显式 PTY 和 sudo stdin 注入。证据:internal/app/app.go、internal/app/config.go、internal/sshclient/client.go、internal/sshclient/runcommand_test.go。 -
Agent 结构化结果契约
--json输出单个 JSON 对象,包含成功状态、退出码、输出、耗时、认证方式和error_kind;sshx 自身失败与远程命令失败可以区分。证据:internal/app/app.go、internal/app/agentmode_test.go、internal/app/usage.go。 -
执行计划预览
--dry-run在建立连接、执行动作、读取 keyring secret、更新known_hosts或写配置前生成本地执行计划;可与--json组合供 Agent 判断。证据:internal/app/dryrun.go、internal/app/agentmode_test.go、internal/app/transfer_test.go。 -
命名主机发现与管理
~/.sshx/settings.json保存命名主机、地址、端口、用户、key 和 password key;支持增删改查、单台或全量连接测试,并可从~/.ssh/config选择性、全有或全无地导入合格主机。配置写入使用私有权限和原子替换。证据:internal/app/settings.go、internal/app/host_manager.go、internal/app/sshconfig.go、internal/app/settings_test.go、internal/app/sshconfig_test.go。 -
SFTP 与主机间文件执行动作
支持上传、下载、列表、建目录、删除,以及两台远端主机之间经本机流式中转的文件或目录传输。证据:
internal/sshclient/client.go、internal/sshclient/transfer.go、internal/app/transfer.go、internal/app/transfer_test.go。 -
凭据与认证边界
密码存放在系统 keyring;默认优先 SSH key,只有显式提供 SSH 登录密码时才回退密码认证;命名主机可独立选择 SSH key 和 sudo password key。证据:
internal/app/password.go、internal/sshclient/client.go、internal/sshclient/client_test.go。 -
通道信任与动作护栏
默认通过
known_hosts严格校验 host key;未知主机接受和不安全校验必须显式开启。明显破坏性命令默认被拦截,--force/--no-safety-check是显式绕过。命令位上的psql/pgcli/sqlite3被导向sshx sql。证据:internal/sshclient/client.go、internal/sshclient/validate.go、internal/sshclient/client_test.go、internal/sshclient/validate_test.go。 -
受控 SQL 执行
sshx sql通过远端已有的psql或sqlite3执行恰好一条语句:本地 fail-closed 分类、策略门闩、变更前备份、结构化 JSON 与审计。PostgreSQL 另有 EXPLAIN 行数估计、表锁事务备份和容器凭据发现;SQLite 以绝对文件路径为身份,只读走file:?mode=ro,变更在BEGIN IMMEDIATE下做表 CSV 或整文件.backup。证据:internal/app/sql.go、internal/sqlsafe/、tests/e2e/sql_sqlite_e2e_test.go。 -
受控文件 Apply
sshx apply替换一个远程正则文件:绝对路径门闩、可选--expect-sha256前置条件、默认 owner-only 备份、同目录临时文件 + rename、保留权限/所有者。--sudo先经 SFTP 暂存再特权安装。不包含 nginx -t 或 reload。证据:internal/app/apply.go、internal/sshclient/apply.go、tests/e2e/apply_e2e_test.go。 -
stdio MCP server
sshx mcp通过 stdio 提供 Model Context Protocol 工具面:sshx_run、sshx_sql、sshx_apply、sshx_inspect、sshx_sftp、sshx_transfer、sshx_host_list与 CLI 契约 1:1 映射,每次 tool call 以一次性子进程重新进入 sshx,结果就是 CLI 的版本化 JSON;force/bypass_reason 必须显式传参,密码管理不暴露,审计事件带entry=mcp标记。证据:internal/app/mcp.go、internal/app/mcp_test.go、tests/e2e/mcp_e2e_test.go。 -
本地结构化审计
非 dry-run 调用默认写入本地 JSONL 审计事件,记录目标、动作、安全上下文、结果和耗时,排除 stdout/stderr,并对命令中的 secret-like 参数做尽力脱敏。证据:
internal/app/audit.go、internal/app/audit_test.go。 -
内置主机环境探测
sshx inspect用一次 SSH 连接返回带来源、权限、目标身份和新鲜度的观察结果;内置系统身份、资源、网卡、路由、DNS、监听端口、防火墙和组合基线能力。证据:internal/app/inspect.go、internal/plugin/builtin.go、tests/e2e/inspect_plugin_e2e_test.go。 -
sshx 本地插件生命周期
Agent 可通过
sshx plugin create在~/.sshx/plugins/(或$SSHX_HOME/plugins/)创建 Docker、Nginx 或自定义应用探测插件,并完成 list/show/validate/test/trust/remove。插件脚本不由 Agent skill 维护;摘要变化会使信任失效。证据:internal/app/plugin.go、internal/plugin/、tests/e2e/inspect_plugin_e2e_test.go。 -
有界远端观察快照
显式启用
--cache=remote-prefer后,仅把规范化、已脱敏 JSON 保存到远端用户~/.sshx/observations/v1/。复用绑定插件版本/摘要、host key、平台、UID、boot ID、权限、参数与 TTL;缓存作为不可信输入校验,使用私有权限和原子替换。证据:internal/app/inspect.go、internal/plugin/observation.go、internal/sshclient/remote_state.go、tests/e2e/inspect_plugin_e2e_test.go。 -
跨平台交付
项目以单二进制形式面向 Linux、macOS 和 Windows,支持 Go 安装、安装脚本、Release 产物和 Homebrew tap。证据:
Makefile、.github/workflows/ci.yml、.github/workflows/release.yml、install.sh、install.ps1。
非目标(铁律)
-
不把 SSH 本身重新实现一遍。 不追求交互式 shell 复用、通用端口转发、SOCKS、X11 或 agent forwarding;SSH 是底层通道,不是功能竞赛对象。
-
不在远端安装驻留 Agent 或插件运行时。 不引入守护进程、后台服务、连接池或常驻控制面;采集器只在单次 SSH 会话中流式执行。远端可显式保存有版本、有时效的被动 JSON 观察结果,但不保存插件代码或可执行运行时。
-
不成为 Ansible、Salt 或工作流引擎。 可以提供有界的多主机执行,但不引入期望状态语言、playbook 生态、调度系统或长期任务编排。
-
不做 HTTP/SSE MCP server、守护进程或常驻协议服务。 stdio MCP server(
sshx mcp)在范围内:它由 MCP 客户端拉起并随会话生灭,每个 tool call 都以一次性子进程重新进入 sshx,复用同一套契约、安全门禁与审计。不得添加 HTTP/SSE 传输、监听端口或任何寿命超过其客户端的服务。 -
不把危险命令防护宣传成沙箱。 sshx 降低误操作和凭据泄露风险,但不承诺安全执行恶意或不可信命令。
-
不成为 CMDB、企业 secret vault 或 SIEM。 sshx 可消费主机配置、使用本地 secret backend、生成审计证据,但不替代组织级资产、密钥和合规平台。
-
不提供明文 secret 存储,也不静默放松 host-key 校验。 便利性不能突破凭据与通道信任边界。
-
不做 GUI/TUI。 核心交互面保持为 flags、stdin、stdout、stderr、退出码和结构化文件;图形化体验属于外部工具。
-
不为了局部平台能力牺牲跨平台一等支持。 Linux、macOS 和 Windows 的核心执行契约必须保持一致。
方向与意图
-
把“执行单元”变成稳定产品契约
每次执行都应能明确表达目标、动作、约束、副作用、安全上下文和结果。无论动作是命令、文件操作还是未来的批量执行,Agent 都能用同一套心智模型预览、执行、判断和审计,而不需要理解内部 SSH 细节。
-
降低复杂命令与脚本的传递损耗
远程命令经本地 shell、参数解析和远端 shell 多层解释时容易发生引用、通配和变量展开损坏。sshx 应让 Agent 能可靠传递复杂执行内容,并保持“实际执行内容”在 dry-run、审计和结果中的语义一致;具体输入形态由实现阶段选择。
-
建立有界的多主机执行能力
Agent 应能对主机集合执行同一检查或动作,并得到逐主机、可聚合、可部分失败的结构化结果。并发必须有界,目标集合必须可预览,失败不能被总成功状态吞掉;该方向服务于执行效率,但不演变成持续编排平台。
-
从命令黑名单走向可解释的执行治理
在现有危险命令防护之外,逐步增强动作分类、只读与变更意图表达、安全绕过原因、调用来源或 run ID 等上下文,使人类审批层或上层 Agent 能基于明确证据决策。治理信息不得伪装成绝对安全保证。
-
强化目标与身份的可发现性
主机导入、列表、分组、标签、连接健康和凭据引用应让 Agent 快速找到正确目标,同时避免把基础设施秘密复制到更多位置。规模化体验仍以简单、可审阅、可迁移的本地配置为底线。
-
把重复探索收敛为可复用探测能力
固定的操作系统与网络事实由二进制内置,Docker、Nginx 和私有应用由 sshx 运行目录中的本地插件表达。每个能力都应有严格 manifest、结果 schema、摘要信任、权限策略、脱敏与有效期,让 Agent 先读可信的新鲜观察,再决定是否重新探测,而不是跨任务重复拼装命令。
-
提升失败恢复效率
连接、认证、host-key、权限、超时、远程退出、部分传输和本地持久化失败应拥有稳定分类与足够上下文。对于会修改状态的动作,结果需要帮助调用者判断“未开始、部分完成、已完成但回执异常”,减少危险重试。
-
扩展文件与受控网络边界执行
继续完善递归文件操作、传输完整性和失败恢复;在需要进入私有网络时,可考虑受控的 jump-host 能力,但不得扩展成通用隧道产品,也不得模糊每一跳的 host-key 与认证决策。
-
保持 secret backend 可演进
默认信任根仍是 OS keyring。未来若接入其他 secret backend,必须保持 secret 不落明文、不进入命令字符串、不静默降级、用途可区分,并让 Agent 只引用凭据而非读取凭据。
完成的样子
sshx 的成功不以支持多少 SSH flag 衡量,而以 Agent 能否用更少步骤完成一次可信远程执行衡量。
- Agent 用一个稳定目标名和一个明确动作即可发起执行,不需要重复处理地址、端口、用户、key、sudo secret 与 host-key 细节。
- dry-run 所展示的目标、动作、安全绕过和副作用,与真实执行及审计记录保持同一语义。
- 人类输出清楚,机器输出稳定;所有一级失败都有可分支的类别,远程命令失败不会与 sshx 自身失败混淆。
- 明显危险动作默认受阻,特权执行与安全绕过显式可见;secret 不出现在普通配置、命令拼接、审计记录或默认终端回显中。
- 多主机执行即使部分失败,也能逐主机说明状态,并避免不受控并发和盲目重试。
- 会修改远端状态的操作能够说明是否执行、是否部分完成以及下一步如何安全判断,而不是只返回一个模糊 EOF 或通用错误。
- 项目继续保持单二进制、无远端驻留组件、无常驻协议服务(stdio MCP 随客户端会话生灭)、无长期控制面的轻量边界。
- Agent 能在 sshx 运行目录快速创建、测试和信任应用探测插件;skill 只维护调用方法,不维护插件脚本。
- 常见系统/网络或应用部署探索可在一次调用中形成可复用观察,且陈旧、身份漂移或不可信缓存不会被静默采用。
- 每项一级能力都有覆盖真实 CLI 与真实 SSH/SFTP 边界的验收证据;安全与状态修改路径同时覆盖失败和恢复语义。
验收矩阵(业务能力覆盖矩阵)
覆盖底线(硬性规定):
- 每个一级功能至少有一条 Happy Path E2E。
- 每个高风险功能至少覆盖一条失败路径。
- 每个涉及权限的功能至少验证两种角色或权限状态。
- 每个会修改系统状态的操作至少验证一次失败后的恢复或回滚。
- 每次新增一级业务功能,必须同步新增对应 E2E 并更新本矩阵。
当前仓库已建立 tests/e2e 编译后二进制验收套件:测试进程通过真实 TCP SSH/SFTP 协议连接隔离服务端,并从进程退出码、stdout/stderr、JSON、远端文件/状态、known_hosts、settings、keyring 和审计 JSONL 观察结果。默认 keyring 场景使用仅在 sshx_e2e 构建标签下启用的隔离后端;macOS CI 还会创建临时系统 Keychain,验证生产二进制跨真实 OS keyring 的完整生命周期。组件测试不计作 CLI E2E,表内证据按实际边界标注。
| 一级功能 | 风险 | 权限 | 修改状态 | Happy Path E2E | 失败路径 | 权限状态覆盖 | 失败恢复/回滚 | 现有证据 |
|---|---|---|---|---|---|---|---|---|
| 单次远程命令执行 | 高 | 是 | 可能 | ✅ | ✅ 远端非零/超时/异常断连 | ✅ operator/reader | ✅ 部分完成后重读状态 | tests/e2e/cli_e2e_test.go |
| Agent JSON / 退出码契约 | 高 | 否 | 否 | ✅ | ✅ stdout/stderr、远端非零与分类失败 | 不适用:只描述结果 | 不适用:不修改状态 | tests/e2e/cli_e2e_test.go |
| dry-run 执行预览 | 中 | 否 | 否 | ✅ | ✅ 组件级无效计划 | 不适用:不获取远端权限 | ✅ 证明零连接、零信任/审计写入 | tests/e2e/cli_e2e_test.go、internal/app/agentmode_test.go |
| 命名主机管理与 SSH config 导入 | 中 | 否 | 是,本地 | ✅ 导入后按别名执行 | ✅ 选择项缺失 | 不适用:单用户本地配置 | ✅ 失败选择不部分写入 | tests/e2e/host_audit_e2e_test.go |
| SFTP 上传/下载/目录操作 | 高 | 是 | 是,远端 | ✅ | ✅ 只读端拒绝写入 | ✅ operator/reader | ✅ 失败上传无目标残留 | tests/e2e/sftp_e2e_test.go |
| 远端到远端传输 | 高 | 是,两端 | 是,两端 | ✅ | ✅ 目标端只读 | ✅ 可写/只读目标 | ✅ 失败无残留,改用可写端重试 | tests/e2e/sftp_e2e_test.go |
| keyring 凭据管理与认证回退 | 高 | 是 | 是,本地 secret | ✅ | ✅ 缺失 secret/公钥被拒 | ✅ key/password-fallback、stored/missing | ✅ 删除后缺失;可重新设置 | tests/e2e/keyring_e2e_test.go、tests/e2e/cli_e2e_test.go |
| host-key 校验 | 高 | 是,信任状态 | 可能修改 known_hosts | ✅ 显式信任后严格复用 | ✅ 未知/变更 key | ✅ strict/accept-unknown | ✅ 首次写入后重新严格连接 | tests/e2e/cli_e2e_test.go |
| 危险动作阻断与显式绕过 | 高 | 是 | 否,仅控制执行准入 | ✅ 显式 --force | ✅ 默认阻断且零连接 | ✅ 默认阻断/显式绕过 | 不适用:策略门本身不修改状态 | tests/e2e/cli_e2e_test.go |
| 本地结构化审计 | 高 | 否 | 是,本地 | ✅ | ✅ 不可写目标可观测 | 不适用:本地调用者同权 | ✅ 修复目标后单事件写入 | tests/e2e/host_audit_e2e_test.go |
| 本地探测插件生命周期 | 高 | 本地调用者权限 | 是,本地 | ✅ create/list/show/validate/test/trust/remove | ✅ 路径逃逸、重复创建、manifest/entrypoint/schema/fixture 分类失败 | ✅ 私有目录/文件权限 | ✅ replace/remove 保留可恢复备份 | tests/e2e/inspect_plugin_e2e_test.go |
| Agent Skill 安装 | 高 | 本地调用者权限 | 是,本地 Agent 信任目录 | ✅ 编译后二进制离线安装/幂等复用 | ✅ 内容冲突与 symlink 目标拒绝 | ✅ 默认目录/显式目录 | ✅ 冲突不覆盖,显式 force 后恢复官方版本 | tests/e2e/skill_e2e_test.go |
| 单主机探测与内置基线 | 高 | 是 | 否,cache off | ✅ 自定义插件与 system.baseline | ✅ 未信任、污染/超限输出、超时、非零退出、不支持平台 | ✅ operator/reader/sudo-required | 不适用:不修改远端状态 | tests/e2e/inspect_plugin_e2e_test.go、tests/e2e/keyring_e2e_test.go |
| 远端观察缓存 | 高 | 是 | 是,远端 JSON | ✅ 冷写入/热复用/并发原子替换 | ✅ TTL/boot ID、格式、大小、属主、权限、symlink、只读端 | ✅ 可写/只读 SFTP | ✅ 失败写入保留原有效快照 | tests/e2e/inspect_plugin_e2e_test.go |
| 有界多主机执行 | 高 | 是 | 可能,多主机 | ✅ sshx run 组/标签选择 + concurrency 1/4/8/32 | ✅ fail_fast、部分失败、零匹配 | ✅ operator 密码角色 | ✅ 每个选中目标都有终态事件 | tests/e2e/run_e2e_test.go、internal/execution/*_test.go |
| 可解释执行治理 | 高 | 是 | 可能 | ✅ run 契约 dry-run/digest/intent/bypass_reason | ✅ blocked、uncertain completion、typed error.kind | ✅ SSH login vs sudo key 分离 | ✅ completion 指导 verify_first/unsafe | tests/e2e/run_e2e_test.go、internal/app/run.go、internal/execution |
| 受控 SQL 执行(PostgreSQL / SQLite) | 高 | 是 | 是,远端库 | ✅ sqlite 只读查询与带备份 UPDATE | ✅ 直连客户端阻断、ATTACH 分类拒绝、缺路径 | ✅ operator 密码角色 | ✅ UPDATE 前 CSV 可还原旧值 | tests/e2e/sql_sqlite_e2e_test.go、internal/sqlsafe/*_test.go、internal/app/sql_test.go |
| 受控文件 Apply | 高 | 是 | 是,远端文件 | ✅ 创建/覆盖/幂等 | ✅ 哈希不匹配、符号链接、只读端 | ✅ operator/reader | ✅ 覆盖前备份可还原旧值 | tests/e2e/apply_e2e_test.go、internal/app/apply_test.go、internal/sshclient/apply_test.go |
| stdio MCP 工具面 | 高 | 是 | 可能,经子进程 | ✅ initialize/tools/list/tools/call 真实执行 | ✅ force 缺 bypass_reason 被拒、非法输入本地拒绝 | ✅ operator 密码角色 | ✅ dry-run 零连接;审计 entry=mcp 可追溯 | tests/e2e/mcp_e2e_test.go、internal/app/mcp_test.go |
当前已达到已实现一级能力的覆盖底线。表中的剩余红项属于尚未实现的方向能力,而不是用组件测试掩盖的既有质量债。未来任何一级能力不得只以参数解析或组件测试作为完成依据;必须沿用编译后二进制边界补充 E2E,并同步更新本矩阵。