Adding a New Event Hook
Relevant source files
The following files were used as context for generating this wiki page:Introduction
The voice-elevenlabs plugin utilizes a hook system to automatically trigger voice notifications for specific events within Claude Code. This allows for hands-free updates, such as announcing when a task is complete. The system is defined by a central JSON configuration file that maps events to executable commands, typically shell scripts.
This document outlines the architecture of the hook system and provides a step-by-step guide on how to define a new event hook, implement its corresponding script, and trigger a voice notification. The existing hooks for Stop, SubagentStop, and Notification events serve as the primary examples.
Sources: README.md:82-87, hooks/hooks.json
Hook System Architecture
The hook system is composed of two main parts: a JSON definition file and a directory of executable scripts. When an event occurs in Claude Code (e.g., a command finishes), it checks hooks.json for a matching event definition. If found, it executes the specified command, passing event context as a JSON payload via standard input.
The following diagram illustrates the data flow from event trigger to audio output.
This diagram shows the end-to-end process of a hook being triggered and resulting in a spoken notification.
Sources: hooks/hooks.json, hooks/scripts/stop-voice.sh, hooks/scripts/subagent-voice.sh
Existing Hooks
The plugin comes with three pre-configured hooks for common events.
| Event | Command | Description |
|---|---|---|
Stop | ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/stop-voice.sh | Announce '[project]: done' when Claude finishes. |
SubagentStop | ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/subagent-voice.sh | Announce '[project]: [task]' when a subagent completes. |
Notification | claude-voice-notifications | Announce when Claude needs permission or input. |
Sources: hooks/hooks.json:2-16
1. Defining the Hook in hooks.json
To add a new hook, you must first add a new JSON object to the hooks array in hooks/hooks.json. Each object defines the event to listen for and the command to execute.
Hook Definition Fields
| Field | Type | Description |
|---|---|---|
event | String | The name of the Claude Code event to trigger on (e.g., Stop, SubagentStop). |
type | String | The type of action to perform. Currently, this is always "command". |
command | String | The command or script to execute. The ${CLAUDE_PLUGIN_ROOT} variable can be used for portability. |
description | String | A brief explanation of what the hook does. |
Sources: hooks/hooks.json:3-7
Example Definition
Here is the definition for the SubagentStop event.
{
"event": "SubagentStop",
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/subagent-voice.sh",
"description": "Announce '[project]: [task]' when subagent completes"
}
Sources: hooks/hooks.json:8-12
2. Implementing the Hook Script
The command specified in hooks.json is typically a shell script that receives a JSON payload via standard input. The script's responsibility is to parse this input, construct a notification message, and send it to the claude-voice-notifications utility.
Input Processing
The script reads from standard input to get the event payload. This payload is a JSON object containing context like the current working directory (cwd) and the path to the conversation transcript (transcript_path).
The scripts use jq to parse this JSON. For example, to get the cwd:
input=$(cat)
cwd=$(echo "$input" | jq -r '.cwd // empty')
Sources: hooks/scripts/stop-voice.sh:5-6, hooks/scripts/subagent-voice.sh:5-6
The subagent-voice.sh script demonstrates a more complex parsing task where it extracts the task description from the transcript file.
description=$(jq -rs '
[.[] | select(.type == "tool_use" and .name == "Task")] |
last |
.input.description // empty
' "$transcript_path" 2>/dev/null)
Sources: hooks/scripts/subagent-voice.sh:16-21
Message Construction
After parsing the necessary information, the script constructs a concise message string. The scripts use logic to create a meaningful message based on the available context (e.g., project name, task description).
# Example from stop-voice.sh
if [ -n "$project" ]; then
message="$project: done"
else
message="Claude done"
fi
Sources: hooks/scripts/stop-voice.sh:15-18
3. Triggering the Voice Notification
The final step in the script is to send the constructed message to the claude-voice-notifications command. This is done by piping a specific JSON payload to the command.
The following sequence diagram shows the interaction between the components.
This diagram clarifies the handoff from the custom hook script to the voice notification utility.
Notification Payload
The payload sent to claude-voice-notifications is a JSON object containing the original event name and the message to be spoken.
echo "{\"hook_event_name\": \"Stop\", \"message\": \"$message\"}" | claude-voice-notifications
This format is consistent across the hook scripts and is also used by the /speak command, indicating it's the standard way to trigger voice output through this utility.
Sources: hooks/scripts/stop-voice.sh:21, hooks/scripts/subagent-voice.sh:41, commands/speak.md:19-21
Summary
Adding a new event hook is a two-part process. First, a new entry must be added to the hooks.json array, mapping a Claude Code event name to a command. Second, an executable script must be created at the specified command path. This script is responsible for parsing event data from standard input, constructing a relevant notification message, and piping it in the correct JSON format to the claude-voice-notifications utility to be spoken aloud. By following the patterns in existing hooks like Stop and SubagentStop, developers can extend the plugin to provide audible feedback for any custom event.