Skip to content

fix(hooks): keep Cyrillic and other non-Latin-1 text intact in Windows hooks - #139

Merged
webdevcody merged 2 commits into
mainfrom
issue-130-cyrillic-characters-are-displayed-as
Sep 17, 2026
Merged

webdevcody merged 2 commits into
mainfrom
issue-130-cyrillic-characters-are-displayed-as

Conversation

@webdevcody

@webdevcody webdevcody commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes the AskUserQuestion overlay showing every Cyrillic character as ? (#130).

Root cause. The overlay text travels from Claude Code to Mission Control through the hook command installed in .claude/settings.local.json. On Windows that command runs under PowerShell, and Windows PowerShell 5.1 is not UTF-8 at either end:

  • [Console]::In.ReadToEnd() decodes stdin with the console code page (OEM/ANSI), so the UTF-8 payload is mis-decoded before it is sent.
  • A string -Body is encoded as ISO-8859-1, which replaces every code point outside Latin-1 (Cyrillic, CJK, emoji, en dash) with ?.

The POSIX hook (curl --data-binary @-) and the server (request.text(), SSE via TextEncoder) pass bytes through untouched, which is why the terminal behind the dialog rendered the same text correctly.

Fix (src/shared/agent-hooks.ts, PowerShell command only):

  • Read stdin as raw UTF-8 bytes via a StreamReader over OpenStandardInput().
  • POST the body as a UTF-8 byte array with charset=utf-8; byte bodies are sent verbatim on both 5.1 and pwsh.
  • For injectContext events (UserPromptSubmit, SessionStart) write the server's response bytes straight to stdout instead of the Invoke-RestMethod decode (also ISO-8859-1 on 5.1) plus ConvertTo-Json round trip. This matches the POSIX hook's verbatim passthrough.

Tests

  • agent-hooks.test.ts: the Windows command reads stdin as UTF-8, posts a byte body, and passes response bytes through only for injectContext events.
  • ask-user-question-api.test.ts: a Cyrillic AskUserQuestion payload posted as raw UTF-8 bytes survives intact through the hook endpoint and the question read endpoint.

Review follow-up (second commit)

  • The script is now actually executed. New tests run the generated hook commands under every PowerShell on PATH against a local server and compare the bytes crossing the wire in both directions (Cyrillic, CJK, emoji, en dash): request body, Authorization, Content-Type, escaped taskId, and the injectContext response on stdout. They skip when no PowerShell is installed. Verified under PowerShell 7.6.6 on macOS and under pwsh on the ubuntu-24.04 CI runner (the tests ran there, not skipped).
  • Fail-soft exit code fixed. Executing it showed that catch {} swallows the error but leaves $? false, which -Command reports as exit code 1 — so Claude saw a hook error on every event whenever Mission Control was unreachable. The script now ends in exit 0, matching the POSIX hook's || true. This predates the PR but lives in the code it rewrites.
  • Root cause reproduced. Under a forced non-UTF-8 console input encoding, the old [Console]::In.ReadToEnd() mangles Cyrillic while the new raw-byte UTF-8 read returns it intact.
  • Security: nothing user-controlled is interpolated into the generated command (slug and event come from a static table), MC_TASK_ID is EscapeDataString-escaped, the token stays in the Authorization header and out of the URL and process args, and non-2xx responses never reach stdout (-ErrorAction Stop).

Still not verified

Windows PowerShell 5.1 itself was not executed (no Windows host). The new tests pick up powershell.exe automatically on Windows, so running the suite there covers it. Only 5.1-compatible syntax is used (::new, Invoke-WebRequest -UseBasicParsing, RawContentStream).

The failing Dependency Audit check is unrelated: it fails identically on main (transitive advisories) and this PR changes no dependencies.

Closes #130

🤖 Generated with Claude Code

Windows PowerShell 5.1 decodes stdin with the console code page and encodes
a string -Body as ISO-8859-1, so every Cyrillic character in an
AskUserQuestion payload reached the native overlay as "?". The POSIX hook
(curl --data-binary) was never affected.

Read stdin as raw UTF-8 bytes, POST the body as bytes (sent verbatim on
both 5.1 and pwsh), and for injectContext events pass the server's response
bytes straight to stdout instead of decoding them and re-serializing via
ConvertTo-Json.

Adds a regression test on the Windows hook installer and a Cyrillic
end-to-end test on the hook API.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

React Doctor found 8 new issues in 8 files · 8 warnings · score 49 / 100 (Critical) · 9 fixed · vs main

8 warnings

src/components/ui/FolderBrowser.tsx

  • ⚠️ L69 React function has high control-flow complexity no-high-complexity-react-function

src/components/views/GitDiffView/index.tsx

  • ⚠️ L18 React function has high control-flow complexity no-high-complexity-react-function

src/components/views/KeybindingsSettings.tsx

  • ⚠️ L191 React function has high control-flow complexity no-high-complexity-react-function

src/components/views/ProjectDialog.tsx

  • ⚠️ L128 React function has high control-flow complexity no-high-complexity-react-function

src/components/views/ProviderUsageIndicator.tsx

  • ⚠️ L37 React function has high control-flow complexity no-high-complexity-react-function

src/components/views/TerminalPane.tsx

  • ⚠️ L185 React function has high control-flow complexity no-high-complexity-react-function

src/lib/use-theme.ts

  • ⚠️ L58 Side effect inside a state updater function no-side-effect-in-state-updater-function

src/routes/__root.tsx

  • ⚠️ L346 React function has high control-flow complexity no-high-complexity-react-function

Reviewed by React Doctor for commit 5f2f8f1. See inline comments for fixes.

…werShell

The PowerShell hook was only ever asserted on by substring, so nothing proved
the generated script parses or runs. Add tests that execute the generated
commands under every PowerShell on PATH (pwsh on the CI runners, plus Windows
PowerShell 5.1 on Windows) against a local server and compare the bytes that
cross the wire in both directions. They skip when no PowerShell is installed.

Running it surfaced a fail-soft gap: `catch {}` swallows the error but leaves
`$?` false, which `-Command` turns into exit code 1, so Claude reported a hook
error on every event whenever Mission Control was unreachable. End the script
with an explicit `exit 0`, matching the POSIX hook's `|| true`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@webdevcody
webdevcody merged commit 92b91d0 into main Sep 17, 2026
7 of 8 checks passed
@webdevcody

webdevcody commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor Author

Merged. 🙏 Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cyrillic characters are displayed as question marks

1 participant