Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
},
"metadata": {
"description": "AI-operated audio notification system for Claude Code. 26 hooks, native matcher routing, TTS, webhook fan-out, status line, focus flow, rate-limit alerts.",
"version": "5.1.1"
"version": "5.1.2"
},
"plugins": [
{
"name": "audio-hooks",
"source": "./plugins/audio-hooks",
"version": "5.1.1",
"version": "5.1.2",
"description": "Audio notifications + AI-controlled config for every Claude Code event",
"author": {
"name": "Chan Meng",
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to Claude Code Audio Hooks will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [5.1.2] - 2026-04-20

Windows audio-playback fix. Every default clip >= ~3.0 s was silently truncated on Windows and WSL because all four PowerShell snippets in `play_audio_windows` and `play_audio_wsl` used a **fixed** `Start-Sleep -Seconds 3` (4 s for WSL) before calling `$player.Stop(); $player.Close()`. The bundled `audio/default/permission-request.mp3` is ~3.4 s — users heard the last ~0.4 s cut off every time Claude Code asked for permission. `elicitation.mp3` (~3.1 s) was also clipped; `subagent-start.mp3` and `notification-urgent.mp3` were on the edge.

### Fixed

- **`hooks/hook_runner.py` — `play_audio_windows` and `play_audio_wsl`** ([#14](https://github.com/ChanMeng666/claude-code-audio-hooks/issues/14), reported by [@Basdanucha](https://github.com/Basdanucha)). All four sites (PowerShell `-Command`, PowerShell `-File` heredoc, WMPlayer.OCX COM, WSL `-Command`) now poll the media player for the actual clip length and sleep for `duration + 500 ms` tail buffer before tearing down the player. For PresentationCore MediaPlayer, we poll `$player.NaturalDuration.HasTimeSpan` with a 1.5 s ceiling (Open() is async), then use `TotalMilliseconds`. For WMPlayer.OCX, we poll `$w.currentMedia.duration`. If the media never reports a duration (corrupt file, etc.), we fall back to `Start-Sleep -Seconds 10` — generous enough to cover any plausible default clip, still bounded so the PowerShell host process doesn't leak. The Python subprocess layer was already fire-and-forget (`subprocess.Popen`) — the fix closes the gap that was *inside* the PowerShell command string. macOS `afplay` and Linux `mpg123`/`ffplay`/`paplay`/`aplay` are unaffected: those players block until playback completes by default.

## [5.1.1] - 2026-04-18

Critical import-time crash fix plus regression-prevention CI. Everyone on v5.0.3 or v5.1.0 should upgrade.
Expand Down
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Claude Code Audio Hooks — AI Operator Guide

> **Version:** 5.1.1 | **Last Updated:** 2026-04-18
> **Version:** 5.1.2 | **Last Updated:** 2026-04-20

This document is the canonical machine-facing guide for the claude-code-audio-hooks project. It is written for **Claude Code (and other AI agents) as the operator** — not for humans. A human user says "install audio hooks for me" or "snooze audio for an hour" and you do everything via the project's structured CLI without any human-in-the-loop confirmation, menu navigation, or log file reading.

Expand Down Expand Up @@ -317,6 +317,7 @@ After editing any canonical file in `/hooks/`, `/bin/`, `/audio/`, or `/config/`

| Version | Date | Highlights |
|---|---|---|
| 5.1.2 | 2026-04-20 | **Fix:** Windows playback truncated clips > 3 s because every PowerShell snippet in `play_audio_windows` and `play_audio_wsl` used a fixed `Start-Sleep -Seconds 3` (the default `permission-request.mp3` is ~3.4 s — the last ~0.4 s was cut off; `elicitation.mp3` at ~3.1 s was also affected) ([#14](https://github.com/ChanMeng666/claude-code-audio-hooks/issues/14)). All four snippets now poll `NaturalDuration.HasTimeSpan` (or `currentMedia.duration` for WMPlayer.OCX) and sleep for the real clip length + 500 ms tail buffer, falling back to 10 s only if the media never reports a duration. |
| 5.1.1 | 2026-04-18 | **Critical fix:** `hook_runner.py` crashed on import with `NameError: name 'Tuple' is not defined`, blocking every `audio-hooks` subcommand on v5.0.3 and v5.1.0 ([#10](https://github.com/ChanMeng666/claude-code-audio-hooks/issues/10)). Adds CI import-smoke workflow (`.github/workflows/smoke.yml`) — 9-job matrix (Ubuntu/Windows/macOS × Python 3.9/3.12/3.13) plus plugin-in-sync check, runs on every push and PR. All version references realigned to `5.1.1` (v5.1.0 had shipped with in-tree strings still reading `5.0.3`). |
| 5.1.0 | 2026-04-13 | **Context window monitor.** Status line gains real-time context usage tracking with colour-coded thresholds (🟢 <50% safe, 🟡 50-80% should `/compact`, 🔴 >80% agent perf degrades). 10 customisable segments (`model`, `version`, `sounds`, `webhook`, `theme`, `snooze`, `focus`, `branch`, `api_quota`, `context`) controlled via `statusline_settings.visible_segments`. |
| 5.0.0 | 2026-04-11 | **AI-first redesign.** Single JSON `audio-hooks` CLI binary (27 subcommands). NDJSON structured logging with stable error codes. SKILL for natural-language project management. Plugin packaging (`/plugin install audio-hooks@chanmeng-audio-hooks`). Status line script with rate-limit bar. Four new hook events (PermissionDenied, CwdChanged, FileChanged, TaskCreated → 26 total). Native matcher routing via synthetic event names. New stdin field parsing (`last_assistant_message`, `worktree`, `agent`, `rate_limits`, `notification_type`, `error_type`, `source`). TTS speak_assistant_message. Rate-limit pre-check with marker debounce. Async webhook subprocess dispatch. Webhook payload schema `audio-hooks.webhook.v1`. All scripts auto-non-interactive on non-TTY. |
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ You type one slash command at install time. Then natural language forever.<br/>
26 hook events, 2 audio themes, rate-limit alerts, webhooks, TTS, context monitor — all operated by Claude Code on your behalf.

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Version](https://img.shields.io/badge/version-5.1.1-blue.svg)](https://github.com/ChanMeng666/claude-code-audio-hooks)
[![Version](https://img.shields.io/badge/version-5.1.2-blue.svg)](https://github.com/ChanMeng666/claude-code-audio-hooks)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-green.svg)](https://github.com/ChanMeng666/claude-code-audio-hooks)
[![Claude Code](https://img.shields.io/badge/Claude_Code-v2.1.80%2B-brightgreen.svg)](https://claude.ai/download)
[![Plugin](https://img.shields.io/badge/install-just_talk_to_Claude-purple.svg)](#-install-in-60-seconds)
Expand Down Expand Up @@ -286,7 +286,7 @@ sequenceDiagram
You->>CC: Show me the last 20 errors and clear the log.
CC-->>You: 2 errors found (WEBHOOK_TIMEOUT). Log cleared.
You->>CC: What version of audio-hooks am I running?
CC-->>You: v5.1.1, plugin install.
CC-->>You: v5.1.2, plugin install.
You->>CC: Please uninstall audio-hooks completely.
CC-->>You: Plugin uninstalled. All hooks removed.
end
Expand Down Expand Up @@ -505,7 +505,7 @@ Real-time context window and API quota bars — color-coded warnings before Clau
</p>

```text
[Opus] Audio Hooks v5.1.1 | 6/26 Sounds | Webhook: ntfy | Theme: Voice
[Opus] Audio Hooks v5.1.2 | 6/26 Sounds | Webhook: ntfy | Theme: Voice
[MUTED 23m] feat/audio-v5 API Quota: 78% Context: 65% /compact
```

Expand Down
2 changes: 1 addition & 1 deletion bin/audio-hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def require_project_root() -> int:
# Project state — version, install detection, hook catalogue
# ---------------------------------------------------------------------------

PROJECT_VERSION = "5.1.1"
PROJECT_VERSION = "5.1.2"

# Canonical hook catalogue. Order matches CLAUDE.md and the install scripts.
HOOK_CATALOG: List[Dict[str, Any]] = [
Expand Down
6 changes: 3 additions & 3 deletions config/default_preferences.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"$schema": "./user_preferences.schema.json",
"_comment": "Claude Code Audio Hooks - Default Configuration Template (v5.1.1)",
"_version": "5.1.1",
"_comment": "Claude Code Audio Hooks - Default Configuration Template (v5.1.2)",
"_version": "5.1.2",
"_description": "This file is the default template for user_preferences.json. Plugin installs auto-copy this to ${CLAUDE_PLUGIN_DATA}/user_preferences.json on first read. AI agents should NOT edit this file directly — use 'audio-hooks set <key> <value>' instead.",

"version": "5.1.1",
"version": "5.1.2",

"_comment_audio_theme": "Audio theme: 'default' for voice recordings, 'custom' for non-voice chimes. Change this one line to switch all audio.",
"audio_theme": "default",
Expand Down
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ Native matcher routing happens at the `settings.json` layer (Claude Code's match
Two-line bottom bar registered in `~/.claude/settings.json` via `audio-hooks statusline install`. Reads stdin JSON Claude Code provides (model name, session_id, workspace.git_worktree, rate_limits, context_window) and emits two lines of plain text with ANSI colors:

```text
[Opus] 🔊 Audio Hooks v5.1.1 | 6/26 Sounds | Webhook: ntfy | Theme: Voice
[Opus] 🔊 Audio Hooks v5.1.2 | 6/26 Sounds | Webhook: ntfy | Theme: Voice
[MUTED 23m] 🌿 feat/audio-v5 ████░░░░ API Quota: 78% █████░░░ Context: 65% ⚠️ /compact
```

Expand Down
48 changes: 39 additions & 9 deletions hooks/hook_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

# Version used for auto-sync: when the installed copy in ~/.claude/hooks/
# detects a newer version in the project directory, it self-updates.
HOOK_RUNNER_VERSION = "5.1.1"
HOOK_RUNNER_VERSION = "5.1.2"

# =============================================================================
# STRUCTURED LOGGING (NDJSON)
Expand Down Expand Up @@ -994,15 +994,23 @@ def play_audio_windows(audio_file: Path) -> bool:

log_debug(f"Windows audio playback: {win_path}")

# Method 1: Direct PowerShell command with MediaPlayer
# Method 1: Direct PowerShell command with MediaPlayer.
# MediaPlayer.Open() is async: we poll NaturalDuration.HasTimeSpan with a
# short ceiling, then sleep for the real clip length + a tail buffer so
# Stop()/Close() don't truncate playback (issue #14). Fallback to a
# generous-but-bounded sleep if Open() never resolves.
try:
ps_cmd = (
'Add-Type -AssemblyName presentationCore; '
'$p = New-Object System.Windows.Media.MediaPlayer; '
f'$p.Open("{win_path_escaped}"); '
'Start-Sleep -Milliseconds 500; '
'$deadline = (Get-Date).AddMilliseconds(1500); '
'while (-not $p.NaturalDuration.HasTimeSpan -and (Get-Date) -lt $deadline) '
'{ Start-Sleep -Milliseconds 50 }; '
'$p.Play(); '
'Start-Sleep -Seconds 3; '
'if ($p.NaturalDuration.HasTimeSpan) '
'{ Start-Sleep -Milliseconds ([int]($p.NaturalDuration.TimeSpan.TotalMilliseconds + 500)) } '
'else { Start-Sleep -Seconds 10 }; '
'$p.Stop(); $p.Close()'
)
proc = subprocess.Popen(
Expand All @@ -1027,9 +1035,16 @@ def play_audio_windows(audio_file: Path) -> bool:
Add-Type -AssemblyName presentationCore
$player = New-Object System.Windows.Media.MediaPlayer
$player.Open("{win_path_escaped}")
Start-Sleep -Milliseconds 500
$deadline = (Get-Date).AddMilliseconds(1500)
while (-not $player.NaturalDuration.HasTimeSpan -and (Get-Date) -lt $deadline) {{
Start-Sleep -Milliseconds 50
}}
$player.Play()
Start-Sleep -Seconds 3
if ($player.NaturalDuration.HasTimeSpan) {{
Start-Sleep -Milliseconds ([int]($player.NaturalDuration.TimeSpan.TotalMilliseconds + 500))
}} else {{
Start-Sleep -Seconds 10
}}
$player.Stop()
$player.Close()
Remove-Item -Path $MyInvocation.MyCommand.Path -Force -ErrorAction SilentlyContinue
Expand All @@ -1050,7 +1065,15 @@ def play_audio_windows(audio_file: Path) -> bool:

# Method 3: Use WMPlayer.OCX COM object
try:
ps_cmd = f'$w = New-Object -ComObject WMPlayer.OCX; $w.URL = "{win_path_escaped}"; Start-Sleep -Seconds 3'
ps_cmd = (
f'$w = New-Object -ComObject WMPlayer.OCX; $w.URL = "{win_path_escaped}"; '
'$deadline = (Get-Date).AddMilliseconds(1500); '
'while (($w.currentMedia -eq $null -or $w.currentMedia.duration -eq 0) '
'-and (Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 50 }; '
'if ($w.currentMedia -ne $null -and $w.currentMedia.duration -gt 0) '
'{ Start-Sleep -Milliseconds ([int]($w.currentMedia.duration * 1000 + 500)) } '
'else { Start-Sleep -Seconds 10 }'
)
proc = subprocess.Popen(
["powershell.exe", "-Command", ps_cmd],
stdout=subprocess.DEVNULL,
Expand Down Expand Up @@ -1208,9 +1231,16 @@ def play_audio_wsl(audio_file: Path) -> bool:
Add-Type -AssemblyName presentationCore
$player = New-Object System.Windows.Media.MediaPlayer
$player.Open("{win_path_escaped}")
Start-Sleep -Milliseconds 500
$deadline = (Get-Date).AddMilliseconds(1500)
while (-not $player.NaturalDuration.HasTimeSpan -and (Get-Date) -lt $deadline) {{
Start-Sleep -Milliseconds 50
}}
$player.Play()
Start-Sleep -Seconds 4
if ($player.NaturalDuration.HasTimeSpan) {{
Start-Sleep -Milliseconds ([int]($player.NaturalDuration.TimeSpan.TotalMilliseconds + 500))
}} else {{
Start-Sleep -Seconds 10
}}
$player.Stop()
$player.Close()
Remove-Item -Path "{win_path_escaped}" -ErrorAction SilentlyContinue
Expand Down
2 changes: 1 addition & 1 deletion plugins/audio-hooks/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "audio-hooks",
"version": "5.1.1",
"version": "5.1.2",
"description": "Audio notifications + AI-controlled config for every Claude Code event. 26 hooks, native matcher routing, TTS, webhooks, status line, focus flow, rate-limit alerts.",
"author": {
"name": "Chan Meng",
Expand Down
2 changes: 1 addition & 1 deletion plugins/audio-hooks/bin/audio-hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def require_project_root() -> int:
# Project state — version, install detection, hook catalogue
# ---------------------------------------------------------------------------

PROJECT_VERSION = "5.1.1"
PROJECT_VERSION = "5.1.2"

# Canonical hook catalogue. Order matches CLAUDE.md and the install scripts.
HOOK_CATALOG: List[Dict[str, Any]] = [
Expand Down
6 changes: 3 additions & 3 deletions plugins/audio-hooks/config/default_preferences.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"$schema": "./user_preferences.schema.json",
"_comment": "Claude Code Audio Hooks - Default Configuration Template (v5.1.1)",
"_version": "5.1.1",
"_comment": "Claude Code Audio Hooks - Default Configuration Template (v5.1.2)",
"_version": "5.1.2",
"_description": "This file is the default template for user_preferences.json. Plugin installs auto-copy this to ${CLAUDE_PLUGIN_DATA}/user_preferences.json on first read. AI agents should NOT edit this file directly — use 'audio-hooks set <key> <value>' instead.",

"version": "5.1.1",
"version": "5.1.2",

"_comment_audio_theme": "Audio theme: 'default' for voice recordings, 'custom' for non-voice chimes. Change this one line to switch all audio.",
"audio_theme": "default",
Expand Down
48 changes: 39 additions & 9 deletions plugins/audio-hooks/hooks/hook_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

# Version used for auto-sync: when the installed copy in ~/.claude/hooks/
# detects a newer version in the project directory, it self-updates.
HOOK_RUNNER_VERSION = "5.1.1"
HOOK_RUNNER_VERSION = "5.1.2"

# =============================================================================
# STRUCTURED LOGGING (NDJSON)
Expand Down Expand Up @@ -994,15 +994,23 @@ def play_audio_windows(audio_file: Path) -> bool:

log_debug(f"Windows audio playback: {win_path}")

# Method 1: Direct PowerShell command with MediaPlayer
# Method 1: Direct PowerShell command with MediaPlayer.
# MediaPlayer.Open() is async: we poll NaturalDuration.HasTimeSpan with a
# short ceiling, then sleep for the real clip length + a tail buffer so
# Stop()/Close() don't truncate playback (issue #14). Fallback to a
# generous-but-bounded sleep if Open() never resolves.
try:
ps_cmd = (
'Add-Type -AssemblyName presentationCore; '
'$p = New-Object System.Windows.Media.MediaPlayer; '
f'$p.Open("{win_path_escaped}"); '
'Start-Sleep -Milliseconds 500; '
'$deadline = (Get-Date).AddMilliseconds(1500); '
'while (-not $p.NaturalDuration.HasTimeSpan -and (Get-Date) -lt $deadline) '
'{ Start-Sleep -Milliseconds 50 }; '
'$p.Play(); '
'Start-Sleep -Seconds 3; '
'if ($p.NaturalDuration.HasTimeSpan) '
'{ Start-Sleep -Milliseconds ([int]($p.NaturalDuration.TimeSpan.TotalMilliseconds + 500)) } '
'else { Start-Sleep -Seconds 10 }; '
'$p.Stop(); $p.Close()'
)
proc = subprocess.Popen(
Expand All @@ -1027,9 +1035,16 @@ def play_audio_windows(audio_file: Path) -> bool:
Add-Type -AssemblyName presentationCore
$player = New-Object System.Windows.Media.MediaPlayer
$player.Open("{win_path_escaped}")
Start-Sleep -Milliseconds 500
$deadline = (Get-Date).AddMilliseconds(1500)
while (-not $player.NaturalDuration.HasTimeSpan -and (Get-Date) -lt $deadline) {{
Start-Sleep -Milliseconds 50
}}
$player.Play()
Start-Sleep -Seconds 3
if ($player.NaturalDuration.HasTimeSpan) {{
Start-Sleep -Milliseconds ([int]($player.NaturalDuration.TimeSpan.TotalMilliseconds + 500))
}} else {{
Start-Sleep -Seconds 10
}}
$player.Stop()
$player.Close()
Remove-Item -Path $MyInvocation.MyCommand.Path -Force -ErrorAction SilentlyContinue
Expand All @@ -1050,7 +1065,15 @@ def play_audio_windows(audio_file: Path) -> bool:

# Method 3: Use WMPlayer.OCX COM object
try:
ps_cmd = f'$w = New-Object -ComObject WMPlayer.OCX; $w.URL = "{win_path_escaped}"; Start-Sleep -Seconds 3'
ps_cmd = (
f'$w = New-Object -ComObject WMPlayer.OCX; $w.URL = "{win_path_escaped}"; '
'$deadline = (Get-Date).AddMilliseconds(1500); '
'while (($w.currentMedia -eq $null -or $w.currentMedia.duration -eq 0) '
'-and (Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 50 }; '
'if ($w.currentMedia -ne $null -and $w.currentMedia.duration -gt 0) '
'{ Start-Sleep -Milliseconds ([int]($w.currentMedia.duration * 1000 + 500)) } '
'else { Start-Sleep -Seconds 10 }'
)
proc = subprocess.Popen(
["powershell.exe", "-Command", ps_cmd],
stdout=subprocess.DEVNULL,
Expand Down Expand Up @@ -1208,9 +1231,16 @@ def play_audio_wsl(audio_file: Path) -> bool:
Add-Type -AssemblyName presentationCore
$player = New-Object System.Windows.Media.MediaPlayer
$player.Open("{win_path_escaped}")
Start-Sleep -Milliseconds 500
$deadline = (Get-Date).AddMilliseconds(1500)
while (-not $player.NaturalDuration.HasTimeSpan -and (Get-Date) -lt $deadline) {{
Start-Sleep -Milliseconds 50
}}
$player.Play()
Start-Sleep -Seconds 4
if ($player.NaturalDuration.HasTimeSpan) {{
Start-Sleep -Milliseconds ([int]($player.NaturalDuration.TimeSpan.TotalMilliseconds + 500))
}} else {{
Start-Sleep -Seconds 10
}}
$player.Stop()
$player.Close()
Remove-Item -Path "{win_path_escaped}" -ErrorAction SilentlyContinue
Expand Down
2 changes: 1 addition & 1 deletion plugins/audio-hooks/skills/audio-hooks/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ The status line displays real-time audio-hooks state and context window usage at

After installing, the status line updates every 60 seconds and shows two lines:
```
[Opus] 🔊 Audio Hooks v5.1.1 | 6/26 Sounds | Webhook: off | Theme: Voice
[Opus] 🔊 Audio Hooks v5.1.2 | 6/26 Sounds | Webhook: off | Theme: Voice
🌿 main ████░░░░ API Quota: 60% █████░░░ Context: 65% ⚠️ /compact
```

Expand Down
Loading