fix(500): the hooks decode a \u escape to UTF-8 in any locale and any awk
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / integration (push) Successful in 1m1s
CI & Build / Python tests (push) Successful in 1m57s
CI & Build / Build & push image (push) Successful in 25s

scribe_json_unescape wrote each escaped code point with sprintf("%c", n),
which writes the character only under gawk in a UTF-8 locale; gawk in the C
locale and mawk in any locale write the single byte n % 256. The server
escapes every non-ASCII character, so "·" reached the session as 0xB7, "—"
as 0x14 and an emoji as a NUL wherever a hook ran without a UTF-8 locale.
The decoder now encodes UTF-8 itself under LC_ALL=C. A new test sends what
the server sends from a bare environment, in three locales (#5495).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 14:50:29 -04:00
co-authored by Claude Opus 5.5
parent eadb08c347
commit ae66d83213
3 changed files with 49 additions and 3 deletions
+28
View File
@@ -21,6 +21,7 @@ guard here can fail (rule 167).
from __future__ import annotations
import json
import os
import re
import subprocess
from pathlib import Path
@@ -156,6 +157,33 @@ def test_a_value_survives_the_round_trip_the_hooks_actually_make(value):
assert got.rstrip("\n") == value.rstrip("\n")
ESCAPED = ["a · b", "an em dash — here", "a face 😀 here", "plain ascii"]
@pytest.mark.parametrize("locale", [None, "C", "C.UTF-8"])
@pytest.mark.parametrize("value", ESCAPED)
def test_an_escaped_character_decodes_to_utf8_in_any_locale(value, locale):
"""The server's JSON escapes every non-ASCII character (`\\u00b7`), and a
hook may run with no locale at all. The round trip above sends raw UTF-8
and never reaches the escape path; this one sends what the server sends,
from an environment with nothing but PATH."""
need_tools("bash", "awk")
env = {"PATH": os.environ["PATH"]}
if locale:
env["LC_ALL"] = locale
body = json.dumps(value)[1:-1]
r = subprocess.run(["bash", "-c", f'. "{DEFS}"; scribe_json_unescape'],
input=body.encode(), capture_output=True, env=env)
assert r.returncode == 0, r.stderr.decode()
assert r.stdout == (value + "\n").encode()
def test_the_utf8_guard_can_fail():
"""A lone byte where the encoded character belongs is what the old
decoder wrote; the comparison above has to tell them apart."""
assert "·\n".encode() != b"\xb7\n"
# --------------------------------------------------------------------------
# Reading: the shell side.