Skip to content

Commit 25103aa

Browse files
committed
release 1.8.0
1 parent 861f5b1 commit 25103aa

9 files changed

Lines changed: 264 additions & 26 deletions

File tree

README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,19 @@ Desktop notifications for AI coding tools - get alerts when tasks complete or in
1313
<img src="assets/multi-tools-support-02.png" width="48%" alt="All tools enabled"/>
1414
</p>
1515

16-
[![Version](https://img.shields.io/badge/version-1.7.4-blue.svg)](https://github.com/mylee04/code-notify/releases)
16+
[![Version](https://img.shields.io/badge/version-1.8.0-blue.svg)](https://github.com/mylee04/code-notify/releases)
1717
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
1818
[![macOS](https://img.shields.io/badge/macOS-supported-green.svg)](https://www.apple.com/macos)
1919
[![Linux](https://img.shields.io/badge/Linux-supported-green.svg)](https://www.linux.org/)
2020
[![Windows](https://img.shields.io/badge/Windows-supported-green.svg)](https://www.microsoft.com/windows)
2121

2222
---
2323

24-
## What's New in v1.7.4
24+
## What's New in v1.8.0
2525

26-
- **Claude agent/team event alerts**: `cn alerts` can now opt into `SubagentStop`, `TeammateIdle`, `TaskCompleted`, and related Claude hook events
27-
- **Less noisy subagent workflows**: Claude event hooks get their own rate-limit bucket via `CODE_NOTIFY_EVENT_RATE_LIMIT_SECONDS`
28-
- **npm package metadata fixed**: npm global install now keeps the `cn`, `cnp`, and `code-notify` binaries when published
26+
- **Immediate AskUserQuestion alerts**: `cn alerts add ask_user` now enables a Claude `PreToolUse` hook for `AskUserQuestion` prompts
27+
- **Preserves custom PreToolUse hooks**: enabling or removing `ask_user` only manages code-notify's own hook and keeps existing user hooks intact
28+
- **Windows UTF-8 stdin fix**: Windows notifications now read redirected Claude hook payloads as UTF-8 before parsing question text
2929

3030
---
3131

@@ -178,6 +178,7 @@ By default, notifications only fire when the AI is idle and waiting for input (`
178178
```bash
179179
cn alerts # Show current config
180180
cn alerts add permission_prompt # Also notify on tool permission requests
181+
cn alerts add ask_user # Notify immediately when Claude asks a question
181182
cn alerts add SubagentStop # Also notify when Claude subagents finish
182183
cn alerts remove permission_prompt # Remove permission notifications
183184
cn alerts reset # Back to default (idle_prompt only)
@@ -189,13 +190,14 @@ cn alerts reset # Back to default (idle_prompt only)
189190
| `permission_prompt` | AI needs tool permission (Y/n) |
190191
| `auth_success` | Authentication success |
191192
| `elicitation_dialog` | MCP tool input needed |
193+
| `ask_user` | Claude asks a question via AskUserQuestion |
192194
| `SubagentStart` | Claude subagent started |
193195
| `SubagentStop` | Claude subagent completed |
194196
| `TeammateIdle` | Claude teammate is waiting for input |
195197
| `TaskCreated` | Claude agent-team task was created |
196198
| `TaskCompleted` | Claude agent-team task completed |
197199

198-
Alert-type matching applies to Claude Code notification hooks and Gemini CLI notification hooks. Claude Code agent/team events are separate hook events and are opt-in via `cn alerts add SubagentStop`, `cn alerts add TeammateIdle`, or `cn alerts add TaskCompleted`.
200+
Alert-type matching applies to Claude Code notification hooks and Gemini CLI notification hooks. `ask_user` is a Claude-only `PreToolUse` hook for `AskUserQuestion`; it is applied immediately when Claude notifications are already enabled. Claude Code agent/team events are separate hook events and are opt-in via `cn alerts add SubagentStop`, `cn alerts add TeammateIdle`, or `cn alerts add TaskCompleted`.
199201

200202
Agent-team and subagent workflows can be noisy if `permission_prompt` is enabled. If you only want idle pings, run `cn alerts remove permission_prompt && cn on`. Codex currently uses completion events from `notify`, so `permission_prompt` and `idle_prompt` settings do not change Codex behavior.
201203

bin/code-notify

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
set -e
77

88
# Version
9-
VERSION="1.7.4"
9+
VERSION="1.8.0"
1010

1111
# Determine the directory where the script is located (resolve symlinks)
1212
SCRIPT_PATH="${BASH_SOURCE[0]}"

lib/code-notify/commands/global.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,7 @@ show_alerts_status() {
10371037
echo ""
10381038
info "Examples:"
10391039
echo " ${CYAN}cn alerts add permission_prompt${RESET} # Also notify on tool permission requests"
1040+
echo " ${CYAN}cn alerts add ask_user${RESET} # Notify immediately when Claude asks a question"
10401041
echo " ${CYAN}cn alerts add SubagentStop${RESET} # Notify when Claude subagents finish"
10411042
echo " ${CYAN}cn alerts add auth_success${RESET} # Also notify on auth success"
10421043
echo " ${CYAN}cn alerts remove permission_prompt${RESET} # Stop permission notifications"
@@ -1120,7 +1121,7 @@ remove_alert_type() {
11201121

11211122
# For ask_user: unregister PreToolUse hook immediately
11221123
if [[ "$type" == "ask_user" ]] && is_tool_enabled "claude"; then
1123-
unregister_ask_user_hook "$GLOBAL_SETTINGS_FILE"
1124+
unregister_ask_user_hook "$GLOBAL_SETTINGS_FILE" "$(get_global_claude_pre_tool_use_command)"
11241125
fi
11251126

11261127
success "Removed: $type"

lib/code-notify/core/config.sh

Lines changed: 112 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,10 @@ get_managed_claude_event_pattern() {
285285
printf '%s\n' '(claude-notify|code-notify.*notifier\.sh|(?:^|[\\/])notify\.(?:ps1|sh)).*(SubagentStart|SubagentStop|TeammateIdle|TaskCreated|TaskCompleted)(?:\s|$)'
286286
}
287287

288+
get_managed_claude_pre_tool_use_pattern() {
289+
printf '%s\n' '(claude-notify|code-notify.*notifier\.sh|(?:^|[\\/])notify\.(?:ps1|sh)).*PreToolUse(?:\s|$)'
290+
}
291+
288292
get_managed_claude_notification_pattern() {
289293
printf '%s\n' '(claude-notify|code-notify.*notifier\.sh|(?:^|[\\/])notify\.(?:ps1|sh)).*(notification|PreToolUse)(?:\s|$)'
290294
}
@@ -935,6 +939,8 @@ PYTHON
935939
register_ask_user_hook() {
936940
local file="$1"
937941
local pre_tool_cmd="$2"
942+
local pre_tool_pattern
943+
pre_tool_pattern="$(get_managed_claude_pre_tool_use_pattern)"
938944

939945
if ! is_notify_type_enabled "ask_user"; then
940946
return 0
@@ -947,12 +953,34 @@ register_ask_user_hook() {
947953
fi
948954

949955
local new_settings
950-
new_settings=$(printf '%s\n' "$settings" | jq --arg cmd "$pre_tool_cmd" '
956+
new_settings=$(printf '%s\n' "$settings" | jq \
957+
--arg cmd "$pre_tool_cmd" \
958+
--arg pattern "$pre_tool_pattern" \
959+
'
951960
def array_or_empty:
952961
if type == "array" then . else [] end;
962+
def is_managed_hook($exact; $pattern):
963+
((.type // "") == "command") and
964+
(
965+
((.command // "") == $exact) or
966+
(((.command // "") | test($pattern)) // false)
967+
);
968+
def strip_managed_ask_user($exact; $pattern):
969+
array_or_empty
970+
| map(
971+
if (.matcher // "") == "AskUserQuestion" and ((.hooks | type) == "array") then
972+
.hooks = (
973+
(.hooks | array_or_empty)
974+
| map(select((is_managed_hook($exact; $pattern)) | not))
975+
)
976+
else
977+
.
978+
end
979+
)
980+
| map(select(((.hooks | type) != "array") or ((.hooks | length) > 0)));
953981
.hooks = (if (.hooks | type) == "object" then .hooks else {} end) |
954982
.hooks.PreToolUse = (
955-
(.hooks.PreToolUse | array_or_empty | map(select(.matcher != "AskUserQuestion"))) + [{
983+
(.hooks.PreToolUse | strip_managed_ask_user($cmd; $pattern)) + [{
956984
"matcher": "AskUserQuestion",
957985
"hooks": [{
958986
"type": "command",
@@ -975,10 +1003,11 @@ register_ask_user_hook() {
9751003
tmp_json=$(mktemp) || return 1
9761004
printf '%s\n' "$settings" > "$tmp_json"
9771005

978-
python3 - "$file" "$pre_tool_cmd" "$tmp_json" << 'PYTHON'
1006+
python3 - "$file" "$pre_tool_cmd" "$pre_tool_pattern" "$tmp_json" << 'PYTHON'
9791007
import json, os, sys, tempfile
1008+
import re
9801009
981-
file_path, pre_tool_cmd, json_file = sys.argv[1:4]
1010+
file_path, pre_tool_cmd, pre_tool_pattern, json_file = sys.argv[1:5]
9821011
9831012
try:
9841013
with open(json_file, "r") as fh:
@@ -995,7 +1024,30 @@ if not isinstance(hooks, dict):
9951024
else:
9961025
hooks = dict(hooks)
9971026
998-
pre_tool_entries = [e for e in hooks.get("PreToolUse", []) if e.get("matcher") != "AskUserQuestion"]
1027+
pre_tool_regex = re.compile(pre_tool_pattern)
1028+
1029+
def is_managed_hook(hook):
1030+
if not isinstance(hook, dict) or hook.get("type") != "command":
1031+
return False
1032+
command = hook.get("command")
1033+
if not isinstance(command, str):
1034+
return False
1035+
return command == pre_tool_cmd or bool(pre_tool_regex.search(command))
1036+
1037+
pre_tool_entries = []
1038+
for entry in hooks.get("PreToolUse", []):
1039+
if not isinstance(entry, dict):
1040+
pre_tool_entries.append(entry)
1041+
continue
1042+
if entry.get("matcher", "") != "AskUserQuestion":
1043+
pre_tool_entries.append(entry)
1044+
continue
1045+
filtered_hooks = [hook for hook in entry.get("hooks", []) if not is_managed_hook(hook)]
1046+
if filtered_hooks:
1047+
new_entry = dict(entry)
1048+
new_entry["hooks"] = filtered_hooks
1049+
pre_tool_entries.append(new_entry)
1050+
9991051
pre_tool_entries.append({
10001052
"matcher": "AskUserQuestion",
10011053
"hooks": [{"type": "command", "command": pre_tool_cmd}]
@@ -1026,6 +1078,9 @@ PYTHON
10261078
# Unregister PreToolUse hook for AskUserQuestion
10271079
unregister_ask_user_hook() {
10281080
local file="$1"
1081+
local pre_tool_cmd="${2:-}"
1082+
local pre_tool_pattern
1083+
pre_tool_pattern="$(get_managed_claude_pre_tool_use_pattern)"
10291084

10301085
if [[ ! -f "$file" ]]; then
10311086
return 0
@@ -1036,14 +1091,32 @@ unregister_ask_user_hook() {
10361091
settings=$(cat "$file")
10371092

10381093
local new_settings
1039-
new_settings=$(printf '%s\n' "$settings" | jq '
1094+
new_settings=$(printf '%s\n' "$settings" | jq \
1095+
--arg cmd "$pre_tool_cmd" \
1096+
--arg pattern "$pre_tool_pattern" \
1097+
'
10401098
def array_or_empty:
10411099
if type == "array" then . else [] end;
1100+
def is_managed_hook($exact; $pattern):
1101+
((.type // "") == "command") and
1102+
(
1103+
((($exact != "") and ((.command // "") == $exact))) or
1104+
(((.command // "") | test($pattern)) // false)
1105+
);
10421106
if (.hooks // {}).PreToolUse then
10431107
.hooks.PreToolUse = (
1044-
(.hooks.PreToolUse | array_or_empty) | map(
1045-
select(.matcher != "AskUserQuestion")
1108+
(.hooks.PreToolUse | array_or_empty)
1109+
| map(
1110+
if (.matcher // "") == "AskUserQuestion" and ((.hooks | type) == "array") then
1111+
.hooks = (
1112+
(.hooks | array_or_empty)
1113+
| map(select((is_managed_hook($cmd; $pattern)) | not))
1114+
)
1115+
else
1116+
.
1117+
end
10461118
)
1119+
| map(select(((.hooks | type) != "array") or ((.hooks | length) > 0)))
10471120
) |
10481121
if (.hooks.PreToolUse | length) == 0 then del(.hooks.PreToolUse) else . end |
10491122
if (.hooks | length) == 0 then del(.hooks) else . end
@@ -1058,10 +1131,11 @@ unregister_ask_user_hook() {
10581131
tmp_json=$(mktemp) || return 1
10591132
cat "$file" > "$tmp_json"
10601133

1061-
python3 - "$file" "$tmp_json" << 'PYTHON'
1134+
python3 - "$file" "$pre_tool_cmd" "$pre_tool_pattern" "$tmp_json" << 'PYTHON'
10621135
import json, os, sys, tempfile
1136+
import re
10631137
1064-
file_path, json_file = sys.argv[1:3]
1138+
file_path, pre_tool_cmd, pre_tool_pattern, json_file = sys.argv[1:5]
10651139
10661140
try:
10671141
with open(json_file, "r") as fh:
@@ -1074,8 +1148,32 @@ finally:
10741148
10751149
hooks = settings.get("hooks", {})
10761150
pre_tool = hooks.get("PreToolUse", [])
1077-
hooks["PreToolUse"] = [e for e in pre_tool if e.get("matcher") != "AskUserQuestion"]
1078-
if not hooks["PreToolUse"]:
1151+
pre_tool_regex = re.compile(pre_tool_pattern)
1152+
1153+
def is_managed_hook(hook):
1154+
if not isinstance(hook, dict) or hook.get("type") != "command":
1155+
return False
1156+
command = hook.get("command")
1157+
if not isinstance(command, str):
1158+
return False
1159+
return (pre_tool_cmd and command == pre_tool_cmd) or bool(pre_tool_regex.search(command))
1160+
1161+
filtered_entries = []
1162+
for entry in pre_tool:
1163+
if not isinstance(entry, dict):
1164+
filtered_entries.append(entry)
1165+
continue
1166+
if entry.get("matcher", "") != "AskUserQuestion":
1167+
filtered_entries.append(entry)
1168+
continue
1169+
filtered_hooks = [hook for hook in entry.get("hooks", []) if not is_managed_hook(hook)]
1170+
if filtered_hooks:
1171+
new_entry = dict(entry)
1172+
new_entry["hooks"] = filtered_hooks
1173+
filtered_entries.append(new_entry)
1174+
1175+
hooks["PreToolUse"] = filtered_entries
1176+
if "PreToolUse" in hooks and not hooks["PreToolUse"]:
10791177
del hooks["PreToolUse"]
10801178
if hooks:
10811179
settings["hooks"] = hooks
@@ -1142,7 +1240,7 @@ disable_hooks_in_settings() {
11421240
" claude"
11431241

11441242
# Remove PreToolUse hook for AskUserQuestion
1145-
unregister_ask_user_hook "$GLOBAL_SETTINGS_FILE"
1243+
unregister_ask_user_hook "$GLOBAL_SETTINGS_FILE" "$(get_global_claude_pre_tool_use_command)"
11461244
}
11471245

11481246
# Disable hooks in project settings.json
@@ -1165,7 +1263,7 @@ disable_project_hooks_in_settings() {
11651263
" claude $(shell_quote "$project_name")"
11661264

11671265
# Remove PreToolUse hook for AskUserQuestion
1168-
unregister_ask_user_hook "$project_settings"
1266+
unregister_ask_user_hook "$project_settings" "$(get_project_claude_pre_tool_use_command "$project_name")"
11691267
}
11701268

11711269
# Enable hooks in project settings.json

lib/code-notify/utils/help.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ ${BOLD}ALERT TYPES:${RESET}
6161
${GREEN}alerts remove${RESET} <type> Remove a notification type
6262
${GREEN}alerts reset${RESET} Reset to default (idle_prompt only)
6363
64-
Notification types: ${CYAN}idle_prompt${RESET} (default), ${CYAN}permission_prompt${RESET}, ${CYAN}auth_success${RESET}, ${CYAN}elicitation_dialog${RESET}
64+
Notification types: ${CYAN}idle_prompt${RESET} (default), ${CYAN}permission_prompt${RESET}, ${CYAN}auth_success${RESET}, ${CYAN}elicitation_dialog${RESET}, ${CYAN}ask_user${RESET}
6565
Claude events: ${CYAN}SubagentStart${RESET}, ${CYAN}SubagentStop${RESET}, ${CYAN}TeammateIdle${RESET}, ${CYAN}TaskCreated${RESET}, ${CYAN}TaskCompleted${RESET}
6666
Note: alert-type matching applies to Claude Code and Gemini CLI hooks.
6767
Codex currently exposes completion events through its notify payload.
@@ -114,6 +114,7 @@ ${BOLD}EXAMPLES:${RESET}
114114
cn update check # Check whether an update is needed and show the update command
115115
cn alerts # Show alert type config
116116
cn alerts add permission_prompt # Also notify on permission requests
117+
cn alerts add ask_user
117118
cn alerts add SubagentStop # Also notify when Claude subagents finish
118119
cn alerts reset # Back to idle_prompt only (less noisy)
119120
cn sound on # Enable notification sounds

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "code-notify",
3-
"version": "1.7.4",
3+
"version": "1.8.0",
44
"description": "Desktop notifications for Claude Code, OpenAI Codex, and Gemini CLI",
55
"license": "MIT",
66
"homepage": "https://github.com/mylee04/code-notify#readme",

scripts/install-windows.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ param(
2020
$ErrorActionPreference = "Stop"
2121

2222
# Version
23-
$VERSION = "1.7.4"
23+
$VERSION = "1.8.0"
2424

2525
# Colors and formatting
2626
function Write-Success { param([string]$Message) Write-Host "[OK] $Message" -ForegroundColor Green }
@@ -107,7 +107,7 @@ function Install-ClaudeNotify {
107107
# Code-Notify PowerShell Module
108108
# https://github.com/mylee04/code-notify
109109
110-
$script:VERSION = "1.7.4"
110+
$script:VERSION = "1.8.0"
111111
$script:ClaudeHome = if ($env:CLAUDE_HOME) { $env:CLAUDE_HOME } else { "$env:USERPROFILE\.claude" }
112112
$script:DefaultSettingsFile = "$script:ClaudeHome\settings.json"
113113
$script:AlternateSettingsFile = "$env:USERPROFILE\.config\.claude\settings.json"

scripts/run_tests.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,13 @@ else
230230
test_fail "Claude event alert hooks failed"
231231
fi
232232

233+
test_start "ask_user alert preservation"
234+
if bash tests/test-ask-user-alert.sh >/dev/null 2>&1; then
235+
test_pass
236+
else
237+
test_fail "ask_user alert preservation failed"
238+
fi
239+
233240
# Summary
234241
echo ""
235242
echo "Test Summary:"

0 commit comments

Comments
 (0)