TUI freezes (keyboard dead, process alive) when SessionStart hook systemMessage contains block characters U+2588/U+2591 - Windows

Status Open
Reported on v2.1.198
Maintainer reply None cached
Activity 0 comments · opened Jul 16, 2026

Environment

  • Claude Code: 2.1.198
  • OS: Windows 11 Home (build 26200)
  • Terminal: VS Code integrated terminal (xterm.js + ConPTY) — not yet tested in standalone Windows Terminal
  • Hook runtime: Windows PowerShell 5.1 (powershell -NoProfile -File ...)

Summary

A SessionStart hook that emits systemMessage containing the block characters (U+2588) or (U+2591) freezes the TUI right after startup: the message may render, but the keyboard goes completely dead while the claude.exe process stays alive. The hook itself finishes normally (verified via a marker file the hook writes as its last step — the marker is always present, so the hang happens on the TUI side after consuming the hook output).

The same systemMessage with those two characters replaced by ASCII (# / -) works flawlessly. Everything else — Thai text, emoji (including surrogate pairs), multi-line content — is fine.

Steps to reproduce

  1. On Windows, add a SessionStart hook that outputs JSON with a systemMessage containing block characters, e.g. a progress bar:

``json
{
"systemMessage": "projects:\n ██████░░░░ 63.5% SmartBoiler\n ███░░░░░░░ 39.6% renovation",
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "test"
}
}
``

(In our setup this is produced by a PowerShell script writing UTF-8 (no BOM) to raw stdout via StreamWriter over [Console]::OpenStandardOutput(), single-line ConvertTo-Json -Compress.)

  1. Start claude interactively in VS Code's integrated terminal.
  2. After the systemMessage appears, try typing.

Expected: normal input.
Actual: keyboard input is dead; the session never recovers; the process keeps running. (Reproduced 2/2 on our machine; historically it looked intermittent, which sent us down the wrong path for a while.)

Isolation evidence (content bisect)

We ran a ladder of systemMessage payloads through the same hook, several fresh-terminal rounds each, and judged pass/fail by keyboard responsiveness:

| Payload | Differs from real payload by | Result |
|---|---|---|
| Single-line plain ASCII | everything | ✅ pass 4/4 |
| 8-line ASCII | isolates multi-line/length | ✅ pass 4/4 |
| Single-line Thai | isolates non-ASCII/wide chars | ✅ pass 3/3 |
| Real payload with █░#- | only the block chars | ✅ pass 5/5 |
| Real payload (contains █░, plus Thai + emoji) | — | 💥 freeze 2/2 |

The only variable separating the passing and freezing arms is U+2588/U+2591. Notably, our project-mode payloads (Thai + emoji /🔄, no block chars) never froze — which originally masqueraded as a mode-dependent bug.

Possibly-related observation

After force-killing a frozen session, the leftover terminal shows the TUI had unrestored state: focus-reporting stays on (ESC[I/ESC[O appear on every focus change) and what looks like a pending DSR-style reply (ESC[9999999997;1n) arrives as input. Speculating: the renderer may issue a terminal query while measuring/laying out the ambiguous-width block characters and block forever waiting for the reply. Take with salt — we only see the outside of this.

Workaround

Sanitize the message before emitting from the hook: #, -. With that in place, systemMessage works reliably in both our project and workspace hooks.

---

Thanks for Claude Code — the hook system is genuinely great, which is exactly why we keep stuffing dashboards into it. 🙏

P.S. Filing this cost us an afternoon of very scientific "open terminal, watch it freeze, close terminal" — roughly 16 test sessions on a Max subscription. If bug bounties are ever paid out in tokens, consider this a humble application. 😄 (Kidding. Mostly.)

View original on GitHub ↗