Last active
May 25, 2026 09:15
-
-
Save stormychel/a2b6a437873b62b4e9750186babcc658 to your computer and use it in GitHub Desktop.
[Windows] Claude Code Stop hook — announces 'Claude is done' via SAPI Microsoft David Desktop voice at rate +2 (PowerShell System.Speech.Synthesis). macOS sibling: https://gist.github.com/stormychel/502e2a6da38bf5798750e5565a5324f7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Install a Claude Code "Stop" hook that announces completion out loud on Windows | |
| using SAPI (System.Speech) with the Microsoft David Desktop voice at rate +2 — | |
| i.e. it runs the equivalent of `say -v Zarvox "Claude is done"` from the macOS | |
| sibling gist (https://gist.github.com/stormychel/502e2a6da38bf5798750e5565a5324f7). | |
| System-wide: writes to %USERPROFILE%\.claude\settings.json (merges, preserves | |
| existing keys). Idempotent: re-running won't add a duplicate. | |
| The hook runs asynchronously (`async: true`) so Claude doesn't block waiting for | |
| the speech to finish. | |
| Usage: | |
| python install-claude-david-hook.py | |
| Disable later: remove the entry from the "hooks" -> "Stop" array in settings.json, | |
| or use the Claude Code `/hooks` UI. | |
| Change the phrase/voice: edit CMD below. To list installed SAPI voices, run in | |
| PowerShell: | |
| Add-Type -AssemblyName System.Speech | |
| (New-Object System.Speech.Synthesis.SpeechSynthesizer).GetInstalledVoices() | | |
| ForEach-Object { $_.VoiceInfo.Name } | |
| Requires: | |
| - Windows with PowerShell (any modern version) | |
| - Microsoft David Desktop voice (default on Windows 10/11). Falls back to the | |
| system default voice if David is not installed. | |
| """ | |
| import json | |
| import pathlib | |
| import subprocess | |
| PHRASE = "Claude is done" | |
| PREFERRED_VOICE = "Microsoft David Desktop" | |
| RATE = 2 # -10..10; +2 is the closest "robotic" feel from a default voice | |
| def voice_installed(name: str) -> bool: | |
| ps = ( | |
| "Add-Type -AssemblyName System.Speech; " | |
| "(New-Object System.Speech.Synthesis.SpeechSynthesizer)" | |
| ".GetInstalledVoices() | ForEach-Object { $_.VoiceInfo.Name }" | |
| ) | |
| try: | |
| out = subprocess.run( | |
| ["powershell", "-NoProfile", "-Command", ps], | |
| capture_output=True, text=True, timeout=15, check=True, | |
| ).stdout | |
| except Exception: | |
| return False | |
| return any(name == line.strip() for line in out.splitlines()) | |
| def build_command() -> str: | |
| if voice_installed(PREFERRED_VOICE): | |
| return ( | |
| "Add-Type -AssemblyName System.Speech; " | |
| "$s = New-Object System.Speech.Synthesis.SpeechSynthesizer; " | |
| f"$s.SelectVoice('{PREFERRED_VOICE}'); " | |
| f"$s.Rate = {RATE}; " | |
| f"$s.Speak('{PHRASE}')" | |
| ) | |
| # Fallback: no voice selection, system default | |
| return ( | |
| "Add-Type -AssemblyName System.Speech; " | |
| f"(New-Object System.Speech.Synthesis.SpeechSynthesizer).Speak('{PHRASE}')" | |
| ) | |
| def main() -> None: | |
| cmd = build_command() | |
| settings = pathlib.Path.home() / ".claude" / "settings.json" | |
| cfg = json.loads(settings.read_text()) if settings.exists() else {} | |
| stop = cfg.setdefault("hooks", {}).setdefault("Stop", []) | |
| already = any( | |
| hook.get("command") == cmd | |
| for entry in stop | |
| for hook in entry.get("hooks", []) | |
| ) | |
| if not already: | |
| stop.append({ | |
| "hooks": [{ | |
| "type": "command", | |
| "shell": "powershell", | |
| "async": True, | |
| "command": cmd, | |
| }] | |
| }) | |
| settings.parent.mkdir(parents=True, exist_ok=True) | |
| settings.write_text(json.dumps(cfg, indent=2)) | |
| state = "already present" if already else "installed" | |
| print(f"OK: David Stop hook {state} at {settings}") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment