---
name: remote-machine-access
description: Connect to a remote/local machine (Windows, WSL, macOS, Linux) from the Hermes server — set up Tailscale, SSH, or other tunnels for file transfer and remote command execution. Doesn't set up new tunnels unless user says so.
version: 1.1.0
author: Hermes Agent
tags: [remote, ssh, tailscale, wsl, vpn, file-transfer, tunnel]
---

# Remote Machine Access

Connect Hermes to a user's remote/local PC (or WSL) so that the agent can execute commands and transfer files to that machine.

## Typical Use Cases

- "下发昨日任务到本地" — deliver scheduled task outputs to user's local PC
- User wants remote command execution on their home/work machine
- File transfer between the Hermes server and the user's PC
- User has Windows + WSL and wants both file transfer + remote command capabilities

## Recommended Approach: Tailscale

Tailscale is the recommended method — free, secure, no public IP needed, works through NAT/firewalls.

### Prerequisites

- Tailscale installed on the Hermes server (check: `which tailscale`)
- User installs Tailscale on their machine

### Setup Steps

1. **Check Tailscale on server:**
   ```bash
   tailscale status          # Show all devices in tailnet
   tailscale ip -4           # Show server's Tailscale IP
   ```

2. **User installs Tailscale on their machine:**
   - **Windows**: https://tailscale.com/download/windows
   - **macOS**: https://tailscale.com/download/macos
   - **Linux**: `curl -fsSL https://tailscale.com/install.sh | sh`

3. **WSL (Windows Subsystem for Linux) — two options:**

   **Option A — Tailscale in WSL (recommended, gets independent IP):**
   ```bash
   curl -fsSL https://tailscale.com/install.sh | sudo bash
   sudo tailscale up
   ```
   Then get IP: `tailscale ip -4`

   **Option B — Use Windows Tailscale to access WSL:**
   - Install Tailscale on Windows
   - Access WSL via Windows hostname/IP + port forwarding

4. **Verify connectivity:**
   ```bash
   tailscale status                    # Check new device appeared
   ping <user-tailscale-ip>            # Basic network test
   ssh ubuntu@<user-tailscale-ip>      # If SSH is set up on WSL
   ```

### SSH Setup Inside WSL

If you need SSH access to WSL:

```bash
# Inside WSL
sudo apt update && sudo apt install -y openssh-server
sudo systemctl enable ssh
sudo systemctl start ssh   # or: sudo service ssh start
```

### SSH Key Exchange (Passwordless Login)

After SSH is installed and running, set up key-based auth:

**1. On the Hermes server**, get your public key:
```bash
cat ~/.ssh/id_rsa.pub   # or ~/.ssh/id_ed25519.pub
# If no keys exist: ssh-keygen -t ed25519 -f ~/.ssh/id_rsa -N "" -q
```

**2. Ask the user to run this in their WSL terminal** (paste the key from step 1):
```bash
mkdir -p ~/.ssh && echo '<your-public-key>' >> ~/.ssh/authorized_keys
chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys
sudo service ssh restart   # or: sudo systemctl restart ssh
```

**3. Test connection from the Hermes server:**
```bash
ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 <username>@<wsl-tailscale-ip> "echo 'Connected'"
```

**⚠️ Pitfall — Permission denied even after adding key:**
- **If connecting to Windows (not WSL):** Windows administrator accounts require a special path — see the **Windows SSH Key Setup (Admin Users)** section below. Using `~/.ssh/authorized_keys` will NOT work for admin users.
- **WSL (or non-Windows):** Check `~/.ssh/authorized_keys` permissions — must be `600` (owner read/write only)
- Check `~/.ssh` directory permissions — must be `700` (owner read/write/execute)
- Always run `chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys` and restart SSH after adding keys
- **Verify the listening port:** Run `sudo service ssh status` and check the listener line — WSL may listen on **port 2222** instead of 22 (e.g. `Server listening on :: port 2222.`). Use `ssh -p 2222 <user>@<ip>` if so.
- Verify the service is running: `sudo service ssh status` — look for `Active: active (running)`

### File Transfer (Deliver Task Outputs)

Once connected via Tailscale, deliver files to the remote machine:

```bash
# SCP a file
scp /path/to/local/file <username>@<remote-ip>:/path/to/destination/

# SCP a directory
scp -r /path/to/local/dir <username>@<remote-ip>:/path/to/destination/

# Rsync (more efficient for repeated transfers)
rsync -avz /path/to/local/dir/ <username>@<remote-ip>:/path/to/destination/
```

### Syncing Files FROM WSL to Linux Server (Pull Mode)

When accessing WSL through Windows SSH (WSL runs inside Windows), standard rsync/SCP paths to `/mnt/c/...` won't work because the SSH endpoint is Windows (not WSL). Use `--rsync-path='wsl.exe rsync'`:

```bash
# Pull files from WSL's Windows filesystem to the Linux server
rsync -avz --rsync-path='wsl.exe rsync' \
  user@windows-tailscale-ip:"/mnt/c/Users/User/Documents/MyFolder/" \
  /local/destination/
```

**Notes:**
- The source path is a Linux-style path (`/mnt/c/...`) as seen from inside WSL
- The SSH target is the machine's IP (Windows with Tailscale), NOT WSL's own IP
- `--rsync-path='wsl.exe rsync'` tells the Windows SSH server to route rsync through WSL
- Add `--delete` for full mirroring (removes files in dest that don't exist in source)
- Works best when both sides have identical rsync versions
- The connection must already have SSH key auth set up (passwordless)

**Example — periodic Obsidian vault sync:**
```bash
rsync -avz --delete --rsync-path='wsl.exe rsync' \
  ookii@100.113.96.40:"/mnt/c/Users/ookii/Documents/ObsidianVault/" \
  /home/server/files/obsidian-vault/
```

**Cron setup for periodic sync:**
```bash
hermes cron create "*/15 * * * *" \
  --name "obsidian-sync" \
  --prompt "Sync Obsidian vault from WSL via rsync..." \
  --deliver origin
```

**⚠️ Pitfall:** SSHing to the Windows IP and trying to `ls /mnt/c/...` directly will fail because the SSH session lands in Windows CMD, not WSL. All path operations must go through `wsl.exe` — either via `--rsync-path` (rsync), or `wsl.exe ls /path` (direct commands).

### Automating with Cron Jobs

After the connection is established, you can set up cron jobs to deliver outputs:

```bash
hermes cron create "daily 9:00" --prompt "Run daily report and save output" --delivery "scp://user@remote-ip:/path/"
```

### Windows SSH Key Setup (Admin Users)

If the Windows user is a member of the Administrators group, OpenSSH for Windows does NOT read `%USERPROFILE%\.ssh\authorized_keys`. Instead it reads:

```
C:\ProgramData\ssh\administrators_authorized_keys
```

**Setup steps (in PowerShell, MUST be run as Administrator):**

```powershell
# Add the public key
$pubkey = '<your-public-key>'
Add-Content "$env:ProgramData\ssh\administrators_authorized_keys" $pubkey

# Set correct permissions (CRITICAL - without this, SSH still denies access)
icacls "$env:ProgramData\ssh\administrators_authorized_keys" /inheritance:r /grant "Administrators:F" /grant "SYSTEM:F"

# Restart SSH service
Restart-Service sshd
```

**Pitfall: Add-Content access denied:** If user gets "Access Denied", they are NOT running PowerShell as Administrator. Ask them to right-click > Run as Administrator.

**Pitfall: sudo not available on Windows by default.** If user is in PowerShell and tries to run `sudo bash`, it will fail with "Sudo is disabled". Commands meant for WSL must be run inside WSL terminal, not PowerShell.

**Pitfall: curl in PowerShell is an alias for Invoke-WebRequest.** Linux-style `curl -fsSL https://... | bash` does NOT work in PowerShell. Direct the user to use a WSL terminal or GUI installer.

### Executing Commands in WSL via Windows SSH

Once connected to Windows via SSH (not WSL directly), use `wsl.exe` to run commands inside WSL:

```bash
# Basic command
ssh user@windows-tailscale-ip "wsl.exe whoami"

# Multi-command
ssh user@windows-tailscale-ip "wsl.exe bash -l -c \"ls -la ~; pwd\""

# Run Hermes in WSL
ssh user@windows-tailscale-ip "wsl.exe /home/user/.local/bin/hermes chat -q 'query' --yolo -Q"
```

**Notes:**
- Use `wsl.exe`, not `wsl` - the `.exe` suffix is required when SSHing from Linux to Windows
- For complex commands with quotes, escape nested double quotes with backslash
- `hermes chat -q` one-shot mode with `--yolo` and `-Q` (quiet mode) works well for remote execution

### Identifying the Correct WSL Tailscale IP

When multiple devices exist in the tailnet, the OS label from `tailscale status` can be misleading:

- A WSL instance may show as "windows" (e.g. hostname `node`, OS label `windows`) — this is common when Tailscale routes through the Windows host
- An old WSL install may still appear as "linux" but be unreachable via SSH (e.g. `localhost-0` with SSH connection refused)
- **Practical debugging approach:** Try SSH on port 22 for ALL online devices. Distinguish devices by the SSH error message:
  - **"Connection refused"** → Linux/WSL with SSH off (or wrong port)
  - **"Permission denied (publickey)"** → SSH server running, key exchange in progress. This is a valid target — just needs key setup.
- When in doubt, ask the user to run `tailscale ip -4` inside their WSL terminal
- **Ping TTL can help identify OS:** TTL=64 typically Linux/WSL, TTL=128 typically Windows, but this is not foolproof when Tailscale wraps traffic

### Chinese/UTF-8 Encoding Issues

When SSHing from a Linux server to Windows, Chinese characters in command output may appear garbled (UTF-8 bytes interpreted as ANSI/GBK). When this happens:

- Test with English output first (`wsl.exe echo hello`) to confirm connection works
- The encoding issue is cosmetic — commands execute correctly despite garbled display
- If you need to read Chinese output, try piping through `iconv` or use `CHCP 65001` on the Windows side
- For WSL commands via Windows SSH, `wsl.exe bash -l -c "your_command"` may produce cleaner output than bare `wsl.exe command`

## Pitfalls

- **New device not appearing?** Ask user to run `tailscale ip -4` on their machine and send you the IP directly - the device might be on a different account/tailnet (user may need to be invited or use the same account).
- **WSL2 network**: WSL2 has a virtualized network adapter; Tailscale installed on Windows alone does NOT automatically give WSL its own Tailscale IP. Install Tailscale inside WSL for direct connectivity.
- **SSH not working on fresh WSL**: WSL doesn't enable SSH by default. Ensure `openssh-server` is installed and `sshd` is running.
- **SSH on non-default port**: WSL SSH may listen on port 2222 instead of 22. Check `sudo service ssh status` output for the listener line. Use `ssh -p 2222` if needed.
- **Tailscale device shows wrong OS label**: A WSL instance may appear as "windows" or with an unexpected hostname in `tailscale status` (e.g. `node` for WSL, or `localhost-0` for an old WSL install). When SSH to the expected linux IP fails, try all online devices.
- **WSL has multiple Tailscale IPs**: If the user has installed Tailscale on both Windows and WSL, they'll see two devices. One may have TTL=64 (Linux/WSL) and another TTL=128 (Windows). Always confirm which IP is actually WSL by testing SSH connectivity.
- **Tailscale IP connects but SSH fails**: If ping works but SSH says "Connection refused", SSH server isn't running in WSL. If ping works and TCP connects but auth fails, it's a key/permission issue.
- **PowerShell curl != Linux curl**: Never send Linux-style `curl -fsSL https://... | bash` commands to a user in PowerShell. Direct them to use WSL terminal or GUI installation.
- **Windows sudo not available**: Don't ask Windows PowerShell users to run `sudo` commands. Use WSL for Linux commands or adjust for PowerShell syntax.
- **Restart SSH after key changes**: Always run `sudo service ssh restart` (WSL) or `Restart-Service sshd` (Windows) after modifying authorized_keys.

## Reference Files

- `references/session-20260515-wecom-小天.md` — Real session log of connecting to a user's Windows+WSL machine via Tailscale (including Windows SSH admin key setup, WSL command execution, and encoding issues).
- `references/tailscale-funnel-html-sharing.md` — How to share generated HTML files publicly via Tailscale Funnel (Python http.server approach, legacy).
- `references/tailscale-funnel-caddy-sharing.md` — **Recommended.** Share files publicly using Caddy + Tailscale Funnel (with directory browsing, path routing, MD file serving, and security notes).

## Verification

```bash
# Check tailnet status
tailscale status

# Test direct connectivity
ping -c 3 <remote-ip>

# Test SSH
ssh -o ConnectTimeout=5 <user>@<remote-ip> "echo 'Connected successfully'"

# Test file transfer
echo "test" | ssh <user>@<remote-ip> "cat > /tmp/hermes-connect-test.txt"
