Skip to content

Instantly share code, notes, and snippets.

@drmingdrmer
Created February 21, 2026 15:50
Show Gist options
  • Select an option

  • Save drmingdrmer/e54e533613f716c2ad48cd398b1bd19a to your computer and use it in GitHub Desktop.

Select an option

Save drmingdrmer/e54e533613f716c2ad48cd398b1bd19a to your computer and use it in GitHub Desktop.
Use this script to let claude speak to report when a task is done.
#!/usr/bin/env python3
"""Stop hook: speak a short summary of the last assistant response using `say`.
Reads the Claude Code transcript and speaks the first ~120 characters of the
last assistant message when Claude finishes responding.
Add to ~/.claude/settings.json:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/stop-say.py",
"timeout": 30
}
]
}
]
}
}
To use a specific voice, pass -v to say(1), e.g.:
subprocess.run(["say", "-v", "Siri", message], check=False)
List available voices with: say -v '?'
"""
import json
import os
import subprocess
import sys
def extract_last_assistant_text(transcript_path: str) -> str | None:
"""Return the text of the last assistant message in the transcript."""
if not os.path.exists(transcript_path):
return None
last_text = None
try:
with open(transcript_path) as f:
for raw in f:
raw = raw.strip()
if not raw:
continue
try:
entry = json.loads(raw)
except json.JSONDecodeError:
continue
# Transcript entries use top-level "type": "assistant",
# with actual message content nested under entry["message"]
if entry.get("type") != "assistant":
continue
content = entry.get("message", {}).get("content", [])
if isinstance(content, list):
for block in content:
if isinstance(block, dict) and block.get("type") == "text":
last_text = block.get("text", "")
break
elif isinstance(content, str):
last_text = content
except OSError:
pass
return last_text
def main() -> None:
data = json.load(sys.stdin)
# stop_hook_active is true when Claude is already continuing due to a Stop
# hook — skip to prevent an infinite loop.
if data.get("stop_hook_active"):
sys.exit(0)
transcript_path = data.get("transcript_path", "").replace("~", os.path.expanduser("~"))
last_text = extract_last_assistant_text(transcript_path)
if last_text:
summary = " ".join(last_text.split())[:120]
message = f"Claude finished. {summary}"
else:
message = "Claude has finished the task."
subprocess.run(["say", message], check=False)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment