An agent skill for managing and accessing remote machines over SSH or Tailscale
SaferSkills independently audited remote-machine-access (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Manage remote machines defined in .remote-nodes/<host>.yaml: discover them, connect via SSH or Tailscale, run commands, transfer files, and add new nodes.
only from SSH keys, ssh-agent, ~/.ssh/config, or Tailscale SSH.
ssh -T (no PTY) — otherwise SIGHUP can killbackgrounded processes when ~/.ssh/config sets RequestTTY=yes.
<host>.yaml, matching its host: field.metadata (model lists, Docker images, recipes, GPU specs, project paths) — a parent config merges into every sub-component's context, and such data duplicates what the component owns and goes stale.
Every ssh/scp/rsync/tailscale ssh command uses the same target:
user: is set in the config, use user@hostname.user: is omitted, use bare hostname and let ~/.ssh/config supply the user(verify with ssh -G <host>).
Examples below use a bare host (gpu-box); substitute user@gpu-box when a user is set.
`hostname` resolution order (try until one connects):
gpu-box.local)~/.ssh/config Host aliastailscale_ip fallbacklan_ip fallbackFall back to tailscale ssh when direct ssh fails or the node is only reachable on the tailnet.
A config is a flat YAML file at .remote-nodes/<host>.yaml defining one machine.
| Field | Required | Description |
|---|---|---|
host | Yes | Identifier; must match the filename |
hostname | Yes | Resolvable SSH target — DNS name, IP, or Tailscale MagicDNS |
user | No | SSH username (omit if set in ~/.ssh/config) |
port | No | SSH port (default 22) |
identity_file | No | SSH key path (else ssh-agent / default keys) |
description | No | Human-readable note |
lan_ip | No | LAN IP for direct access / fallback |
tailscale_ip | No | Tailscale IP fallback |
Default to the minimal form; add fields only when ~/.ssh/config doesn't supply them:
# minimal
host: gpu-box
hostname: gpu-box# with explicit fields
host: gpu-box
user: alice
hostname: gpu-box
port: 22
identity_file: ~/.ssh/id_ed25519
description: GPU workstation
lan_ip: 192.168.1.50
tailscale_ip: 100.64.0.1Parser note: keep configs flat (no nested mappings) and put comments on their own lines, not inline after a value — some tooling reads these with a simple line parser, not a full YAML library.
To resolve a host the user names ("run this on gpu-box"):
./.remote-nodes/ first, then the parent dir's .remote-nodes/ (**one levelup only**). Merge both; the closer dir wins for a duplicate host.
<host>.yaml, build the target, try ssh then tailscale ssh.Sharing one config across deeply-nested components: because discovery scans only one level up, a component more than one level below a shared config can't see it. Symlink rather than copy, to keep a single source of truth:
# from the component's .remote-nodes/ dir; adjust ../ depth to reach the shared config
ln -s ../../../.remote-nodes/<host>.yaml <host>.yaml.remote-nodes/;else a named subproject's .remote-nodes/. Create it if missing.
<dir>/.remote-nodes/<host>.yaml (filename = host) using the minimal form.python3 -c "import yaml; c=yaml.safe_load(open('.remote-nodes/gpu-box.yaml')); \
assert c.get('host') and c.get('hostname'), 'missing host/hostname'; print('config OK')"
ssh -T gpu-box "hostname" # or: tailscale ssh gpu-box "hostname"ssh -T gpu-box "nvidia-smi" # non-interactive (always -T)
ssh -T gpu-box -i ~/.ssh/id_ed25519 "docker ps"
ssh gpu-box # interactive session
tailscale ssh gpu-box "uptime" # via Tailscale
# multi-line
ssh -T gpu-box <<'EOF'
cd /path/to/project && git pull
EOFUse -N -f for background tunnels. Always kill temporary tunnels when done.
ssh -L 8000:localhost:8000 gpu-box -N -f # local: localhost:8000 → remote:8000
ssh -R 3000:localhost:3000 gpu-box -N -f # remote: remote:3000 → local:3000
ssh -D 1080 gpu-box -N -f # dynamic SOCKS proxy
pkill -f 'ssh -[LRD]' # kill all tunnels (or kill <pid>)
lsof -i :8000 # verify gonescp ./file gpu-box:/remote/path/ # push (scp gpu-box:/path ./ to pull)
scp -r ./dir gpu-box:/remote/dir/ # recursive
rsync -avz ./dir/ gpu-box:/remote/dir/ # sync (add --dry-run to preview)
rsync -avz --delete ./dir/ gpu-box:/remote/dir/ # mirror (deletes extra dest files)
rsync -avz --exclude='.git' ./ gpu-box:/remote/proj/
rsync -avz gpu-box:/remote/dir/ ./local/ # pull~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.