diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index 8eee1bd..9123fd7 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -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",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0e192e1..8ddc03d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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.
diff --git a/CLAUDE.md b/CLAUDE.md
index c9ede9d..71b0af6 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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.
@@ -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. |
diff --git a/README.md b/README.md
index c573fff..a443339 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ You type one slash command at install time. Then natural language forever.
26 hook events, 2 audio themes, rate-limit alerts, webhooks, TTS, context monitor — all operated by Claude Code on your behalf.
[](https://opensource.org/licenses/MIT)
-[](https://github.com/ChanMeng666/claude-code-audio-hooks)
+[](https://github.com/ChanMeng666/claude-code-audio-hooks)
[](https://github.com/ChanMeng666/claude-code-audio-hooks)
[](https://claude.ai/download)
[](#-install-in-60-seconds)
@@ -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
@@ -505,7 +505,7 @@ Real-time context window and API quota bars — color-coded warnings before Clau
```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
```
diff --git a/bin/audio-hooks.py b/bin/audio-hooks.py
index 0f057e6..eacb75b 100644
--- a/bin/audio-hooks.py
+++ b/bin/audio-hooks.py
@@ -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]] = [
diff --git a/config/default_preferences.json b/config/default_preferences.json
index 86edfc4..d008325 100644
--- a/config/default_preferences.json
+++ b/config/default_preferences.json
@@ -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 ' 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",
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 2b719fc..d79ac65 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -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
```
diff --git a/hooks/hook_runner.py b/hooks/hook_runner.py
index c03aaf4..037b32d 100644
--- a/hooks/hook_runner.py
+++ b/hooks/hook_runner.py
@@ -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)
@@ -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(
@@ -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
@@ -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,
@@ -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
diff --git a/plugins/audio-hooks/.claude-plugin/plugin.json b/plugins/audio-hooks/.claude-plugin/plugin.json
index 4a9e93e..02567c3 100644
--- a/plugins/audio-hooks/.claude-plugin/plugin.json
+++ b/plugins/audio-hooks/.claude-plugin/plugin.json
@@ -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",
diff --git a/plugins/audio-hooks/bin/audio-hooks.py b/plugins/audio-hooks/bin/audio-hooks.py
index 0f057e6..eacb75b 100644
--- a/plugins/audio-hooks/bin/audio-hooks.py
+++ b/plugins/audio-hooks/bin/audio-hooks.py
@@ -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]] = [
diff --git a/plugins/audio-hooks/config/default_preferences.json b/plugins/audio-hooks/config/default_preferences.json
index 86edfc4..d008325 100644
--- a/plugins/audio-hooks/config/default_preferences.json
+++ b/plugins/audio-hooks/config/default_preferences.json
@@ -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 ' 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",
diff --git a/plugins/audio-hooks/hooks/hook_runner.py b/plugins/audio-hooks/hooks/hook_runner.py
index c03aaf4..037b32d 100644
--- a/plugins/audio-hooks/hooks/hook_runner.py
+++ b/plugins/audio-hooks/hooks/hook_runner.py
@@ -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)
@@ -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(
@@ -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
@@ -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,
@@ -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
diff --git a/plugins/audio-hooks/skills/audio-hooks/SKILL.md b/plugins/audio-hooks/skills/audio-hooks/SKILL.md
index b4377ce..6916d1f 100644
--- a/plugins/audio-hooks/skills/audio-hooks/SKILL.md
+++ b/plugins/audio-hooks/skills/audio-hooks/SKILL.md
@@ -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
```