Skip to content

Instantly share code, notes, and snippets.

@wilsonowilson
Created January 18, 2026 12:20
Show Gist options
  • Select an option

  • Save wilsonowilson/a2b7d4a69189752289389a533d136171 to your computer and use it in GitHub Desktop.

Select an option

Save wilsonowilson/a2b7d4a69189752289389a533d136171 to your computer and use it in GitHub Desktop.
Get claude code to talk to you
#!/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