{“content”:”---\nname: windows-remote-file-edit\ndescription: Edit files on remote Windows PC via SSH when direct commands fail — base64 + PS1 upload workflow\ntags: [ssh, windows, powershell, remote-edit, file-transfer]\n---\n\n# Windows Remote File Edit via SSH\n\nEdit files on remote Windows PC through Hermes (Termux/Android) when direct SSH heredoc or PowerShell exec fails due to escaping/length issues.\n\n## When to Use\n- SSH commands with complex strings (PowerShell, regex, special chars) fail with “exec request failed on channel 0”\n- Base64-encoded content exceeds SSH exec length limit\n- PowerShell -replace regex patterns get mangled by bash/SSH escaping\n- Need to modify a remote file precisely (line-by-line or string replacement)\n\n## The Reliable Workflow: Base64 + PS1 Upload\n\n### Step 1: Read remote file → base64 → local\nbash\nSSH_KEY=~/.ssh/id_ed25519_hermes\[email protected]\nREMOTE_PATH=\"D:\\\\path\\\\to\\\\file.js\"\n\nssh -i \"$SSH_KEY\" -o StrictHostKeyChecking=no \"$HOST\" \\\n \"powershell -Command \\\"[Convert]::ToBase64String([System.IO.File]::ReadAllBytes('$REMOTE_PATH'))\\\"\" \\\n > ~/remote_file.b64\n\n\n### Step 2: Python — decode, modify, re-encode\npython\nimport base64\n\nwith open('~/remote_file.b64') as f:\n b64_data = f.read().strip()\n\njs_bytes = base64.b64decode(b64_data)\ncontent = js_bytes.decode('utf-8')\nlines = content.split('\\n')\n\n# Apply targeted fixes by line index or content\nnew_lines = []\ni = 0\nwhile i < len(lines):\n line = lines[i]\n # Example: fix function signature\n if line.strip() == 'function secDragOver(e) {':\n new_lines.append('function secDragOver(e, si, el) {')\n i += 1; continue\n new_lines.append(line)\n i += 1\n\nnew_content = '\\n'.join(new_lines)\nnew_b64 = base64.b64encode(new_content.encode('utf-8')).decode()\n\nwith open('~/modified_file.b64', 'w') as f:\n f.write(new_b64)\n\n\n### Step 3: Write base64 to remote temp in CHUNKS\n\nCritical: Base64 for even a 22KB HTML file is ~30KB — too long for a single SSH command or embedded in a PS1 script. Use chunked Add-Content instead:\n\nbash\n# 3a. Clear/create the remote temp file first\nssh -i \"$SSH_KEY\" -o StrictHostKeyChecking=no \"$HOST\" \\\n \"powershell -Command \\\"Remove-Item 'C:/Users/sozo/AppData/Local/Temp/modified_file.b64' -EA SilentlyContinue\\\"\"\n\n# 3b. Write base64 in 2000-char chunks (Add-Content -NoNewline is essential)\nCHUNK_SIZE=2000\nfor i in $(seq 0 $CHUNK_SIZE $(($(wc -c < ~/modified_file.b64) - 1))); do\n chunk=$(sed -n \"${i},$((i+CHUNK_SIZE-1))p\" ~/modified_file.b64)\n ssh -i \"$SSH_KEY\" -o StrictHostKeyChecking=no \"$HOST\" \\\n \"powershell -Command \\\"Add-Content -Path 'C:/Users/sozo/AppData/Local/Temp/modified_file.b64' -Value '$chunk' -NoNewline\\\"\"\ndone\n\n\n> Why chunks? A single SSH command with 30KB of base64 embedded fails with “exec request failed on channel 0”. The chunk approach avoids any embedded content in the SSH command itself.\n\n### Step 4: Decode on remote and write the file\nbash\nssh -i \"$SSH_KEY\" -o StrictHostKeyChecking=no \"$HOST\" \\\n 'powershell -Command \"$b64 = Get-Content C:/Users/sozo/AppData/Local/Temp/modified_file.b64 -Raw; [System.IO.File]::WriteAllBytes('\\\"'\\\"'D:\\\\path\\\\to\\\\file.js'\\\"'\\\"', [Convert]::FromBase64String($b64)); Write-Output \\\"Done\\\"\"'\n\n\n## Alternative: Simple PS1 Upload (Better for JSON/YAML Text Files)\n\nFor patching JSON or YAML config files (not binary), this simpler approach works well:\n\n### Step 1: Write PS1 script locally\nUse write_file to create the .ps1 file in ~/.hermes/ or $HOME/.\n\n### Step 2: SCP upload to PC temp\nbash\nSCP_PATH='C:\\Users\\sozo\\AppData\\Local\\Temp'\nscp -o StrictHostKeyChecking=no /path/to/patch.ps1 \"[email protected]:${SCP_PATH}\\\\patch.ps1\"\n\n\n### Step 3: Execute via SSH\nbash\nssh [email protected] \"powershell -ExecutionPolicy Bypass -File C:\\Users\\sozo\\AppData\\Local\\Temp\\patch.ps1\"\n\n\n### When to use this vs base64 method:\n- PS1 upload: Simple string/value replacements in JSON, YAML, or text configs. No binary content.\n- Base64 method: Binary files, large files, or when the PS1 regex doesn’t reliably match the target.\n\n### Example PS1 for JSON patch (OpenClaw config):\npowershell\n$json = Get-Content \"D:\\OpenClaw_Home\\.openclaw\\openclaw.json\" -Raw | ConvertFrom-Json\n$json.agents.defaults.model = \"openrouter/free\"\n$json.agents.defaults.models = @{\n \"openrouter/free\" = @{}\n \"minimax/MiniMax-M2.7\" = @{}\n \"openrouter/auto\" = @{}\n}\n$json.agents.list[0].model.primary = \"openrouter/free\"\n$json | ConvertTo-Json -Depth 20 | Set-Content \"D:\\OpenClaw_Home\\.openclaw\\openclaw.json\" -Encoding UTF8\n\n\n### Example PS1 for YAML patch (Hermes2 config):\npowershell\n$c = Get-Content \"D:\\hermes2\\config.yaml\" -Raw\n$c = $c -replace 'default: deepseek/deepseek-chat', 'default: openrouter/free'\nSet-Content \"D:\\hermes2\\config.yaml\" -Value $c -Encoding UTF8\n\n\n---\n\n## Key Learnings (Pitfalls)\n\n1. SSH exec fails with long commands: exec request failed on channel 0 — means SSH exec channel died. Split into chunked base64 upload + decode instead of SCP+PS1.\n2. Python repr() is ambiguous for \\n: When repr() shows \\\\n, it could mean literal backslash+n OR an actual newline escaped by repr. Always use ord(c) to confirm: ord=10 = actual newline, ord=92 = backslash. This matters when searching for literal \\n strings in HTML/JS source.\n3. Box-drawing characters corrupt: Files with ── (U+2500) get corrupted to 鈹€ on Windows UTF-8 read. Use [System.Text.Encoding]::UTF8 explicitly, or errors='replace' on decode. The corruption is cosmetic — JS logic unaffected.\n4. Remote temp path: Use C:/Users/sozo/AppData/Local/Temp/ (Windows path via SSH) — /tmp/ doesn’t exist on Windows SSH.\n5. Duplicate declarations: If patching a file that already has partial patches, read current state via Select-String before writing. Duplicate let declarations cause “identifier already declared” errors.\n6. Event binding vs function signature mismatch: When HTML adds event listeners like e => secDragStart(si, secEl), the handler function must accept (si, el) not (e). Always verify bindings match signatures.\n7. SCP upload permission denied on /tmp/: Local Termux /tmp/ may not be writable by scp. When the chunked Add-Content approach is used, no local temp file is needed for SCP — just write chunks directly via SSH.\n8. PS1 regex -replace may not match multi-line or indented YAML keys: Simple -replace 'pattern', 'value' works for single-line values. For complex YAML with indentation sensitivity, test patterns first or use the base64 method.\n\n9. Python on Windows GBK stdout encoding: When running Python scripts via SSH on Windows (python script.py), sys.stdout and sys.stderr are encoded with GBK (system codepage). Unicode characters like (U+25B4), (U+25BE), or any non-GBK char cause UnicodeEncodeError: 'gbk' codec can't encode character. Even reading the file with UTF-8 works fine — the problem is only with output. Workarounds:\n - Use print(ascii(text)) instead of print(text) — ASCII escapes won’t trigger encoding errors\n - Use sys.stdout.buffer.write(f\"{text}\\n\".encode('utf-8')) for binary stdout\n - For hex debugging: print(ord(c), hex(ord(c))) instead of printing the char directly\n - This also affects repr() output — repr internally calls str() which encodes to stdout encoding\n - Alternative: use PowerShell’s Out-File -Encoding UTF8 or write to a temp file for output inspection\n\n10. Git push from remote PC via SSH — must pull first: When pushing from a PC where other sessions may have committed, git push fails with ! [rejected] main -> main (fetch first). Always do this sequence:\n powershell\n # Step 1: pull (accept remote changes, auto-merge if possible)\n git -C \"D:\\\\path\\\\to\\\\repo\" pull origin main --no-edit\n # If auto-merge fails with CONFLICT, resolve conflicts first, then:\n git -C \"D:\\\\path\\\\to\\\\repo\" add -A\n git -C \"D:\\\\path\\\\to\\\\repo\" commit -m \"resolve conflict\"\n # Step 2: now push\n git -C \"D:\\\\path\\\\to\\\\repo\" push origin main\n \n For trivial/predicted conflicts (like accepting all remote changes), use git checkout --theirs <file> then git add <file> before commit.\n\n## Vision Analysis Limitation\n\n**vision_analyze tool cannot read image URLs** — it only processes:\n- Local file paths (file:///path/to/image.png)\n- Telegram message attachments (user uploads directly in chat)\n- Files saved to Hermes home dir (/data/data/com.termux/files/home/)\n\nWorkarounds for analyzing remote images:\n1. Download to local first: curl -o ~/image.png https://example.com/image.png → then use file:///data/data/com.termux/files/home/image.png\n2. Upload to Google Drive (get shareable link) → but vision_analyze can’t read Drive links either\n3. Ask user to send via Telegram — most reliable\n4. Delegate to subagent with vision tools: use delegate_task with vision-enabled agent\n\nFor HS Design logo: The logo at https://hsdesign.biz/images/logo.png is 1920×736 PNG with RGBA. User reports white background and odd proportions. Awaiting user to send screenshot via Telegram for analysis.\n\n---\n\n## SSH Connection Defaults\n- Key: ~/.ssh/id_ed25519_hermes\n- Host: 100.83.112.84\n- User: sozo\n- Project path: D:\\\\OpenClaw_Home\\\\.openclaw\\\\workspace\\\\projects\\\\hs-design-landing\\\\\n\n## HS Design Landing Page Notes\n- CSS is embedded inline inside <style> tags in index.html — NOT a separate .css file\n- This means edits go to index.html, not a separate CSS file\n- nav-logo: <div class=\"nav-logo\"><img src=\"images/logo.png\" alt=\"...\"></div> at top of nav\n\n## Key Learnings (Pitfalls) — Updated\n11. Git commit -m with spaces fails via SSH cmd: git commit -m \"fix: multi word message\" via SSH+cmd fails because words are treated as separate path arguments. Fix: use git commit -F FILE with a commit message file instead:\n bash\n # Write message to local temp, base64 upload, then:\n ssh $HOST \"cmd /c \\\"cd /d D:\\\\path\\\\to\\\\repo && git commit -F C:\\\\Users\\\\sozo\\\\AppData\\\\Local\\\\Temp\\\\commit_msg.txt && git push\\\"\"\n \n This applies to any multi-word commit messages via SSH on Windows.\n\n12. Google Drive token auto-refresh: Token stored in ~/.hermes/google_token.json expires. Refresh programmatically:\n python\n import json, urllib.request, urllib.parse, datetime\n t = json.load(open('/data/data/com.termux/files/home/.hermes/google_token.json'))\n data = urllib.parse.urlencode({\n 'client_id': t['client_id'],\n 'client_secret': t['client_secret'],\n 'refresh_token': t['refresh_token'],\n 'grant_type': 'refresh_token'\n }).encode()\n req = urllib.request.Request('https://oauth2.googleapis.com/token', data=data)\n resp = urllib.request.urlopen(req, timeout=10)\n result = json.loads(resp.read())\n t['token'] = result['access_token']\n t['expiry'] = (datetime.datetime.utcnow() + datetime.timedelta(seconds=result.get('expires_in', 3600))).strftime('%Y-%m-%dT%H:%M:%SZ')\n with open('/data/data/com.termux/files/home/.hermes/google_token.json', 'w') as f:\n json.dump(t, f, indent=2)\n \n The account field in token JSON is empty; the token applies to [email protected].\n\n13. Download file from Google Drive by ID: After refreshing token, download using:\n python\n import urllib.request\n TOKEN=json.l...en']\n FILE_ID = \"17D05SzdBBqRKJHxFhr78JOfLSSvkO79D\" # extract from drive.google.com/open?id=FILE_ID\n req = urllib.request.Request(\n f\"https://www.googleapis.com/drive/v3/files/{FILE_ID}?alt=media\",\n headers={\"Authorization\": f\"Bearer {TOKEN}\"}\n )\n with open('/data/data/com.termux/files/home/FILENAME', 'wb') as f:\n f.write(urllib.request.urlopen(req).read())\n \n File metadata: GET /drive/v3/files/{FILE_ID}?fields=name,mimeType,size.\n\n14. SCP file upload to Windows — never use quoted D:\\path directly:\n - scp file \"sozo@host:D:\\\\path\\\\file\" → fails: No such file or directory\n - Correct: SCP to temp first, then PowerShell move:\n bash\n scp -i ~/.ssh/id_ed25519_hermes FILE sozo@host:C:/Users/sozo/AppData/Local/Temp/file\n \n - PowerShell New-Item -Path \"D:\\dir\" | Out-Null fails when run via SSH bash context because bash interprets the pipe before PowerShell sees it\n - Fix: Write a .ps1 script, SCP to temp, execute via SSH:\n bash\n ssh host \"powershell -ExecutionPolicy Bypass -File C:/Users/sozo/AppData/Local/Temp/script.ps1\"\n \n - Forward slashes in SCP target (C:/Users/...) work better than backslashes on Windows SSH.\n - Temp path for Windows SSH: always use C:/Users/sozo/AppData/Local/Temp/ (not /tmp/).\n”}