When running Claude Code inside WSL, it’s easy to miss when it’s waiting for your input — especially if you’ve switched to another window while it works. This post documents the notification system I set up: Windows toast notifications that fire when Claude stops and when it needs permission, with click-to-focus on the exact WezTerm pane.
How It Works#
Claude Code has a hooks system in ~/.claude/settings.json. Two events are useful here:
Stop— fires when Claude finishes a response and is waiting for input. The hook payload includeslast_assistant_message,cwd, andtranscript_path.PermissionRequest— fires when Claude needs approval to run a tool (Bash command, file write, etc.). The payload includestool_nameandtool_input.
Both hooks run shell commands asynchronously (async: true) so they never block Claude.
| |
The Scripts#
~/.claude/notify-stop.sh#
Extracts the title, message body, and cwd from the hook’s stdin JSON, then launches a PowerShell notification:
| |
~/.claude/notify-permission.sh#
Same structure, but builds the body from the tool name and its key argument:
| |
This gives notifications like Bash: git status or Edit: src/main.go.
~/.claude/notify-stop.ps1#
The PowerShell script does four things:
- Skip if already focused — check if the target WezTerm window is already the foreground window; if so, exit silently.
- Show balloon notification — Windows system tray balloon with title and body.
- On click: find the right pane — query
wezterm.exe cli list --format json, filter bycwd, get the pane ID and window title. - Activate and raise the window —
activate-paneswitches to the right tab, then Win32 APIs bring the window to front.
The Hard Parts#
Environment variables don’t cross WSL→PowerShell#
You can’t set a bash variable and read it with $Env:VAR in PowerShell. The fix: write a JSON file from bash (using jq -n) and read it with ConvertFrom-Json in PowerShell.
Finding the right WezTerm window#
wezterm cli list --format json returns each pane’s cwd as a file:// URI (e.g. file://pc/home/huyang/workdir). Matching against the hook’s cwd (/home/huyang/workdir) uses EndsWith:
| |
We do this lookup at click time, not at notification time, to get the freshest window title.
Window title encoding mismatch#
WezTerm prefixes the active pane title with ⠂ (a braille dot, U+2802) while Claude is working. When piped through PowerShell’s ConvertFrom-Json, this arrives as Γ£│ (UTF-8 bytes misread as cp1252). Win32 GetWindowText returns ? for the same character.
The fix: strip all leading non-ASCII and non-alphanumeric characters from both strings before comparing:
| |
SetForegroundWindow is blocked by Windows#
Windows blocks foreground-window stealing from background processes. keybd_event(Alt) tricks help in some cases, but the reliable fix is AttachThreadInput — attach your thread to the current foreground thread before calling SetForegroundWindow:
| |
Result#
- Claude finishes a response → Windows notification showing the last message and which project it’s from
- Claude needs permission → notification showing the exact command/file
- Clicking either notification focuses the correct WezTerm window and pane
- No notification if you’re already looking at that window