Created
January 18, 2026 12:20
-
-
Save wilsonowilson/a2b7d4a69189752289389a533d136171 to your computer and use it in GitHub Desktop.
Get claude code to talk to you
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 | |
| """ | |
| Dynamic notification hook for Claude Code. | |
| Reads the transcript and generates a contextual spoken message. | |
| """ | |
| import json | |
| import re | |
| import subprocess | |
| import sys | |
| from pathlib import Path | |
| # Debug: log what we receive | |
| DEBUG_LOG = Path.home() / ".claude-hook-debug.json" | |
| def sanitize_for_speech(text: str) -> str: | |
| """Clean text for natural speech - handle file extensions, paths, etc.""" | |
| # Replace common file extensions with speakable versions | |
| text = re.sub(r'\.([a-zA-Z]{1,4})(?=[\s,\)\]\}]|$)', r' dot \1', text) | |
| # Replace remaining dots between words (like package.json) | |
| text = re.sub(r'(?<=[a-zA-Z])\.(?=[a-zA-Z])', ' dot ', text) | |
| # Clean up paths | |
| text = text.replace('/', ' slash ') | |
| # Remove any code-like characters that sound weird | |
| text = re.sub(r'[{}\[\]`"\'<>]', '', text) | |
| return text | |
| def get_last_action(transcript_path: str) -> str: | |
| """Extract what Claude last did from the transcript.""" | |
| try: | |
| with open(transcript_path, "r") as f: | |
| lines = f.readlines() | |
| # Parse JSONL - look for recent tool uses and messages | |
| tools_used = [] | |
| last_assistant_text = "" | |
| for line in reversed(lines[-50:]): # Check last 50 entries | |
| try: | |
| entry = json.loads(line.strip()) | |
| msg_type = entry.get("type") | |
| if msg_type == "assistant": | |
| # Get the text content | |
| message = entry.get("message", {}) | |
| content = message.get("content", []) | |
| for block in content: | |
| if isinstance(block, dict) and block.get("type") == "text": | |
| text = block.get("text", "") | |
| if text and not last_assistant_text: | |
| # Get first sentence as summary | |
| last_assistant_text = text.split(".")[0][:100] | |
| if isinstance(block, dict) and block.get("type") == "tool_use": | |
| tool_name = block.get("name", "") | |
| if tool_name and tool_name not in tools_used: | |
| tools_used.append(tool_name) | |
| if last_assistant_text and tools_used: | |
| break | |
| except json.JSONDecodeError: | |
| continue | |
| # Generate message based on what we found | |
| if tools_used: | |
| tool_summary = { | |
| "Edit": "edited files", | |
| "Write": "created files", | |
| "Read": "reviewed code", | |
| "Bash": "ran commands", | |
| "Grep": "searched code", | |
| "Glob": "found files", | |
| "Task": "completed subtasks", | |
| } | |
| actions = [tool_summary.get(t, t) for t in tools_used[:3]] | |
| return f"Done. {', '.join(actions)}." | |
| if last_assistant_text: | |
| return last_assistant_text | |
| return "Task complete" | |
| except Exception: | |
| return "Done" | |
| def main(): | |
| try: | |
| input_data = json.load(sys.stdin) | |
| except json.JSONDecodeError: | |
| input_data = {} | |
| # Debug: save what we receive | |
| with open(DEBUG_LOG, "w") as f: | |
| json.dump(input_data, f, indent=2) | |
| event = input_data.get("hook_event_name", "") | |
| transcript_path = input_data.get("transcript_path", "") | |
| # Check if Claude provided a stop message | |
| stop_reason = input_data.get("stop_reason") or input_data.get("stopReason") or input_data.get("message") | |
| if event == "Stop": | |
| if stop_reason: | |
| message = stop_reason | |
| elif transcript_path: | |
| message = get_last_action(transcript_path) | |
| else: | |
| message = "Done" | |
| subprocess.Popen(["say", message, "-v", "Fred"]) | |
| elif event == "Notification": | |
| subprocess.Popen(["say", "Waiting for input", "-v", "Fred"]) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment