---
name: windows-to-vps-file-transfer
description: Move files and folders from a Windows laptop to a Linux VPS for processing when the user works in a split-local/VPS architecture (Hermes Desktop on Windows, agent runtime on VPS). Trigger when the user references project artifacts, configs, resumes, scripts, or any files that need to be read or executed on the VPS but were authored or stored on Windows. Use BEFORE assuming a project doesn't exist or before asking the user to paste file contents in chat. Covers three transfer paths (PowerShell scp with password, WinSCP, hPanel file manager + tarball) and the order of preference. Pairs with `verifying-user-claims` — that skill covers the "I don't see this project" question, this one covers the recovery step that follows.
---

# Windows → VPS File Transfer

This user's architecture is **VPS (Hostinger, 2.25.172.164) = single source of truth, Windows laptop "Connie" = pure client running Hermes Desktop**. Project artifacts (resumes, scripts, configs, JDs, drafts) live on the Windows side because that's where the user works. When the agent on the VPS needs to read or run those files, they have to cross the boundary. This skill covers the three ways to do that, in order of preference, plus the gotchas that come up.

## When to use this skill

- The user points you at a path on Windows (`C:\Users\Rob\...`) and asks you to read it
- You discover that prior work lives on the local box and you need it on the VPS to proceed
- The user wants to deploy a script (e.g. the GHL webhook handler) from Windows to the VPS
- Any time a "paste the file in chat" reflex would otherwise fire — files are bigger and more sensitive than chat-friendly text

**The reflex to break:** don't ask the user to paste file contents in chat. (a) the GHL PIT token was burned this way before, (b) large files don't paste, (c) it loses the file's structure. Move the file instead.

## Path 1 — PowerShell `scp` with password (preferred for ad-hoc, no install)

Windows 10/11 ships with OpenSSH. No SmartScreen warning, no extra install, faster than the hPanel browser uploader.

```powershell
# First-time: confirm OpenSSH client is present
Get-Command scp -ErrorAction SilentlyContinue

# Push a folder (trailing slash on the source matters)
scp -r "C:\Users\Rob\Business_Projects\Project_1_Job_Seeker\*" root@2.25.172.164:/root/Business_Projects/Project_1_Job_Seeker/

# Push a single file
scp "C:\Users\Rob\Downloads\ghl-webhook.py" root@2.25.172.164:/root/scripts/
```

What happens on first run:
1. Prompts: *"Are you sure you want to continue connecting (yes/no/[fingerprint])?"* — type `yes` and Enter
2. Prompts: *"root@2.25.172.164's password:"* — type the VPS root password (same one used for the Hostinger web terminal)
3. Transfers, prints progress, returns

**Gotchas:**
- PowerShell parses `<` as a redirection operator — never use angle-bracket placeholders in commands the user runs (per `verifying-user-claims` pitfall). Either substitute real values or use `$env:USER@<hostname>` with an explicit "replace `<hostname>`" callout.
- The trailing `\*` on the source matters: `scp -r "C:\path\folder"` copies the folder itself, `scp -r "C:\path\folder\*"` copies the folder's *contents* into the target. For a project tree, the latter is usually what you want.
- The Hostinger VPS uses a self-signed SSH host key by default. The "Are you sure you want to continue" prompt is expected — type `yes`.
- Passwords typed in a PowerShell prompt are echoed as `*` characters (silent input). That's normal. The user pastes/types the password and hits Enter; nothing to verify visually.

## Path 2 — WinSCP (preferred for large folders, resumable, reusable)

WinSCP is the right tool when:
- The transfer is >50MB and the user is on a slow link
- The user wants to browse both sides during the transfer
- They'll be doing more transfers in the future (GHL webhook deploy, resume pipeline, etc.)

**Download/install:** https://winscp.net — open-source, GPL, signed by the author. On install, SmartScreen will warn "not Microsoft verified" — this is normal (see `verifying-user-claims` pitfall). Click **More info** → **Run anyway**.

**First connection:**
- File protocol: **SCP**
- Host name: `2.25.172.164`
- Port: 22 (default)
- User name: `root`
- Password: VPS root password
- Accept the host key fingerprint prompt on first connect

**Layout:** left pane = Windows, right pane = VPS at `/root/`. Drag to copy. Right-click for sync, queue, etc.

**Why it's worth the install:** the GHL webhook Python file, the resume pipeline files, any future VPS-deployable artifact all benefit from a persistent SCP tool on the laptop. One install, many uses.

## Path 3 — hPanel file manager + tarball (fallback, no SSH at all)

Use when: the user only has Hostinger browser access and is not ready to enable SSH key auth or run PowerShell scp commands. Slower (~5–15 min for 50MB) but works with no setup.

**Step 1 — bundle on Windows (PowerShell):**
```powershell
$src = "C:\Users\Rob\Business_Projects\Project_1_Job_Seeker"
$dst = "C:\Users\Rob\Downloads\Project_1_Job_Seeker.tar.gz"
tar -czf $dst -C (Split-Path $src) (Split-Path $src -Leaf)
Get-Item $dst | Select-Object Name, Length, LastWriteTime
```

`tar` is built into Windows 10/11. `-C` changes to the parent dir, the final arg is the folder name — this avoids the "C:\" absolute path being baked into the archive (which would extract as `./C:/Users/...` on the VPS).

**Step 2 — upload via browser:**
- hPanel → VPS → Files → navigate to `/root/`
- Drag `Project_1_Job_Seeker.tar.gz` from Windows Explorer into the browser
- Wait. For 50MB expect 5–15 min depending on the link to Hostinger's file manager backend.

**Step 3 — extract on the VPS (Hostinger web terminal):**
```bash
mkdir -p /root/Business_Projects
tar -xzf /root/Project_1_Job_Seeker.tar.gz -C /root/Business_Projects/
ls -la /root/Business_Projects/Project_1_Job_Seeker/
```

**Alternative upload clients for Step 2:** WinSCP (if they end up installing it anyway), FileZilla (SFTP, also works), or the `scp` command from Path 1.

## Decision matrix

| Scenario | Path | Why |
|---|---|---|
| Ad-hoc, one file or small folder, user has PowerShell handy | 1 (`scp`) | No install, fast, no SmartScreen |
| Large folder (>50MB), reusable tool wanted, multiple transfers expected | 2 (WinSCP) | Faster than browser upload, persistent tool |
| User has no SSH access at all, or SmartScreen is making them nervous | 3 (hPanel + tarball) | Works with zero setup on Windows side |
| Sensitive content (PII, tokens) | Any path — but **scrub before tar/transfer** | Files leaving the laptop is a bigger blast radius than chat paste; warn the user |
| **Continuous two-way sync of a folder the user edits on multiple devices and the agent reads/writes on the VPS (e.g. the Obsidian vault)** | **4 (Syncthing)** | Keeps VPS = source of truth while Connie/tablet edit locally; agent writes appear in Obsidian with no manual step |

## Path 4 — Syncthing (CONTINUOUS sync — Obsidian vault and shared knowledge folders)

One-shot transfers (Paths 1–3) are wrong for a folder that must stay continuously in sync between Connie and the VPS — above all the Obsidian vault at `/root/.hermes/vault` ↔ `C:\Users\Rob\Documents\Obsidian Vault`. Use Syncthing. It is installed and PAIRED on both ends (2026-07-19); don't re-pair, just use it. Full setup/pairing/verify procedure and pitfalls: `references/syncthing-sync.md`.

Quick facts:
- VPS: `syncthing@root` systemd service (enabled). Config at `/root/.local/state/syncthing/config.xml` (NOT `~/.config`). GUI + REST on `127.0.0.1:8384`; API key = `<apikey>` value in config.xml.
- Folder ID `hermes-vault` → `/root/.hermes/vault`, shared to device `34F6KTU` ("Connie"). Peer-direct over TCP 22000 (verified, not relayed).
- Connie: winget `Syncthing.Syncthing` v2.x, auto-starts via Scheduled Task "Syncthing" (hidden `--no-console --no-browser`, at logon, registered from ADMIN PowerShell). GUI `127.0.0.1:8384`.
- Add a third device (tablet): pair its device ID via REST + share `hermes-vault` to it (reference has the exact curl), approve on the device, point it at the local vault folder.
- Syncthing syncs dotfiles incl. `.obsidian/` workspace config — desired here (keeps settings consistent across devices).
- 404 on `127.0.0.1:8384` from Connie = the syncthing process died (window closed/sleep), NOT a sync break — `Get-Process syncthing`, relaunch, it resumes.
- **Auto-start Scheduled Task must be registered under the user who logs in.** Devices differ in username (Surface = `rkbla`, not `Rob` — never assume `C:\Users\Rob`). Run the block from THAT user's elevated shell so `$env:USERNAME` / `$env:LOCALAPPDATA` resolve to the interactive account, or the task starts Syncthing for the wrong profile (or no one).
- **Use the vault as a COMMAND channel, not just file sync — the email-to-self tax.** When the target device isn't the one running the chat (e.g. Surface while chatting from Connie/VPS TUI), every PowerShell block you emit costs the user an email-to-self → copy → paste round trip. Rob called this out explicitly ("you realize, every code I have to email myself to input it in the tablet"). The fix: write the commands as a `.ps1` into the vault on the VPS (`/root/.hermes/vault/Personal/<task>.ps1`), it syncs to the device in seconds, and the user right-clicks → **Run with PowerShell**. Give the file an action name (`get-desktop-logs.ps1`, not `script.ps1`), `Write-Host` section headers, and a trailing `pause` so the window stays open for reading. Output still has to come back via chat paste (no return channel until that device's Hermes Desktop is repointed at the VPS), but the command side becomes one-click. Rule: **on any device that has the vault synced, never send a copy-paste block longer than ~3 lines — send a synced .ps1 instead.** The same drop-read-delete pattern transports small secrets (root cert, session token) without chat.

## After the transfer — verify before trusting

Per the `verifying-user-claims` verification protocol, confirm the file actually landed and is what you expected:

```bash
# On the VPS, right after transfer
ls -la /root/Business_Projects/Project_1_Job_Seeker/    # top level
find /root/Business_Projects/Project_1_Job_Seeker/ -type f | wc -l   # file count
du -sh /root/Business_Projects/Project_1_Job_Seeker/    # total size
```

If file count or size doesn't match what the user described, the transfer is incomplete or the wrong path was used. Don't read partial files — ask the user to re-run or check the upload.

## Pitfalls

- **"Paste the file in chat" is almost never the right move.** It's slower, it loses structure, and the GHL token precedent shows it's risky. Move the file. If a file is too small to move, it's small enough to inline directly into a tool call from the user's side (read on Windows, paste summary in chat).
- **Don't scp into `/` or other system dirs by accident.** Always specify `/root/<subdir>/`. If the target dir doesn't exist, `scp` will fail with a permission error or create the file at an unintended path.
- **Windows paths in tar archives** — the `-C (Split-Path $src) (Split-Path $src -Leaf)` pattern is intentional. Without it, the archive contains `./C:/Users/Rob/...` and extracts with that prefix on the VPS. The pattern bakes in only the folder name.
- **SSH host key prompts are normal on first connect.** Tell the user to type `yes` and accept the fingerprint. Subsequent connects to the same host skip the prompt.
- **The VPS root password is the same one used for the Hostinger web terminal** — don't make the user look it up twice. State that explicitly when you ask for it.
- **Do not paste SSH passwords in chat.** (a) they're long, (b) the user types them into a PowerShell prompt that doesn't echo them anyway, (c) the same "burned secret" pattern as the GHL token applies.
- **Windows-originated tarballs spew scary-looking errors on `tar -xzf` but the extraction is fine.** When the archive was created on Windows (NTFS) and contains Windows-specific metadata, GNU/BSD tar on Linux will print things like `Cannot open: File exists`, `Cannot utime: Read-only file system`, `Cannot change ownership to uid 0, gid 0: Read-only file system`, `Ignoring unknown extended header keyword 'SCHILY.fflags'`, and may fail to extract the Windows reserved name `nul`. **The actual files extract successfully** — these errors are noise about metadata tar can't restore on this filesystem, not extraction failures. Verify with `ls -la`, `find ... -type f | wc -l`, and `du -sh` on the destination; the file count and size should match what was on Windows. Don't panic, don't re-run, don't tell the user it failed. Filter the stderr to get to the success signals. (Encountered on the 2026-07-14 Job Seeker push: 54MB tarball produced ~280KB of error text but 2,308 files / 68MB extracted correctly.)
- **`/root` is a read-only mount on this Hostinger VPS — extract to `/root/.hermes/` instead.** Verify with `findmnt /root` (should show `ro,...`) and a `touch /root/test_$$.tmp` write probe before extracting. The `/root/.hermes/` directory is mounted read-write as an overlay (that's where the Hermes runtime persists app data) and is the right home for transferred project trees. Symptom of the mistake: tar prints `Cannot mkdir: No such file or directory` for nested paths and exits non-zero. **The error is the OS returning EROFS through tar, not a missing parent dir — it gets misread as a path/tarball problem and leads to 2–3 wasted re-extraction attempts.** Diagnostic protocol: run `findmnt /root && touch /root/probe_$$.tmp 2>&1` first; if the touch returns "Read-only file system", that confirms it. The "No such file or directory" tar prints is misleading — *the parent dir already exists*, the new write inside it is being rejected. If BSD tar keeps misbehaving on extraction, fall back to Python: `python3 -c "import tarfile; tarfile.open('/root/X.tar.gz','r:gz').extractall('/root/.hermes/', filter='data')"`. The earlier `mkdir -p /root/Business_Projects && tar -xzf … -C /root/Business_Projects/` examples in this skill work only because pre-existing files in `/root/Business_Projects/` predate the read-only mount; newly written content will fail.
- **Verify the tarball on Windows BEFORE you scp it.** Windows `tar.exe` is BSD-derived and has known quirks: it can return exit 0 while silently dropping files (paths with special characters, reserved names like `nul`, or when the source contains a stage-dir layout that gets misread). Concrete failure encountered on 2026-07-14: a 1.1MB tarball intended to bundle three projects contained only 8 entries instead of the expected 23, with no error message. The discipline: after `tar -czf`, count the contents before scp — PowerShell: `tar -tzf "$dst" | Where-Object { $_ -notmatch '/$' } | Measure-Object`; bash: `tar -tzf "$dst" | grep -v '/$' | wc -l`. Confirm the count matches what `Get-ChildItem -Recurse -File` reported on the source. Only scp after the tarball contents are confirmed. This is the same "verify by content, not by exit code" principle as the post-transfer check, applied one step earlier — and it saves the user from waiting through a useless upload.

## Related

- `verifying-user-claims` — the "I don't see this project on the VPS, where does it live?" question, plus the verification protocol that confirms the transfer succeeded
- `operating-hermes-gateway` — covers the broader Windows/VPS architecture (single gateway on VPS, Windows as pure client) that makes this transfer pattern necessary
- `references/multi-project-tarball.md` — when you're pushing multiple Windows folders in one upload (multiple `Project_*` siblings, multiple skills, etc.). Two patterns: per-project tarball (most reliable, per-project OK/MISMATCH table) and the staging-dir dance (one upload, requires verification at every step). Plus the Python `tarfile` fallback for out-of-order entries.
- `references/syncthing-sync.md` — Path 4 detail: the VPS↔Connie Syncthing pairing that keeps the Obsidian vault (`/root/.hermes/vault`) continuously synced, plus a SECOND folder `claude-code-sessions` (Connie's `.claude\projects` → `/root/.hermes/mirrors/claude-code-sessions`, VPS receive-only) that mirrors raw Claude Code transcripts for digest scanning. Device IDs (incl. Surface tablet `B2RA3UL`, whose Windows user is `rkbla` not `Rob` — per-device usernames differ, never assume `C:\Users\Rob`), REST pairing curls (POST new folder, PUT to add a device to an EXISTING folder, `/rest/cluster/pending/folders` to discover peer-initiated offers), systemd service, Connie auto-start Scheduled Task, verify-sync REST calls, vault-merge procedure, and the gotchas (config path is `~/.local/state`, 404-on-GUI = process died not sync break, winget PATH needs a fresh shell, stuck-at-96% = restart the VPS service, folder paths are GUI-locked so typos need remove+re-add with the same folder ID).
- `references/claude-code-desktop.md` — Claude Code on Connie: which Claude surfaces save sessions locally (only the terminal `claude` tool; the desktop app and claude.ai stay on Anthropic's servers, unreachable), the taskbar/desktop launcher pattern (`powershell.exe -NoExit -Command claude` .lnk), and the OneDrive "Microsoft Copilot Chat Files" desktop-redirect gotcha — never guess the desktop path, use `[Environment]::GetFolderPath('Desktop')`.

## Curation conventions for the synced vault (user-locked, 2026-07-19)

The vault is for CURATED notes, not raw dumps. Rules the user set:

- **File ideas ONLY into** `01_Job_Seeker`, `02_Pipeline_Layer`, `03_Real_Results_Front_Desk`, `04_OriginatorOS`, `Personal`, `Memories`. `Jarvis/` and `Agentic OS/` are parked Julian Goldie assets — never file there.
- **"File that" = write a distilled note** into the right folder, not a raw transcript paste.
- **Raw transcripts live OUTSIDE the vault** (`/root/.hermes/mirrors/`). They are inputs to curation; only distilled notes enter the vault.
- **Never auto-file wholesale.** Automation (e.g. a future weekly digest) surfaces CANDIDATES; the user picks what gets filed. The human is the filter, the robot is the memory.
