Problem
The claude-notifications-go plugin does not support per-status webhook filtering — webhook enabled/disabled is global and the per-status enabled flag affects both desktop and webhook simultaneously (see upstream issue #74).
This makes the following common setup impossible out of the box:
Desktop: notify on everything (full local feedback)
Discord: only notify when main task completes OR I need to make a decision (remote signal for "something needs me")
If we enable webhook globally, Discord gets flooded with every review_complete / intermediate response. If we disable webhook globally, Discord never notifies. Upstream fix is uncertain (no PR tracker, no existing user request before #74).
Goal — Discord notification policy
| Event |
Desktop |
Discord |
| Main task complete (Stop) |
✅ |
✅ |
| Permission prompt (Notification) |
✅ |
✅ |
| AskUserQuestion (PreToolUse) |
✅ |
✅ |
| ExitPlanMode (PreToolUse) |
✅ |
✅ |
| Review-style completion |
✅ |
❌ |
| Other Stop (chat, no clear completion) |
✅ |
❌ |
| SubagentStop |
❌ |
❌ |
Core idea: Discord fires only for "main task complete" or "needs my decision".
Proposed solution — independent CC hook, sibling to the plugin
Rather than patch the plugin or run a local webhook proxy, add a standalone Claude Code hook that runs in parallel with claude-notifications-go.
Claude Code
├─→ claude-notifications-go hooks ─→ desktop (all 4 statuses, unchanged)
└─→ new hook (this feature) ─→ filter script ─→ Discord (4 selected events)
- Plugin keeps
webhook.enabled = false — plugin does desktop only
- Our hook reads the same webhook URL from the plugin's config (single source of truth)
- No daemon, no fork, no launchd
Trigger matrix
The hook script reads hook-event JSON from stdin and decides whether to forward to Discord:
| Hook |
Matcher |
Action |
PreToolUse |
ExitPlanMode |
send as "Plan Ready" |
PreToolUse |
AskUserQuestion |
send as "Question" |
Notification |
permission_prompt |
send as "Permission Needed" |
Stop |
— |
send as "Task Complete" if last assistant message matches completion keywords |
SubagentStop |
— |
skip entirely |
Why filter Stop by keywords
Stop fires after every assistant turn, not just task completions. Matching against a keyword list (completed, done, finished, 完成, 搞定, 已处理, 好了, …) preserves the plugin's existing semantic — the plugin already classifies Stop this way.
Alternative discussed and rejected: "fire Discord on every Stop" — too noisy for conversational use.
Deliverables
-
scripts/discord-filter.sh — ~60 LOC bash
- Parse stdin JSON (
hook_event_name, tool_name, transcript_path, session_id, cwd)
- Decide trigger based on event/matcher; if Stop, read last assistant message from transcript and keyword-match
- Build Discord embed (title + color by status, session name + git branch, timestamp)
curl POST to Discord URL
-
Hook registrations in ~/.claude/settings.json (documented snippet users paste)
Stop
PreToolUse with matcher ExitPlanMode|AskUserQuestion
Notification with matcher permission_prompt
-
Optional throttle: file-based last-sent timestamp to dedupe within a short window (e.g. 30s per session+status)
-
README section explaining setup, keywords, how to customize
Edge cases & risks
| Risk |
Mitigation |
| Keyword list can miss (e.g. assistant finishes without a completion word) |
Prefer recall: add common Chinese + English completion terms; easy to extend |
| Double source of truth for Discord URL if users manage both places |
Read from plugin config (~/.claude/claude-notifications-go/config.json → notifications.webhook.url); keep plugin's webhook.enabled = false |
| Discord URL is a secret |
Document chmod 700 ~/.claude/scripts/; reference plugin's existing .gitignore guidance |
| If upstream issue #74 is implemented later, this feature becomes redundant |
Migration is trivial: delete two files, flip plugin config — document this as a sunset path |
Testing plan
Manual verification for each event type:
- Simple task → desktop + Discord both fire
- Permission prompt → desktop + Discord
- Active
ExitPlanMode → desktop + Discord
- Active
AskUserQuestion → desktop + Discord
- Review-style response (e.g. "已审查") → desktop only, Discord silent
- Subagent completion → both silent
- Non-completion chat ("let me check…") → both silent (or desktop only, Discord silent)
Estimated effort
- Script: 30–40 min
- Hook registration + README: 10 min
- Test: 10 min
- Total: ~1 hour
Problem
The claude-notifications-go plugin does not support per-status webhook filtering — webhook enabled/disabled is global and the per-status
enabledflag affects both desktop and webhook simultaneously (see upstream issue #74).This makes the following common setup impossible out of the box:
If we enable webhook globally, Discord gets flooded with every
review_complete/ intermediate response. If we disable webhook globally, Discord never notifies. Upstream fix is uncertain (no PR tracker, no existing user request before #74).Goal — Discord notification policy
Core idea: Discord fires only for "main task complete" or "needs my decision".
Proposed solution — independent CC hook, sibling to the plugin
Rather than patch the plugin or run a local webhook proxy, add a standalone Claude Code hook that runs in parallel with
claude-notifications-go.webhook.enabled = false— plugin does desktop onlyTrigger matrix
The hook script reads hook-event JSON from stdin and decides whether to forward to Discord:
PreToolUseExitPlanModePreToolUseAskUserQuestionNotificationpermission_promptStopSubagentStopWhy filter
Stopby keywordsStopfires after every assistant turn, not just task completions. Matching against a keyword list (completed,done,finished,完成,搞定,已处理,好了, …) preserves the plugin's existing semantic — the plugin already classifies Stop this way.Alternative discussed and rejected: "fire Discord on every Stop" — too noisy for conversational use.
Deliverables
scripts/discord-filter.sh— ~60 LOC bashhook_event_name,tool_name,transcript_path,session_id,cwd)curlPOST to Discord URLHook registrations in
~/.claude/settings.json(documented snippet users paste)StopPreToolUsewith matcherExitPlanMode|AskUserQuestionNotificationwith matcherpermission_promptOptional throttle: file-based last-sent timestamp to dedupe within a short window (e.g. 30s per session+status)
READMEsection explaining setup, keywords, how to customizeEdge cases & risks
~/.claude/claude-notifications-go/config.json→notifications.webhook.url); keep plugin'swebhook.enabled = falsechmod 700 ~/.claude/scripts/; reference plugin's existing .gitignore guidanceTesting plan
Manual verification for each event type:
ExitPlanMode→ desktop + DiscordAskUserQuestion→ desktop + DiscordEstimated effort