Files
ergosteurandClaude Opus 5 fd022a195c Add ssh-copy-id-win: ssh-copy-id with Windows OpenSSH support
A fork of ssh-copy-id that installs keys on Windows OpenSSH servers, for
both regular users and members of Administrators, while keeping the
standard behaviour on *nix targets.

Windows design constraints:

- Dispatches everything as `cmd.exe /c "<inner>"`, since a host's sshd
  DefaultShell is unpredictable. PowerShell is never assumed present.
- Uses exactly one quote pair with no quotes inside <inner>. A PowerShell
  DefaultShell re-escapes nested quotes as \", which cmd.exe cannot parse.
  Staying quote-free means cd'ing to the base directory first and using
  short relative paths, and deduping on the whitespace-free base64 blob.
- No hard-coded drive letters; %USERPROFILE% and %ProgramData% only.
- Administrators are written to administrators_authorized_keys with the
  SYSTEM + Administrators ACL that Windows sshd requires, because the
  stock Match Group administrators block makes it the only file consulted
  for those accounts.
- Group and ACL checks match on SIDs, not names, so they work on
  non-English Windows locales.
- All steps share one multiplexed connection, so password-auth hosts
  prompt once rather than once per step.

Two cmd.exe parsing traps are worked around, both of which corrupted the
remote file with no error output:

- `echo KEY>>file && echo DONE` leaves the space before `&&` inside the
  echoed text, appending a trailing space that defeats exact-match dedup
  and duplicates the key on every run. Uses `(echo KEY)>>file` instead.
- An unparenthesised `if not exist ... & ...` swallows the rest of the
  command chain when the directory already exists, silently skipping
  everything after it.

Verified on Linux, Windows Administrator, and Windows regular-user
targets, each for both first install and duplicate detection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 12:11:09 -04:00

5.2 KiB

ssh-scripts

SSH key / authentication tooling. These notes travel with the repo.

ssh-copy-id-win

A personal fork of ssh-copy-id that supports Windows OpenSSH servers (both regular users and Administrators) while keeping full *nix behaviour.

This repo is the source of truth; ~/.local/bin/ssh-copy-id-win is a deployed copy on the fish PATH. They are separate files, so edits here do not take effect until redeployed:

cp ./ssh-copy-id-win ~/.local/bin/ssh-copy-id-win   # deploy
diff ./ssh-copy-id-win ~/.local/bin/ssh-copy-id-win # check for drift

Edit the repo copy, not the deployed one. If they ever disagree, the deployed copy is the one that has been running.

ssh-copy-id-win [-i identity] [-p port] [-o ssh_opt] [-A|-U] [-n] [user@]host
  -A / -U   force the Administrator / regular-user path on Windows
  -n        dry run, prints the exact remote command

Design constraints (do not regress these)

  • cmd.exe only. Everything is dispatched as cmd.exe /c "<inner>" because a host's sshd DefaultShell is unpredictable. PowerShell is never assumed to exist on a target.
  • Exactly one quote pair, zero quotes inside <inner>. If the remote default shell is PowerShell, sshd runs powershell -c "cmd.exe /c \"<inner>\"" and PowerShell re-escapes nested quotes as \", which cmd.exe does not understand. To stay quote-free the script cd /d %USERPROFILE% (or %ProgramData%) first and then uses short relative paths that cannot contain spaces, and dedups on the base64 key blob alone since that field is whitespace-free.
  • No hard-coded drive letters. %USERPROFILE% and %ProgramData% only.
  • Admins need the ProgramData file. Stock Windows sshd_config has a Match Group administrators block making %ProgramData%\ssh\administrators_authorized_keys the only file consulted for admin accounts. It also requires a restrictive ACL (SYSTEM + Administrators), which the script reapplies after every write.
  • Locale independence. Never match English text in Windows command output. Admin detection uses the S-1-5-32-544 SID; ACLs are granted by SID (*S-1-5-32-544, *S-1-5-18).
  • One connection. All steps share a multiplexed ControlMaster connection so password-auth hosts prompt once, not once per step.

Two cmd.exe traps that fail silently

Both of these produced a corrupt file with no error message, and both cost real debugging time. They are also commented in the script header.

  1. Trailing space from a mid-line redirect. echo KEY>>file && echo DONE — cmd lifts the redirect out of the middle, so the space before && ends up inside the echoed text. Every key landed as ...comment with a trailing space, so the next run's exact-match dedup never matched and appended a duplicate. Forever. Use (echo KEY)>>file instead.

  2. if swallowing the rest of the chain. if not exist ssh mkdir ssh & findstr ... && echo A || echo B — when the directory already exists, cmd treats the entire remainder as the if-body and skips all of it, so the command produces no output at all. Parenthesise it: (if not exist ssh mkdir ssh) & ...

Testing

Verified paths: Linux, Windows Administrator, Windows regular user — each for both first-install and duplicate detection.

The three targets used were a Linux host, a domain Windows host reached as a member of Administrators (key auth), and the same Windows host reached as an ordinary domain user (password auth only). Specific hostnames and accounts are deliberately kept out of this repo.

Conventions when working on this:

  • Password-auth hosts cannot be tested from inside a Claude Code session. The Bash tool and the ! prefix give ssh no TTY, so it dies with exit 255. Ask the user to run those in a real terminal.
  • Test additive changes with a throwaway ssh-keygen key rather than the real one — it never risks locking you out of the box you are testing on.
  • When rewriting a remote authorized_keys, stage to %TEMP% and copy /y over the target, so the original survives a failed write.
  • Where pwsh 7 happens to be installed on a target it is fine for debugging only — the script must never depend on it. pwsh -EncodedCommand (base64 UTF-16LE) sidesteps all quoting layers; certutil -encodehex <in> <out> gives byte-level output for spotting trailing whitespace and CRLF issues, and needs nothing beyond cmd.
  • Test against a non-English Windows locale if you can. Command output is localised, which is exactly why group and ACL checks match on SIDs (S-1-5-32-544, S-1-5-18) rather than on names like "Administrators".
  • Always verify a written key by authenticating with it, not just by reading the file back.

Local environment

The user's shell is fish, but the Bash tool runs bash/zsh. Checking $PATH, aliases, or functions through the Bash tool reflects the wrong shell — verify with fish -c '...', and persist path changes with fish_add_path.

Note ssh-copy-id upstream picks its default key with ls -dt ~/.ssh/id*.pub | head -1 — i.e. by mtime, newest first — whereas ssh itself uses fixed default filenames (id_rsa, id_ecdsa, id_ed25519, ...). The two can disagree, which is confusing when a touch reorders things.