Adding Text-to-Speech to Claude Code Notifications with Kokoro and Hooks

Adding Text-to-Speech to Claude Code Notifications with Kokoro and Hooks
Photo by Priscilla Du Preez 🇨🇦 / Unsplash

Ever wished your AI coding assistant could literally speak to you? Today we'll walk through setting up Claude Code to announce its notifications using text-to-speech, making your coding workflow more accessible and hands-free.

What We're Building

Claude Code has a powerful feature called "hooks" that let you run custom commands at specific points in its workflow. We'll use this to:

  1. Catch notification events from Claude Code
  2. Send the notification text to Kokoro (a high-quality TTS engine)
  3. Play the audio through your speakers

The result? Instead of just seeing notifications, you'll hear Claude Code announce things like "Task completed successfully" or "Waiting for your permission to run this command."

Prerequisites

Before we start, make sure you have:

  • Claude Code installed and working
  • Kokoro-FastAPI running locally (GitHub repo)
  • uv package manager installed (installation guide)
  • A MacBook Pro (though this works on other systems with minor tweaks)

Step 1: Set Up Your Python Environment

We'll use uv to manage our Python dependencies. This is cleaner than installing packages globally and avoids version conflicts.

In your home directory, create a new Python project:

cd ~
uv init claude-tts-hooks
cd claude-tts-hooks

Add the required packages:

uv add openai pyaudio

This creates a virtual environment and installs:

  • openai: For communicating with Kokoro-FastAPI (which uses OpenAI-compatible endpoints)
  • pyaudio: For playing audio directly to your speakers

Step 2: Create the Hook Script

Create a file called kokoro_tts_hook.py in your project directory:

#!/usr/bin/env python3
"""
Claude Code Notification Hook for Kokoro TTS
Sends notification text to Kokoro-FastAPI for text-to-speech conversion
"""

import json
import sys
import os
import tempfile
from pathlib import Path
try:
    from openai import OpenAI
    import pyaudio
except ImportError as e:
    print(f"Missing required packages: {e}", file=sys.stderr)
    print("Install with: uv add openai pyaudio", file=sys.stderr)
    sys.exit(1)

# Configuration - Customize these for your setup
KOKORO_BASE_URL = "http://localhost:8880/v1"
KOKORO_VOICE = "af_bella"  # Try: af_bella, af_sarah, am_adam, etc.
AUDIO_OUTPUT_METHOD = "speakers"  # "speakers" or "file"
AUDIO_OUTPUT_DIR = os.path.expanduser("~/claude_notifications")

def play_to_speakers(client, text):
    """Stream audio directly to speakers using PyAudio"""
    try:
        # Set up audio output
        player = pyaudio.PyAudio().open(
            format=pyaudio.paInt16,
            channels=1,
            rate=24000,
            output=True
        )
        
        # Stream TTS audio in real-time
        with client.audio.speech.with_streaming_response.create(
            model="kokoro",
            voice=KOKORO_VOICE,
            response_format="pcm",  # Raw audio format
            input=text
        ) as response:
            # Play audio chunks as they arrive
            for chunk in response.iter_bytes(chunk_size=1024):
                player.write(chunk)
        
        player.stop_stream()
        player.close()
        return True
        
    except Exception as e:
        print(f"Error playing to speakers: {e}", file=sys.stderr)
        return False

def clean_notification_text(text):
    """Clean and prepare notification text for better TTS"""
    import re
    
    # Remove markdown formatting
    text = re.sub(r'[*_`]', '', text)
    
    # Keep it reasonable length
    max_length = 200
    if len(text) > max_length:
        text = text[:max_length] + "..."
    
    # Make tech terms sound better
    replacements = {
        "Claude Code": "Claude Code",
        "TTS": "text to speech",
        "API": "A P I",
        "JSON": "jay-son",
        "HTTP": "H T T P"
    }
    
    for old, new in replacements.items():
        text = text.replace(old, new)
    
    return text.strip()

def main():
    try:
        # Read the notification data from Claude Code
        input_data = json.load(sys.stdin)
    except json.JSONDecodeError as e:
        print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
        sys.exit(1)

    # Extract the notification message
    message = input_data.get("message", "")
    title = input_data.get("title", "Claude Code")
    
    if not message:
        sys.exit(1)  # Nothing to say
    
    # Prepare the text for TTS
    if title and title != "Claude Code":
        tts_text = f"{title}: {message}"
    else:
        tts_text = message
    
    tts_text = clean_notification_text(tts_text)
    
    # Connect to Kokoro
    try:
        client = OpenAI(
            base_url=KOKORO_BASE_URL,
            api_key="not-needed"  # Kokoro doesn't require real auth
        )
    except Exception as e:
        print(f"Error connecting to Kokoro: {e}", file=sys.stderr)
        sys.exit(1)
    
    # Generate and play the speech
    success = play_to_speakers(client, tts_text)
    
    if success:
        # Tell Claude Code we succeeded and don't clutter the transcript
        output = {
            "continue": True,
            "suppressOutput": True
        }
        print(json.dumps(output))
        sys.exit(0)
    else:
        sys.exit(1)

if __name__ == "__main__":
    main()

Make the script executable:

chmod +x kokoro_tts_hook.py

Step 3: Configure Claude Code Hooks

Now we need to tell Claude Code to run our script when notifications happen. There are two ways to do this:

  1. Open Claude Code and run the /hooks command
  2. Select the Notification event
  3. For the matcher, leave it empty (this catches all notifications)
  4. Choose "User settings" to apply this to all your projects
  5. Press Esc to save and exit

Add a new hook with this command:

uv run /Users/yourusername/claude-tts-hooks/kokoro_tts_hook.py

(Replace yourusername with your actual username)

Option B: Manual Configuration

Edit your ~/.claude/settings.json file and add this configuration:

{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "uv run /Users/yourusername/claude-tts-hooks/kokoro_tts_hook.py"
          }
        ]
      }
    ]
  }
}

Step 4: Start Kokoro-FastAPI

Make sure your Kokoro-FastAPI server is running. If you haven't set it up yet, follow the instructions in the Kokoro-FastAPI repository.

Typically, you'll start it with:

python main.py

The server should be accessible at http://localhost:8880.

Step 5: Test Your Setup

Let's test if everything works:

  1. Open Claude Code in a new project
  2. When Claude asks for permission, you should hear the notification spoken aloud!

Ask Claude to run a command that requires permission, like:

Create a simple Python script that prints "Hello World"

Customization Options

Changing Voices

Kokoro supports different voices. Edit the KOKORO_VOICE variable in your script:

KOKORO_VOICE = "af_sarah"  # Try: af_bella, af_sarah, am_adam, etc.

Saving Audio Files Instead

If you prefer to save notifications as audio files rather than playing them immediately:

AUDIO_OUTPUT_METHOD = "file"
AUDIO_OUTPUT_DIR = os.path.expanduser("~/Desktop/claude_audio")

Adjusting the Kokoro Server URL

If your Kokoro server runs on a different port or machine:

KOKORO_BASE_URL = "http://localhost:8881/v1"  # Different port
# or
KOKORO_BASE_URL = "http://192.168.1.100:8880/v1"  # Different machine

How It Works

Here's what happens behind the scenes:

  1. Claude Code generates a notification (like "Task completed" or "Permission required")
  2. The hook system activates and runs our Python script
  3. Our script receives notification data as JSON via stdin
  4. Text gets cleaned and prepared for better speech synthesis
  5. The script calls Kokoro-FastAPI using the OpenAI-compatible interface
  6. Audio streams directly to your speakers in real-time

The beauty of using hooks is that this happens automatically—no manual intervention required.

Troubleshooting

"Module not found" errors

Make sure you're in the right directory and have run uv add openai pyaudio. The uv run command should handle the virtual environment automatically.

No audio output

  • Check that Kokoro-FastAPI is running and accessible
  • Verify your system audio settings
  • Try changing the voice or audio format

Hook not triggering

  • Run /hooks in Claude Code to verify your configuration
  • Check that the script path is correct and absolute
  • Look for error messages in Claude Code's transcript mode (Ctrl-R)

PyAudio installation issues on macOS

If PyAudio fails to install, you might need:

brew install portaudio

Why This Matters

Adding TTS to Claude Code notifications creates a more accessible and multitasking-friendly development environment. You can:

  • Stay focused on your code while knowing what Claude is doing
  • Work hands-free during certain workflows
  • Catch important notifications even when not looking at the screen
  • Improve accessibility for visually impaired developers

Going Further

This is just the beginning! You could extend this setup to:

  • Filter notifications by type (only speak errors, not successes)
  • Use different voices for different events (error voice vs. success voice)
  • Add sound effects before or after the speech
  • Integrate with other TTS engines like ElevenLabs or Azure Speech
  • Create hooks for other events like file saves or command completions

The hook system in Claude Code is incredibly powerful, and TTS integration is just one example of how you can customize your AI-assisted development workflow.

Conclusion

By combining Claude Code's hooks, Kokoro's high-quality TTS, and uv's clean dependency management, we've created a seamless audio notification system that makes AI-assisted coding more accessible and engaging.

The setup might seem complex at first, but once it's working, you'll wonder how you ever coded without your AI assistant speaking to you. Give it a try and let the future of voice-enabled development begin!


Have questions or improvements? The Claude Code hooks system is incredibly flexible, and there are many ways to customize this setup for your specific workflow.