Cyrillic Paths in PowerShell and ssh: the Encoding Trap
You work from Windows to a Linux server (say, one of the NVIDIA GB10 class), check a file whose path contains Cyrillic, and get "does not exist". The file is right there — it is the path that broke on the way. Here you see why, how to catch it with three commands, and how to carry paths without any loss.
Set-Content and Out-File "add a BOM or write ANSI" — in Windows PowerShell 5.1 Set-Content writes the ANSI code page while Out-File and > write UTF-16 (we measured both); the advice "save the script as UTF-8 and run it with -File" — for 5.1 a script with Cyrillic must be UTF-8 with BOM, otherwise it is read as ANSI; and the explanation that blamed only "capturing" — there are two directions: $OutputEncoding (towards the external program) and [Console]::OutputEncoding (from it). We added: the difference between PowerShell 5.1 and 7, a three-line check of your own setup, the capture fix ([Console]::OutputEncoding), a table for files, a note on chcp, how ssh handles the locale (by default nothing is sent), Python (PYTHONUTF8, PYTHONIOENCODING, UTF-8 mode as the default from Python 3.15) and an invented example instead of real folders. We removed internal hub names and real folders from the examples. base64 stays the most reliable way to carry a path — but it is not "the only cure": capturing has a simpler fix.
cmd /c type) plays the part of ssh, because the capture mechanism is the same. We have not tried scp/rsync with Cyrillic, nor chcp 65001 as a fix. There was no GB10-class machine, so there is no "TESTED" label.01What you'll learn
- Why a Cyrillic path "looks fine" on screen while the file check says "does not exist".
- What differs between Windows PowerShell 5.1 and PowerShell 7 when it comes to encoding.
- How to check in 30 seconds what is set on your machine and how to fix output capture.
- How to write and read Cyrillic files without loss, and why a script needs a BOM in 5.1.
- How to carry paths as base64 and how to make ssh and Python speak UTF-8.
- One safety rule before every delete.
02Before you start
- A Windows computer with PowerShell — at least the built-in Windows PowerShell 5.1 (PowerShell 7 is optional).
- The OpenSSH client (
ssh) and access to a Linux server — for example a machine of the NVIDIA GB10 class with DGX OS. - Python 3 on the server (for the base64 examples) — on Ubuntu it is there by default.
- Nothing is deleted in this lesson. All examples use an invented path — do not run a delete on your own files until you have passed "Check".
03Steps
-
See the trap with your own eyes
The script below is pure ASCII (the word is built from character codes) so that it depends on nothing.
cmd /c typeis an external program that returns UTF-8 bytes — exactly likesshwhen the server sends a name with Cyrillic.powershell · on your computer# the word "Документи" ("Documents") built from character codes only $c = [string]::new([char[]](0x414,0x43E,0x43A,0x443,0x43C,0x435,0x43D,0x442,0x438)) $t = Join-Path $env:TEMP 'enc_test.txt' # UTF-8 bytes, the way Linux sends them [IO.File]::WriteAllBytes($t, [Text.Encoding]::UTF8.GetBytes($c)) $x = cmd /c type $t # external program, its output is captured "before: $($c.Length) chars · captured: $($x.Length) chars · equal: $($x -eq $c)" Remove-Item $tOn our machine (Windows 11, Windows PowerShell 5.1, code page 437) the result was 9 → 18 characters, "equal: False". Every letter doubled, because its two bytes were read as two characters. The console may still be able to "draw" them — the internal string, though, is already different.
-
Check what is set on your machine
powershell$PSVersionTable.PSVersion $OutputEncoding.WebName [Console]::OutputEncoding.WebName [Console]::InputEncoding.WebName chcpOn ours:
5.1.26100.…,us-ascii,IBM437,IBM437and "Active code page: 437". Yours may differ — it depends on the language and settings of Windows. What each line means:Setting What it controls Default (per the documentation) $OutputEncodingHow PowerShell sends text to an external program (pipe, ssh)Windows PowerShell 5.1: ASCII · PowerShell 7: UTF-8 [Console]::OutputEncodingHow the console translates the output of external programs The console code page, from the system locale (.NET). ⚠️ We have not checked the PowerShell 7 behaviour — run the command and look chcpThe active console code page Depends on the system (437, 850, 866…); 65001 is UTF-8 ASCII in
$OutputEncodingmeans every Cyrillic letter sent to an external program from 5.1 turns into?— irreversibly. Capturing from the program is the other end of the same trap. -
Fix the output capture
Tell the console that external programs speak UTF-8. This is for the current session (it does not change Windows):
powershell[Console]::InputEncoding = [Text.UTF8Encoding]::new($false) [Console]::OutputEncoding = [Text.UTF8Encoding]::new($false) $OutputEncoding = [Console]::OutputEncodingRepeat the test from step 1: on our machine, after this setting the captured text became equal to the original ("equal: True"). The PowerShell documentation advises that
$OutputEncodingshould match[Console]::InputEncoding, so we set them together. If you want this for every session, put the lines in your profile — but then it applies to everything you launch from there.⚠️What aboutchcp 65001?It is often recommended as the matching switch on the console itself. Microsoft'schcppage does not include it in its table of code pages, and we have not tested it — so we offer it as an idea to try, not as a fix. -
Write and read files with an explicit encoding
This is where the two versions differ most. We measured on 5.1; for 7 we follow the documentation.
Command Windows PowerShell 5.1 PowerShell 7 Set-Contentwithout-EncodingThe system's ANSI code page — on ours Cyrillic became 3F(?)UTF-8 without BOM Out-Fileand>UTF-16 with BOM (measured: first bytes FF FE)UTF-8 without BOM -Encoding asciievery non-ASCII character → ?the same -Encoding utf8UTF-8 with BOM (measured: EF BB BF)UTF-8 without BOM; utf8BOMis a separate valuepowershell# write and read with an explicit encoding — works in both versions Set-Content -Path .\notes.txt -Value $text -Encoding utf8 Get-Content -Path .\notes.txt -Encoding utf8 # optional: UTF-8 by default for every cmdlet in the session $PSDefaultParameterValues['*:Encoding'] = 'utf8'Microsoft's documentation also reminds us about the BOM: for best compatibility UTF-8 files should be without a BOM, because Unix tools (
cat,sed,awk) do not understand it. So: a file that Linux programs will read, write in PowerShell 7 withutf8, or in 5.1 with[IO.File]::WriteAllText($path, $text, [Text.UTF8Encoding]::new($false)). -
A script with Cyrillic: UTF-8 with BOM for 5.1
Windows PowerShell 5.1 reads a script without a BOM as ANSI. So a script with Cyrillic written in an editor that keeps UTF-8 without BOM (as VS Code does) breaks its strings or fails to parse (
Unexpected token,string is missing the terminator). According to Microsoft, if the script has non-ASCII characters, save it as UTF-8 with BOM. Do not pass long scripts as a single string through-Command— save them to a file and use-File, or encode them in base64 (step 6). -
Carry the path as base64
Why: base64 is pure ASCII and goes through any pipe, code page and file unchanged. You encode on the UTF-8 side, carry ASCII, and decode to a real Unicode string right before use. The path in the example is invented.
bash · on the serverpython3 - <<'EOF' import base64 p = "Документи/Отчети/годишен-отчет.txt" print(base64.b64encode(p.encode("utf-8")).decode("ascii")) EOF # 0JTQvtC60YPQvNC10L3RgtC4L9Ce0YLRh9C10YLQuC/Qs9C+0LTQuNGI0LXQvS3QvtGC0YfQtdGCLnR4dA==powershell · on your computerfunction ConvertFrom-B64([string]$b) { [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($b)) } $path = ConvertFrom-B64 '0JTQvtC60YPQvNC10L3RgtC4L9Ce0YLRh9C10YLQuC/Qs9C+0LTQuNGI0LXQvS3QvtGC0YfQtdGCLnR4dA==' $path # a correct Unicode string, in memory # for a local file: always -LiteralPath # Get-Item -LiteralPath $pathIn the other direction — a script for the server with Cyrillic inside — you encode the script to base64 on your computer and pass it so that only ASCII crosses the pipe:
powershell · on your computer$b64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($script)) ssh <user>@<server-address> "echo $b64 | base64 -d | bash"We checked the round trip (Linux → base64 → decoding in PowerShell 5.1) on an invented path: the result is the same string. The
sshconnection itself was not run (see "What we have not run ourselves"). -
Set up the locale and Python on the server
Two things to remember. First, OpenSSH sends no environment variables by default (
SendEnv) and the server accepts none (AcceptEnv) — your locale does not travel by itself. ⚠️ Distributions often change this in their configuration files; it is not in the official OpenSSH documentation, so check your own machine. The simplest approach is to set the locale in the command itself.bash · on the serverlocale -a | grep -i utf # is C.UTF-8 there? LC_ALL=C.UTF-8 python3 -c "import sys; print(sys.stdout.encoding)"Second, Python has its own UTF-8 mode. According to the documentation it switches on automatically when the locale is
CorPOSIX. We checked in the sandbox:bashLC_ALL=C python3 -c "import sys; print(sys.stdout.encoding, sys.flags.utf8_mode)" # utf-8 1 LC_ALL=C PYTHONUTF8=0 python3 -c "import sys; print(sys.stdout.encoding, sys.flags.utf8_mode)" # ascii 0Variable What it does (docs.python.org) PYTHONUTF8=1Turns UTF-8 mode on; 0turns it off. The mode makesopen()and paths use UTF-8PYTHONIOENCODINGSets the encoding of stdin/stdout/stderr. On Windows it is ignored for an interactive console unless PYTHONLEGACYWINDOWSSTDIOis set; redirected files and pipes are not affectedPython 3.15+ UTF-8 mode is the default (PEP 686, status "Final"). Before that, on Windows open()uses the system code page — set$env:PYTHONUTF8 = '1'before starting the interpreterFor
scpandrsync— file names are bytes, so the capture and transport rules above apply to whatever you call from PowerShell. ⚠️ We have not checked special settings for encoding their names themselves (rsynchas an--iconvoption — seeman rsync, not verified here). -
The safety rule before deleting
🛑"Does not exist" often means "broken string"Never delete on the strength of a "missing" check if the path went through a pipe raw with non-ASCII. First prove you are reading the right string (decode from base64 or run the test from step 1), check existence and size, and only then act. Do the heavy path work on Linux (Python/bash) — UTF-8 is native there, and let Windows be only a thin transport.Situation ✅ Do ❌ Don't A Cyrillic path between Windows and Linux base64 end to end a raw string through the pipe Writing a file -Encoding utf8(or explicit UTF-8 without BOM for Linux)-Encoding ascii, or "the default" in 5.1A script with Cyrillic for 5.1 UTF-8 with BOM, run with -Filea long string through -CommandAccessing the path -LiteralPathand a decoded stringan interpolated string with Cyrillic through a pipe Before deleting a check with a decoded path trusting "does not exist"
04Check
- You know which PowerShell you use and the values of
$OutputEncodingand[Console]::OutputEncoding. - The test from step 1 gives "equal: True" after the fix.
- Nothing on your side writes with
-Encoding ascii; scripts with Cyrillic for 5.1 are UTF-8 with BOM. - A Cyrillic path goes from Linux to Windows as base64 and is decoded to a Unicode string.
- Python on Windows runs with
PYTHONUTF8=1(or is 3.15+).
Quiz
1. What is the default value of $OutputEncoding in Windows PowerShell 5.1?
2. A file check returns "does not exist", but the path has Cyrillic and went through ssh and PowerShell. The most sensible first move?
3. Which encoding do Out-File and the > operator use by default in Windows PowerShell 5.1?
4. A script with Cyrillic saved without a BOM is run in Windows PowerShell 5.1. What happens?
05What's next
06Sources
- about_Character_Encoding — default encodings and the BOM in 5.1 and 7, scripts without a BOM.
- about_Preference_Variables —
$OutputEncoding(ASCII in 5.1, UTF-8 in 7). - Console.OutputEncoding (.NET) · chcp — the console code page.
- Python: command line and environment —
PYTHONUTF8,PYTHONIOENCODING· UTF-8 mode · PEP 686 — the default mode from 3.15. - ssh_config (
SendEnv) · sshd_config (AcceptEnv) — environment variables over ssh.