Install
$ agentstack add skill-extractumio-extractum-skills-mac-notification-claude-code-config Open-source listing, not yet scanned by AgentStack. Follow the source repository for install instructions.
Security review
⚠ Flagged1 finding(s); flagged for manual review. · v0.1.0 How review works →
- • Prompt-injection patterns
- • Secret / credential exfiltration
- • Dangerous shell & filesystem operations
- • Untrusted network calls
- • Known-malicious package signatures
- high Destructive filesystem operation.
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
macOS Native Notifications for Claude Code
Send native macOS notification banners when Claude Code needs your attention — long-running tasks finishing, permission requests, and MCP input prompts. Notifications are suppressed when iTerm2 is already focused, and include the terminal session identity so you know exactly which session needs you.
Works both locally on macOS and remotely on Linux (Ubuntu/Debian) via iTerm2's tmux -CC integration.
When to use this skill
- User says something like "notify me when Claude finishes", "set up macOS notifications for Claude Code", "I want a banner when permission is needed", or "configure iTerm2 triggers for remote Claude notifications".
- User is running Claude Code on a remote Linux box via
tmux -CCand wants native notifications on their Mac. - User wants to customize the minimum task duration, switch to Terminal.app instead of iTerm2, or add a sound to notifications.
- Troubleshooting: notifications aren't appearing, marker text is visible in terminal, stale
/tmp/.claude-task-start-*files, or iTerm2 Trigger isn't firing.
What you get
| Event | Notification | When | |---|---|---| | Stop | Task completed with elapsed time | Only if task took > 2 minutes | | PermissionRequest | Claude is blocked waiting for approval | Always | | Elicitation | MCP tool needs your input | Always |
All notifications are suppressed when iTerm2 is the frontmost app. Clicking a notification brings iTerm2 to focus, even across macOS Spaces/desktops.
Part 1: Local macOS Setup
For Claude Code running directly on your Mac.
Prerequisites
- macOS (tested on Sequoia)
- iTerm2
- Homebrew
- Claude Code with hooks support
- Python 3 (pre-installed on macOS)
Installation
1. Install terminal-notifier
brew install terminal-notifier
2. Configure macOS Notification Settings
Go to System Settings > Notifications > terminal-notifier and set:
- Allow Notifications: On
- Alert style: Alerts (stays on screen until clicked; use Banners if you prefer auto-dismiss)
Also ensure Do Not Disturb / Focus Mode is off, or terminal-notifier is allowed through your Focus filter.
3. Create the hook scripts
mkdir -p ~/.claude/hooks
~/.claude/hooks/mark-start.sh
Records the timestamp and iTerm2 tab title when the user submits a prompt.
#!/bin/bash
# Writes a start timestamp when user submits a prompt
input=$(cat)
session_id=$(/usr/bin/python3 -c "import sys,json; print(json.load(sys.stdin).get('session_id',''))" /dev/null)
stamp="/tmp/.claude-task-start-${session_id}"
# Clean up stale files older than 24h from crashed sessions
find /tmp -name ".claude-task-start-*" -mmin +1440 -delete 2>/dev/null
# Capture timestamp and iTerm2 tab/window title
tab_title=$(osascript -e '
tell application "iTerm2"
tell current session of current tab of current window
return name
end tell
end tell
' 2>/dev/null)
printf '%s\n%s' "$(date +%s)" "$tab_title" > "$stamp"
~/.claude/hooks/notify.sh
Unified notification handler. Parses JSON from stdin, checks whether a notification is warranted, and sends it via terminal-notifier.
#!/bin/bash
# Unified Claude Code notification hook
# Handles: Stop (long tasks), PermissionRequest, Elicitation
input=$(cat)
eval "$(/usr/bin/python3 -c "
import sys, json
d = json.load(sys.stdin)
event = d.get('hook_event_name', '')
cwd = d.get('cwd', '')
session_id = d.get('session_id', '')
message = d.get('last_assistant_message', '')
tool_name = d.get('tool_name', '')
error = d.get('error', '')
mcp_server = d.get('mcp_server_name', '')
def trunc(s, n=200):
return s[:n].rsplit(' ', 1)[0] + '...' if len(s) > n else s
message = trunc(message)
error = trunc(error)
print(f'EVENT={repr(event)}')
print(f'CWD={repr(cwd)}')
print(f'SESSION_ID={repr(session_id)}')
print(f'MESSAGE={repr(message)}')
print(f'TOOL_NAME={repr(tool_name)}')
print(f'TOOL_ERROR={repr(error)}')
print(f'MCP_SERVER={repr(mcp_server)}')
" /dev/null)"
project=$(basename "$CWD")
terminal_notifier=$(which terminal-notifier)
# Skip notification if iTerm is the frontmost app (user is already looking at it)
frontmost=$(osascript -e 'tell application "System Events" to get name of first process whose frontmost is true' 2>/dev/null)
[ "$frontmost" = "iTerm2" ] && exit 0
case "$EVENT" in
Stop)
stamp="/tmp/.claude-task-start-${SESSION_ID}"
if [ -f "$stamp" ]; then
start_time=$(head -1 "$stamp")
tab_title=$(tail -1 "$stamp")
now=$(date +%s)
elapsed=$((now - start_time))
rm -f "$stamp"
[ "$elapsed" -lt 120 ] && exit 0
mins=$((elapsed / 60))
secs=$((elapsed % 60))
"$terminal_notifier" \
-title "Done" \
-subtitle "${tab_title:-$project} -- ${mins}m ${secs}s" \
-message "${MESSAGE:-Task completed}" \
-activate com.googlecode.iterm2
fi
;;
PermissionRequest|Elicitation)
# Fetch current tab title live for blocking events
tab_title=$(osascript -e '
tell application "iTerm2"
tell current session of current tab of current window
return name
end tell
end tell
' 2>/dev/null)
if [ "$EVENT" = "PermissionRequest" ]; then
"$terminal_notifier" \
-title "Needs Approval" \
-subtitle "${tab_title:-$project}" \
-message "Permission needed for: $TOOL_NAME" \
-activate com.googlecode.iterm2
else
"$terminal_notifier" \
-title "Input Needed" \
-subtitle "${tab_title:-$project} -- ${MCP_SERVER:-MCP}" \
-message "Tool $TOOL_NAME needs your input" \
-activate com.googlecode.iterm2
fi
;;
esac
Make both scripts executable:
chmod +x ~/.claude/hooks/mark-start.sh ~/.claude/hooks/notify.sh
4. Configure Claude Code hooks
Add the following hooks block to your ~/.claude/settings.json:
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/mark-start.sh"
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/notify.sh"
}
]
}
],
"PermissionRequest": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/notify.sh"
}
]
}
],
"Elicitation": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/notify.sh"
}
]
}
]
}
}
No restart required — Claude Code picks up settings changes automatically.
Part 2: Remote Linux Setup (via iTerm2 + tmux -CC)
For Claude Code running on a remote Ubuntu/Debian server, accessed via iTerm2's native tmux integration (tmux -CC).
How It Works
Remote Server (Ubuntu/Debian) Local Mac (iTerm2)
================================ ================================
Claude Code hook fires
|
v
remote-notify.sh
|
v
Writes marker string to
tmux pane TTY:
@@CLAUDE_NOTIFY|type|sub|msg@@
|
+--- flows through tmux -CC ----------> iTerm2 Trigger catches regex
|
v
Runs local-notify.sh
|
v
terminal-notifier shows
native macOS notification
The transport is plain text through the tmux session. No reverse SSH, no extra ports, no daemons.
Prerequisites
On the remote server (Ubuntu 22/24, Debian 12+):
- Python 3 (
apt install python3) - tmux
- Claude Code with hooks support
On your local Mac:
- Everything from Part 1 (terminal-notifier, iTerm2)
- iTerm2 Trigger configured (see below)
Remote Installation
1. Create hook scripts on the remote server
mkdir -p ~/.claude/hooks
~/.claude/hooks/mark-start.sh
Records the timestamp, tmux pane title, and pane TTY path when the user submits a prompt.
#!/bin/bash
# Writes a start timestamp when user submits a prompt (remote/Linux version)
# Captures tmux session:window.pane identity and pane title for notifications
input=$(cat)
session_id=$(python3 -c "import sys,json; print(json.load(sys.stdin).get('session_id',''))" /dev/null)
stamp="/tmp/.claude-task-start-${session_id}"
# Clean up stale files older than 24h from crashed sessions
find /tmp -maxdepth 1 -name ".claude-task-start-*" -mmin +1440 -delete 2>/dev/null
# Capture tmux pane title and identity
pane_title=""
pane_tty=""
if [ -n "$TMUX" ]; then
pane_title=$(tmux display-message -p '#{session_name}:#{window_name}' 2>/dev/null)
if [ -n "$TMUX_PANE" ]; then
pane_tty=$(tmux display-message -t "$TMUX_PANE" -p '#{pane_tty}' 2>/dev/null)
else
pane_tty=$(tmux display-message -p '#{pane_tty}' 2>/dev/null)
fi
fi
# Store: line 1 = timestamp, line 2 = pane title, line 3 = pane tty
printf '%s\n%s\n%s' "$(date +%s)" "$pane_title" "$pane_tty" > "$stamp"
~/.claude/hooks/notify.sh
Outputs a marker string to the tmux pane TTY. iTerm2 Trigger on your Mac catches it and calls local-notify.sh.
#!/bin/bash
# Remote Claude Code notification hook (Ubuntu/Debian + tmux -CC -> iTerm2)
#
# Outputs a marker string to the tmux pane TTY.
# iTerm2 Trigger on the local Mac catches the marker and calls local-notify.sh.
#
# Handles: Stop (long tasks), PermissionRequest, Elicitation
input=$(cat)
eval "$(python3 -c "
import sys, json
d = json.load(sys.stdin)
event = d.get('hook_event_name', '')
cwd = d.get('cwd', '')
session_id = d.get('session_id', '')
message = d.get('last_assistant_message', '')
tool_name = d.get('tool_name', '')
mcp_server = d.get('mcp_server_name', '')
def trunc(s, n=200):
return s[:n].rsplit(' ', 1)[0] + '...' if len(s) > n else s
def clean(s):
return trunc(s).replace('|', '/').replace('\n', ' ').replace('\r', '')
print(f'EVENT={repr(event)}')
print(f'CWD={repr(cwd)}')
print(f'SESSION_ID={repr(session_id)}')
print(f'MESSAGE={repr(clean(message))}')
print(f'TOOL_NAME={repr(tool_name)}')
print(f'MCP_SERVER={repr(mcp_server)}')
" /dev/null)"
project=$(basename "$CWD")
stamp="/tmp/.claude-task-start-${SESSION_ID}"
# Resolve the TTY to write the marker to
# Priority: stored TTY from mark-start, then current tmux pane, then /dev/tty
resolve_tty() {
# Try stored TTY from mark-start (line 3 of stamp file)
if [ -f "$stamp" ]; then
local stored_tty
stored_tty=$(sed -n '3p' "$stamp")
if [ -n "$stored_tty" ] && [ -w "$stored_tty" ]; then
echo "$stored_tty"
return
fi
fi
# Try tmux
if [ -n "$TMUX_PANE" ]; then
local tty
tty=$(tmux display-message -t "$TMUX_PANE" -p '#{pane_tty}' 2>/dev/null)
if [ -n "$tty" ] && [ -w "$tty" ]; then
echo "$tty"
return
fi
fi
if [ -n "$TMUX" ]; then
local tty
tty=$(tmux display-message -p '#{pane_tty}' 2>/dev/null)
if [ -n "$tty" ] && [ -w "$tty" ]; then
echo "$tty"
return
fi
fi
echo "/dev/tty"
}
notify_tty=$(resolve_tty)
send_marker() {
local type="$1" subtitle="$2" message="$3"
printf '@@CLAUDE_NOTIFY|%s|%s|%s@@\n' "$type" "$subtitle" "$message" > "$notify_tty" 2>/dev/null
}
case "$EVENT" in
Stop)
if [ -f "$stamp" ]; then
start_time=$(sed -n '1p' "$stamp")
pane_title=$(sed -n '2p' "$stamp")
now=$(date +%s)
elapsed=$((now - start_time))
rm -f "$stamp"
[ "$elapsed" -lt 120 ] && exit 0
mins=$((elapsed / 60))
secs=$((elapsed % 60))
send_marker "DONE" "${pane_title:-$project} -- ${mins}m ${secs}s" "${MESSAGE:-Task completed}"
fi
;;
PermissionRequest)
send_marker "APPROVAL" "$project" "Permission needed for: $TOOL_NAME"
;;
Elicitation)
send_marker "INPUT" "$project -- ${MCP_SERVER:-MCP}" "Tool $TOOL_NAME needs your input"
;;
esac
Make both executable:
chmod +x ~/.claude/hooks/mark-start.sh ~/.claude/hooks/notify.sh
2. Configure Claude Code on the remote server
Add the same hooks block to the remote ~/.claude/settings.json:
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/mark-start.sh"
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/notify.sh"
}
]
}
],
"PermissionRequest": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/notify.sh"
}
]
}
],
"Elicitation": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/notify.sh"
}
]
}
]
}
}
3. Create the local receiver script on your Mac
~/.claude/hooks/local-notify.sh
Called by the iTerm2 Trigger when it catches the marker pattern. Runs terminal-notifier locally.
#!/bin/bash
# Called by iTerm2 Trigger when it catches @@CLAUDE_NOTIFY|...|...|...@@
# Arguments: $1 = type (DONE/APPROVAL/INPUT), $2 = subtitle, $3 = message
type="$1"
subtitle="$2"
message="$3"
# Skip notification if iTerm is the frontmost app
frontmost=$(osascript -e 'tell application "System Events" to get name of first process whose frontmost is true' 2>/dev/null)
[ "$frontmost" = "iTerm2" ] && exit 0
terminal_notifier=$(which terminal-notifier)
[ -z "$terminal_notifier" ] && terminal_notifier="/opt/homebrew/bin/terminal-notifier"
case "$type" in
DONE)
"$terminal_notifier" \
-title "Claude Code -- Done" \
-subtitle "$subtitle" \
-message "$message" \
-activate com.googlecode.iterm2
;;
APPROVAL)
"$terminal_notifier" \
-title "Claude Code -- Needs Approval" \
-subtitle "$subtitle" \
-message "$message" \
-activate com.googlecode.iterm2
;;
INPUT)
"$terminal_notifier" \
-title "Claude Code -- Input Needed" \
-subtitle "$subtitle" \
-message "$message" \
-activate com.googlecode.iterm2
;;
esac
chmod +x ~/.claude/hooks/local-notify.sh
4. Configure the iTerm2 Trigger
This is the bridge that catches marker text flowing through tmux and runs the local script.
- Open iTerm2 > Settings > Profiles (select your profile) > Advanced
- Scroll to Triggers and click Edit
- Click + to add a new trigger with these values:
| Field | Value | |---|---| | Regular Expression | @@CLAUDE_NOTIFY\|([^|]*)\|([^|]*)\|([^|]*)@@ | | Action | Run Command... | | Parameters | $HOME/.claude/hooks/local-notify.sh "\1" "\2" "\3" | | Instant | Checked | | Enabled | Checked |
- Click Close
Important: The Instant checkbox must be checked so the trigger fires immediately when the marker text appears, without waiting for a newline or cursor movement.
Reference
Hook data available via stdin (JSON)
| Field | Available in | Description | |---|---|---| | session_id | All events | Unique session identifier | | cwd | All events | Current working directory | | hook_event_name | All events | Event type (Stop, PermissionRequest, etc.) | | last_assistant_message | Stop | Claude's final
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: extractumio
- Source: extractumio/extractum-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.