Как уже в курсе любимый читатель, мы живем в эпоху искусственного интеллекта и агентской разработки. ИИшечки пишут код, код работает, все счастливы. Но есть одна проблема — всё около-иишное как правило живёт и мыслится в окружении macOS/Linux и в контексте в первую очередь питона. А что делать, если ты хочешь использовать иишку для разработки на павершелле и использовать Клода в среде Виндоуз? Ответ по умолчанию — страдать.
Чтобы не было проблем с кириллицей, скрипты нужно хранить в UTF-16 LE с BOM — так их одинаково однозначно опознают что PowerShell 5.1, что pwsh 7+, что тестовые редакторы под Windows, что git/gitlab, и все живут дружно. А вот Claude Code с его встроенными инструментами `Edit` и `Write` живёт только в своей реальности UTF-8 и плевать хотел, в какой кодировке был файл. А поэтому и читать-редактировать старые наработки по умолчанию не может, и новое пишет сломанное.
Проблема старая и известная, но как ’это часто бывает с корнер кейсами, проблемы индейцев шерифа не волнуют — ни фикса, ни какой-либо настройки для клода не существует и, скорее всего, не предвидится. ИЧСХ, проблема тут не только кириллическая, у японцев например ровно та же беда, только с Shift_JIS вместо UTF-16. Но кому какое дело до этих ваших туземных особенностей?
Сразу как только я наступил на эти грабли, родился обходной маневр: инструкцией в CLAUDE.md агенту было сказано для редактирования PowerShell скриптов делать построчную замену вызовом того же PowerShell. И это в целом работало, но сильно замедляло (и часто ломало) процесс — решение было кустарным, хрупким, и разрешение спрашивалось на каждый чих. Агент регулярно забывал инструкции, а чтобы вспомнить сначала бегло бежал по садовому инвентарю, отнимая моё время, нервные клетки, и тратя драгоценные токены.
Сегодня, устав от очередной порчи скрипта при небольшом изменении, я отправил агента в рисёрч с требованием раз и навсегда решить проблему в корне. Клод обнаружил в интернетах братьев-японцев и других потерпевших, но главное — придумал использовать хуки, которые прозрачно конвертируют файл туда-обратно при редактировании встроенными инструментами. Я затестил и это работает. Щас расскажу.
Доктор Крюк, или как я перестал страдать и полюбил хуки
У Claude Code есть `PreToolUse`/`PostToolUse`/`Stop` hooks — команды, которые выполняются автоматически вокруг вызовов инструментов. Идея простая: пусть файл на диске «в покое» всегда лежит в UTF-16 LE, а хуки на лету превращают его в UTF-8 непосредственно перед тем, как его коснётся Read/Edit/Write, и обратно — сразу после. Модель вообще не должна догадываться, что файл когда-то был не в UTF-8.
Настройки хуков в ~/.claude/settings.json:
"hooks": {
"PreToolUse": [
{
"matcher": "Read|Edit",
"hooks": [{
"type": "command",
"command": "C:\\Program Files\\PowerShell\\7\\pwsh.exe",
"args": ["-NoProfile", "-File", "C:\\Users\\[USER]\\.claude\\hooks\\ps1-pre-encoding.ps1"]
}]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "C:\\Program Files\\PowerShell\\7\\pwsh.exe",
"args": ["-NoProfile", "-File", "C:\\Users\\[USER]\\.claude\\hooks\\ps1-post-encoding.ps1"]
}]
}
],
"Stop": [
{
"hooks": [{
"type": "command",
"command": "C:\\Program Files\\PowerShell\\7\\pwsh.exe",
"args": ["-NoProfile", "-File", "C:\\Users\\[USER]\\.claude\\hooks\\ps1-restore-all.ps1"]
}]
}
]
}pre-encoding.ps1 (перед Read/Edit — если увидел BOM UTF-16 LE, конвертирует в UTF-8 и запоминает путь):
$ErrorActionPreference = 'Stop'
$stateFile = "C:\Users\[USER]\.claude\hooks\ps1-utf16-state.json"
$scopeRoot = "[ВАШ ПУТЬ]"
try {
$inputJson = [Console]::In.ReadToEnd()
$data = $inputJson | ConvertFrom-Json
$filePath = $data.tool_input.file_path
if (-not $filePath) { exit 0 }
if ($filePath -notmatch '\.ps1$') { exit 0 }
if (-not $filePath.StartsWith($scopeRoot, [System.StringComparison]::OrdinalIgnoreCase)) { exit 0 }
if (-not (Test-Path -LiteralPath $filePath -PathType Leaf)) { exit 0 }
$bytes = [System.IO.File]::ReadAllBytes($filePath)
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) {
$content = [System.IO.File]::ReadAllText($filePath, [System.Text.Encoding]::Unicode)
[System.IO.File]::WriteAllText($filePath, $content, (New-Object System.Text.UTF8Encoding($false)))
$state = @()
if (Test-Path -LiteralPath $stateFile) {
$raw = Get-Content -LiteralPath $stateFile -Raw -ErrorAction SilentlyContinue
if ($raw) { $state = @($raw | ConvertFrom-Json) }
}
if ($state -notcontains $filePath) { $state += $filePath }
($state | ConvertTo-Json) | Set-Content -LiteralPath $stateFile -Encoding utf8
}
} catch {
exit 0
}
exit 0post-encoding.ps1 (после Edit/Write — конвертирует обратно в UTF-16 LE, но только файлы из стейта):
$ErrorActionPreference = 'Stop'
$stateFile = "C:\Users\[USER]\.claude\hooks\ps1-utf16-state.json"
try {
$inputJson = [Console]::In.ReadToEnd()
$data = $inputJson | ConvertFrom-Json
$filePath = $data.tool_input.file_path
if (-not $filePath) { exit 0 }
if ($filePath -notmatch '\.ps1$') { exit 0 }
if (-not (Test-Path -LiteralPath $stateFile)) { exit 0 }
$raw = Get-Content -LiteralPath $stateFile -Raw -ErrorAction SilentlyContinue
if (-not $raw) { exit 0 }
$state = @($raw | ConvertFrom-Json)
if ($state -notcontains $filePath) { exit 0 }
if (-not (Test-Path -LiteralPath $filePath -PathType Leaf)) { exit 0 }
$content = [System.IO.File]::ReadAllText($filePath, [System.Text.Encoding]::UTF8)
[System.IO.File]::WriteAllText($filePath, $content, [System.Text.Encoding]::Unicode)
$state = @($state | Where-Object { $_ -ne $filePath })
($state | ConvertTo-Json) | Set-Content -LiteralPath $stateFile -Encoding utf8
} catch {
exit 0
}
exit 0restore-all.ps1 (Stop-хук — подчищает в конце хода всё, что осталось не восстановленным после одиночного Read без последующего Edit):
$ErrorActionPreference = 'Stop'
$stateFile = "C:\Users\[USER]\.claude\hooks\ps1-utf16-state.json"
try {
[Console]::In.ReadToEnd() | Out-Null
if (-not (Test-Path -LiteralPath $stateFile)) { exit 0 }
$raw = Get-Content -LiteralPath $stateFile -Raw -ErrorAction SilentlyContinue
if (-not $raw) { exit 0 }
$state = @($raw | ConvertFrom-Json)
foreach ($filePath in $state) {
if (-not (Test-Path -LiteralPath $filePath -PathType Leaf)) { continue }
$bytes = [System.IO.File]::ReadAllBytes($filePath)
$isUtf16Le = $bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE
if (-not $isUtf16Le) {
$content = [System.IO.File]::ReadAllText($filePath, [System.Text.Encoding]::UTF8)
[System.IO.File]::WriteAllText($filePath, $content, [System.Text.Encoding]::Unicode)
}
}
Set-Content -LiteralPath $stateFile -Value "[]" -Encoding utf8
} catch {
exit 0
}
exit 0Собственно, делать эти настройки руками не нужно — просто скопипастите текст заметки в чат своему Клоду и попросите настроить так же.
Доклад окончен
Скрипты на диске и в git теперь всегда лежат в UTF-16 LE с BOM — ровно так, как его и ждёт PowerShell. Пока файла касается Claude Code — он на долю секунды «мигает» в UTF-8 и обратно, а модель работает через свои обычные беззаботные Edit/Write. Роботы работают, человек не напрягается.
Важная тонкость — в скрипты зашита проверка на них самих, чтобы создавая-редактируя их Клод не попадал в бесконечную кодировочную петлю.
Пойду, завайбкожу ещё что-нибудь.
Слава роботам! КЭНК!