Skip to content

Commit e4ae85b

Browse files
ChanMeng666claude
andcommitted
feat: per-hook notification mode overrides (v4.3.0)
Add `notification_settings.per_hook` to independently control audio and desktop notifications per hook type. Frequent hooks like pretooluse can use audio_only to avoid slow desktop notification queuing, while critical hooks keep both channels. New `disabled` mode suppresses audio and desktop but preserves TTS/logging. Fully backward compatible — omitting per_hook uses the global mode as before. - Add per-hook mode resolution with validation and fallback in hook_runner.py - Add `--hook-mode` CLI flag to configure.sh - Add `disabled` as a valid notification mode - Update config templates, CLAUDE.md, README.md, CHANGELOG.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent dcc4558 commit e4ae85b

File tree

7 files changed

+202
-12
lines changed

7 files changed

+202
-12
lines changed

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,26 @@ All notable changes to Claude Code Audio Hooks will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [4.3.0] - 2026-02-17
9+
10+
### Added
11+
- **Per-hook notification mode overrides**: New `notification_settings.per_hook` config allows independently controlling audio and desktop notifications per hook type (e.g., `"pretooluse": "audio_only"` to skip desktop notifications for frequent hooks)
12+
- **`disabled` notification mode**: Suppresses both audio and desktop notifications while still allowing TTS and logging — different from `enabled_hooks: false` which skips everything
13+
- **`--hook-mode` CLI flag**: `bash scripts/configure.sh --hook-mode pretooluse=audio_only posttooluse=disabled` for quick per-hook mode configuration
14+
- Per-hook mode validation with automatic fallback to global mode on invalid values
15+
16+
### Changed
17+
- `hook_runner.py` notification mode resolution now checks `per_hook` overrides before falling back to global `notification_settings.mode`
18+
- Debug logging now shows both per-hook and global mode for each hook trigger
19+
- Updated `config/default_preferences.json` and `config/user_preferences.json` with `per_hook` field
20+
- Updated CLAUDE.md, README.md with per-hook notification mode documentation
21+
22+
### Upgrade
23+
24+
No reinstall needed — existing installations self-update automatically on the next hook trigger after `git pull`. The `per_hook` field is fully backward compatible: if absent, all hooks use the global mode as before.
25+
26+
---
27+
828
## [4.2.2] - 2026-02-14
929

1030
### Fixed

CLAUDE.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Claude Code Audio Hooks - AI Assistant Guide
22

3-
> **Version:** 4.2.2 | **Last Updated:** 2026-02-14
3+
> **Version:** 4.3.0 | **Last Updated:** 2026-02-17
44
55
This document is designed for AI assistants (Claude Code, Cursor, Copilot, etc.) to understand and help users install this project correctly.
66

@@ -238,7 +238,11 @@ D:/github_repository/claude-code-audio-hooks
238238
},
239239
"notification_settings": {
240240
"mode": "audio_and_notification",
241-
"show_context": true
241+
"show_context": true,
242+
"per_hook": {
243+
"pretooluse": "audio_only",
244+
"posttooluse": "audio_only"
245+
}
242246
},
243247
"tts_settings": {
244248
"enabled": false,
@@ -382,6 +386,7 @@ Instruct user to:
382386

383387
| Version | Date | Key Changes |
384388
|---------|------|-------------|
389+
| 4.3.0 | 2026-02-17 | Per-hook notification mode overrides: independently control audio/desktop notifications per hook type via `notification_settings.per_hook` |
385390
| 4.2.2 | 2026-02-14 | Robust theme switching: remove conflicting `audio_files` config, add hook_runner.py auto-sync from project dir, configure.sh syncs on theme switch |
386391
| 4.2.0 | 2026-02-13 | Add 4 new hooks (PostToolUseFailure, SubagentStart, TeammateIdle, TaskCompleted), 14 total hooks, 14 unique audio files |
387392
| 4.0.3 | 2026-02-11 | Fix Windows installer hook filtering, uninstaller hook_runner.py detection, defensive wrapping |

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,13 +791,46 @@ Configure in `config/user_preferences.json`:
791791
| `audio_only` | Yes | No | Classic behavior, backward compatible |
792792
| `notification_only` | No | Yes | Silent environments, visual-only alerts |
793793
| `audio_and_notification` | Yes | Yes | Maximum awareness (recommended) |
794+
| `disabled` | No | No | Suppress both (TTS/logging still works) |
794795

795796
Desktop notifications show context from Claude Code:
796797
- **Stop**: "Task completed"
797798
- **Notification**: "Authorization needed: Allow Bash command?"
798799
- **PreToolUse**: "Running: Bash"
799800
- **SubagentStop**: "Background task finished (Explore)"
800801

802+
### **Per-Hook Notification Mode** *(v4.3.0)*
803+
804+
Override the global mode for specific hooks. Hooks not listed in `per_hook` fall back to the global `mode`. This is useful when frequent hooks (like `pretooluse`) should only play audio without queuing slow desktop notifications:
805+
806+
```json
807+
{
808+
"notification_settings": {
809+
"mode": "audio_and_notification",
810+
"show_context": true,
811+
"per_hook": {
812+
"pretooluse": "audio_only",
813+
"posttooluse": "audio_only",
814+
"precompact": "disabled"
815+
}
816+
}
817+
}
818+
```
819+
820+
| Mode | Audio | Desktop Popup | Notes |
821+
|------|-------|---------------|-------|
822+
| `audio_only` | Yes | No | Fast, no desktop notification delay |
823+
| `notification_only` | No | Yes | Visual-only, no audio |
824+
| `audio_and_notification` | Yes | Yes | Both channels |
825+
| `disabled` | No | No | Suppresses both (TTS/logging still works) |
826+
827+
> **Note:** `"disabled"` is different from `enabled_hooks: false` — the hook still fires for TTS and logging, it just skips audio and desktop notifications.
828+
829+
**CLI shortcut:**
830+
```bash
831+
bash scripts/configure.sh --hook-mode pretooluse=audio_only posttooluse=disabled
832+
```
833+
801834
### **Text-to-Speech**
802835

803836
Enable spoken context-aware messages:

config/default_preferences.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,11 @@
6464
},
6565

6666
"notification_settings": {
67-
"_comment": "Desktop notification mode: audio_only, notification_only, audio_and_notification",
67+
"_comment": "Desktop notification mode: audio_only, notification_only, audio_and_notification, disabled",
6868
"mode": "audio_and_notification",
69-
"show_context": true
69+
"show_context": true,
70+
"_comment_per_hook": "Per-hook mode overrides. Omit a hook to use the global mode above.",
71+
"per_hook": {}
7072
},
7173

7274
"tts_settings": {

config/user_preferences.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,14 @@
6464
},
6565

6666
"notification_settings": {
67-
"_comment": "Desktop notification mode: audio_only, notification_only, audio_and_notification",
67+
"_comment": "Desktop notification mode: audio_only, notification_only, audio_and_notification, disabled",
6868
"mode": "audio_and_notification",
69-
"show_context": true
69+
"show_context": true,
70+
"_comment_per_hook": "Override the global mode for specific hooks. Omit a hook to use the global mode.",
71+
"per_hook": {
72+
"pretooluse": "audio_only",
73+
"posttooluse": "audio_only"
74+
}
7075
},
7176

7277
"tts_settings": {

hooks/hook_runner.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
# Version used for auto-sync: when the installed copy in ~/.claude/hooks/
3131
# detects a newer version in the project directory, it self-updates.
32-
HOOK_RUNNER_VERSION = "4.2.2"
32+
HOOK_RUNNER_VERSION = "4.3.0"
3333

3434
# =============================================================================
3535
# DEBUG LOGGING SYSTEM
@@ -1028,11 +1028,20 @@ def run_hook(hook_type: str, stdin_data: dict = None) -> int:
10281028
# Load config once for notification/TTS settings
10291029
config = load_config()
10301030

1031-
# Determine notification mode (backward compatible: default to audio_only)
1031+
# Determine notification mode with per-hook override support
10321032
notification_settings = config.get("notification_settings", {})
1033-
mode = notification_settings.get("mode", "audio_only")
1034-
1035-
# Play audio (unless mode is notification_only)
1033+
global_mode = notification_settings.get("mode", "audio_only")
1034+
per_hook_modes = {k: v for k, v in notification_settings.get("per_hook", {}).items() if not k.startswith("_")}
1035+
mode = per_hook_modes.get(hook_type, global_mode)
1036+
1037+
# Validate mode (fall back to global if invalid)
1038+
valid_modes = ("audio_only", "notification_only", "audio_and_notification", "disabled")
1039+
if mode not in valid_modes:
1040+
log_debug(f"Invalid per_hook mode '{mode}' for {hook_type}, falling back to '{global_mode}'")
1041+
mode = global_mode
1042+
log_debug(f"Notification mode for {hook_type}: {mode} (global={global_mode})")
1043+
1044+
# Play audio (unless mode is notification_only or disabled)
10361045
if mode in ("audio_only", "audio_and_notification"):
10371046
audio_file = get_audio_file(hook_type)
10381047
if not audio_file:
@@ -1049,14 +1058,18 @@ def run_hook(hook_type: str, stdin_data: dict = None) -> int:
10491058
log_error(f"Failed to play audio: {audio_file}")
10501059
elif mode == "notification_only":
10511060
log_trigger(hook_type, "AUDIO_SKIPPED", f"mode={mode}")
1061+
elif mode == "disabled":
1062+
log_trigger(hook_type, "AUDIO_SKIPPED", "mode=disabled")
10521063

1053-
# Desktop notification
1064+
# Desktop notification (unless mode is audio_only or disabled)
10541065
if mode in ("notification_only", "audio_and_notification"):
10551066
context = get_notification_context(hook_type, stdin_data or {})
10561067
urgency = "critical" if hook_type in ("notification", "permission_request", "posttoolusefailure") else "normal"
10571068
notif_sent = send_desktop_notification("Claude Code", context, urgency)
10581069
if notif_sent:
10591070
log_debug(f"Desktop notification sent for {hook_type}: {context}")
1071+
elif mode == "disabled":
1072+
log_trigger(hook_type, "NOTIFICATION_SKIPPED", "mode=disabled")
10601073

10611074
# TTS (text-to-speech)
10621075
tts_settings = config.get("tts_settings", {})

scripts/configure.sh

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,99 @@ print("OK")
364364
PYTHON_SCRIPT
365365
}
366366

367+
# Programmatic per-hook notification mode setting
368+
cmd_hook_mode() {
369+
local assignments=("$@")
370+
if [ ${#assignments[@]} -eq 0 ]; then
371+
echo -e "${RED}Error: At least one hook=mode assignment required${NC}" >&2
372+
echo "Usage: $0 --hook-mode <hook>=<mode> [hook2=mode2 ...]" >&2
373+
echo "Valid modes: audio_only, notification_only, audio_and_notification, disabled" >&2
374+
echo "Example: $0 --hook-mode pretooluse=audio_only posttooluse=disabled" >&2
375+
exit 1
376+
fi
377+
378+
local valid_modes="audio_only notification_only audio_and_notification disabled"
379+
local changed=0
380+
381+
for assignment in "${assignments[@]}"; do
382+
if [[ ! "$assignment" =~ ^([a-z_]+)=(.+)$ ]]; then
383+
echo -e "${YELLOW}Warning: Invalid format '$assignment', use hook=mode${NC}" >&2
384+
continue
385+
fi
386+
387+
local hook_name="${BASH_REMATCH[1]}"
388+
local mode="${BASH_REMATCH[2]}"
389+
390+
# Validate hook name
391+
local index=$(get_hook_index "$hook_name")
392+
if [ -z "$index" ]; then
393+
echo -e "${YELLOW}Warning: Unknown hook '$hook_name', skipping${NC}" >&2
394+
continue
395+
fi
396+
397+
# Validate mode
398+
local mode_valid=false
399+
for vm in $valid_modes; do
400+
if [[ "$mode" == "$vm" ]]; then
401+
mode_valid=true
402+
break
403+
fi
404+
done
405+
if [[ "$mode_valid" != "true" ]]; then
406+
echo -e "${YELLOW}Warning: Invalid mode '$mode' for $hook_name, skipping${NC}" >&2
407+
echo " Valid modes: $valid_modes" >&2
408+
continue
409+
fi
410+
411+
echo -e "${GREEN}${NC} Set $hook_name mode = $mode"
412+
((changed++))
413+
done
414+
415+
if [ $changed -eq 0 ]; then
416+
echo -e "${RED}No valid assignments to apply${NC}" >&2
417+
exit 1
418+
fi
419+
420+
# Apply all changes via Python
421+
python3 << PYTHON_SCRIPT
422+
import json
423+
424+
config_file = "$CONFIG_FILE"
425+
assignments = """$@""".split()
426+
427+
try:
428+
with open(config_file, 'r') as f:
429+
config = json.load(f)
430+
except:
431+
config = {}
432+
433+
notification_settings = config.setdefault("notification_settings", {})
434+
per_hook = notification_settings.setdefault("per_hook", {})
435+
436+
for assignment in assignments:
437+
if '=' in assignment:
438+
hook_name, mode = assignment.split('=', 1)
439+
per_hook[hook_name] = mode
440+
441+
# Remove _comment keys from per_hook if present (clean save)
442+
per_hook_clean = {k: v for k, v in per_hook.items() if not k.startswith('_')}
443+
notification_settings["per_hook"] = per_hook_clean
444+
445+
with open(config_file, 'w') as f:
446+
json.dump(config, f, indent=2)
447+
448+
print("OK")
449+
PYTHON_SCRIPT
450+
451+
if [ $? -eq 0 ]; then
452+
echo -e "\n${GREEN}${BOLD}✓ Per-hook notification modes saved${NC}"
453+
echo -e "${YELLOW}Remember to restart Claude Code to apply changes${NC}"
454+
else
455+
echo -e "${RED}${NC} Failed to save per-hook modes" >&2
456+
exit 1
457+
fi
458+
}
459+
367460
# Programmatic theme switching
368461
cmd_theme() {
369462
local theme=$1
@@ -514,6 +607,12 @@ ${CYAN}PROGRAMMATIC MODE${NC} (with arguments):
514607
custom - Modern UI sound effects, no voice (audio/custom/)
515608
Example: $0 --theme custom
516609
610+
${BOLD}--hook-mode <hook>=<mode> [hook2=mode2 ...]${NC}
611+
Set per-hook notification mode overrides
612+
Valid modes: audio_only, notification_only, audio_and_notification, disabled
613+
Hooks not listed fall back to the global notification_settings.mode
614+
Example: $0 --hook-mode pretooluse=audio_only posttooluse=disabled
615+
517616
${BOLD}--reset${NC}
518617
Reset to recommended defaults
519618
(Enables: notification, stop, subagent_stop, permission_request; Disables: all others)
@@ -544,6 +643,9 @@ ${CYAN}EXAMPLES${NC}:
544643
# Mixed operations
545644
$0 --enable notification --disable pretooluse --set stop=true
546645
646+
# Set per-hook notification modes (audio only for noisy hooks)
647+
$0 --hook-mode pretooluse=audio_only posttooluse=disabled
648+
547649
# Check if notification hook is enabled
548650
$0 --get notification
549651
@@ -815,6 +917,16 @@ process_arguments() {
815917
cmd_theme "$1"
816918
exit 0
817919
;;
920+
--hook-mode)
921+
shift
922+
local hook_modes=()
923+
while [ $# -gt 0 ] && [[ ! "$1" =~ ^-- ]]; do
924+
hook_modes+=("$1")
925+
shift
926+
done
927+
cmd_hook_mode "${hook_modes[@]}"
928+
exit 0
929+
;;
818930
--reset)
819931
cmd_reset
820932
exit 0

0 commit comments

Comments
 (0)