Back to skill

Security audit

Claude Notifications

Security checks for vulnerabilities and agentic risk

Overview

The skill is coherent with its notification purpose, but it persistently changes Claude, SSH, and macOS startup settings and has unsafe setup/listener behavior that merits user review.

Install only if you are comfortable with a skill changing Claude hook settings, SSH config, remote devpod files, and adding a persistent macOS LaunchAgent. Review the setup script first, use only trusted devpod hostnames, and consider fixing host validation, listener authentication, and port-conflict handling before running it.

Vulnerability Patterns
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/setup_notifications.py:406
Finding

Unvalidated Devpod Host Allows SSH Option Injection and Local Command Execution

Content
View full analysis
/dev/null 2>&1; then error "Cannot SSH to $host. Ensure the devpod is running." fi ssh "$host" "mkdir -p ~/.claude/scripts" scp "$SCRIPTS_DIR/notify.sh" "$host:~/.claude/scripts/notify.sh" ssh "$host" "chmod +x ~/.claude/scripts/notify.sh" ``` ### Technical Analysis The value supplied through `--devpod` is accepted without validation and passed to `ssh` as a positional argument. Using an argument array or shell quoting prevents shell metacharacter expansion, but it does not prevent the SSH client from interpreting a value beginning with `-` as a command-line option. A malicious host value can therefore inject SSH options. Of particular concern is `ProxyCommand`, which instructs the local SSH client to run a helper command. The command runs on the local Mac with the privileges of the user executing the setup script. The same unvalidated value is reused across multiple `ssh` and `scp` calls, increasing the available attack surface. This behavior is not necessary for the declared notification fun ...[truncated 1303 chars]
Remediation
View remediation
str: if not HOST_PATTERN.fullmatch(host): error("Invalid devpod SSH host or alias") return host ``` ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/notify-listener.py:25
Finding

Persistent Notification Listener Accepts Unauthenticated Requests

Content
View full analysis
MAX_BODY: self.send_response(413) self.end_headers() return data = json.loads(self.rfile.read(length)) if length else {} message = data.get("message", "Claude Code needs attention") title = data.get("title", "Claude Code") sound = data.get("sound", "Glass") bundle_id = data.get("bundle_id") cmd = [TERMINAL_NOTIFIER, "-message", message, "-title", title, "-sound", sound] if bundle_id: cmd.extend(["-activate", bundle_id]) result = subprocess.run(cmd, timeout=5, capture_output=True) if result.returncode != 0: print(f"terminal-notifier failed: {result.stderr.decode()}", file=sys.stderr, flush=True) self.send_response(500) else: self.send_response(200) ``` ### Technical Analysis The listener accepts any HTTP POST reaching `127.0.0.1:19876` and does not require an authentication token, validate an origin, or restrict callers. Although loopback binding prevents direct access through ordinary external network interfaces, it does not establish caller identity. The configured SSH `RemoteForward` exposes access to the listener through each configured devpod session. Consequently, any process on the local Mac—and potentially any process able to access the forwarded port on the devpod—can submit arbitrary notification text, titles, sounds, and activation bundle identifiers. The command is passe ...[truncated 1412 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/setup_notifications.py:225
Finding

Setup Terminates Unrelated Processes Based Solely on Port Ownership

Content
View full analysis
/dev/null || true)" [[ -n "$stale_pids" ]] && kill "$stale_pids" 2>/dev/null || true for _i in {1..10}; do lsof -ti :${NOTIFY_PORT} >/dev/null 2>&1 || break sleep 0.5 done ``` ### Technical Analysis The setup treats every process using port `19876` as a stale notification listener. It does not verify the process owner, executable path, launchd label, command line, or relationship to this Skill before sending `SIGTERM`. A fixed port can legitimately be occupied by an unrelated application. Terminating the process solely because it owns that port exceeds the minimum privileges and actions required to install the listener. The safe behavior is to stop the known launchd service and report any remaining conflict. The shell implementation also quotes the complete `lsof` output as one argument to `kill`. If multiple newline-separated PIDs are returned, this can behave incorrectly and make cleanup inconsistent. ### Attack Path 1. A legitimate user process starts listening on or otherwise using port `19876`. 2. The user or Agent runs the notification setup. 3. Setup unloads the known ...[truncated 669 chars]
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (82)

Agent Config Directory Access

High
Category
Agent Snooping
Confidence
91% confidence
Finding

The skill explicitly modifies ~/.claude/settings.json to install notification hooks, which is a sensitive agent configuration file that can affect future agent behavior. Persistent hook changes in the agent config directory are high-impact because they can survive beyond the current session and may be abused to run unintended commands whenever Claude emits notifications.

Content

Scanner excerpt · SKILL.md (reported line 49)May include surrounding context.

md
- Adds `RemoteForward 19876` to `~/.ssh/config` for each devpod
- Copies `notify.py` to each devpod and configures Claude Code hooks there
- Adds `set -g allow-passthrough on` to each devpod's `~/.tmux.conf`
- Configures Claude Code notification hooks in `~/.claude/settings.json`
- Sends a test notification

### 3. Remind user of manual steps

Agent Config Directory Access

High
Category
Agent Snooping
Confidence
85% confidence
Finding

Skill reads from agent configuration directories (.claude/, .codex/, .gemini/). These directories may contain API keys, personal settings, and other credentials that the skill has no legitimate need to access.

Content

Scanner excerpt · SKILL.md (reported line 86)May include surrounding context.

md
- **Do Not Disturb blocking**: Add "Script Editor" to Focus exceptions, or disable DND
- **Listener not running**: `launchctl list | grep claude` and `lsof -i :19876`
- **Restart listener**: `launchctl unload ~/Library/LaunchAgents/com.claude.notify-listener.plist && launchctl load ~/Library/LaunchAgents/com.claude.notify-listener.plist`
- **Listener logs**: `cat ~/.claude/logs/notify-listener.log`
- **Test locally**: `python3 ~/.claude/scripts/notify.py "test" --title "Claude Code" --sound "Glass"`
- **Test devpod tunnel**: From devpod tmux session: `python3 ~/.claude/scripts/notify.py "test" --title "Claude Code" --sound "Glass"`
- **Debug mode**: `DEBUG=1 python3 ~/.claude/scripts/notify.py "test"`

Agent Config Directory Access

High
Category
Agent Snooping
Confidence
90% confidence
Finding

The script accesses and modifies ~/.claude/settings.json on a remote host to install command hooks. Agent configuration files are security-sensitive because they can cause future command execution inside the agent environment, so writing to them should be treated as a privileged, high-trust operation.

Content

Scanner excerpt · scripts/setup-notifications.sh (reported line 195)May include surrounding context.

sh
ssh "$host" 'python3 -c "
import json, os

settings_path = os.path.expanduser(\"~/.claude/settings.json\")
os.makedirs(os.path.dirname(settings_path), exist_ok=True)

data = {}

Agent Config Directory Access

High
Category
Agent Snooping
Confidence
96% confidence
Finding

The script writes to ~/.claude/settings.json on remote systems to install command hooks that execute later. In an agent-skill context, modifying agent configuration is sensitive because it establishes future behavior and can persist execution pathways beyond the current session.

Content

Scanner excerpt · scripts/setup_notifications.py (reported line 300)May include surrounding context.

python
python_script = """
import json, os

settings_path = os.path.expanduser("~/.claude/settings.json")
os.makedirs(os.path.dirname(settings_path), exist_ok=True)

data = {}

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
87% confidence
Finding

The skill instructs execution of shell commands that install software, modify SSH configuration, write files under ~/.claude, and configure remote hosts, but it declares no explicit tool scope or permissions boundary. This increases the chance an agent can perform sensitive filesystem, shell, and network actions without transparent user review, which is dangerous for a skill that changes local persistence and remote connectivity.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
88% confidence
Finding

The explicit use of launchctl load demonstrates the skill is enabling persistence immediately, not just documenting a passive file location. In the context of this skill, the danger is elevated because the listener is part of an SSH reverse-tunnel notification path, so persistence combines with network-reachable behavior and could expand the attack surface if misconfigured.

Content

Scanner excerpt · SKILL.md (reported line 85)May include surrounding context.

md
- **No notifications showing**: Check System Settings > Notifications for both your terminal app AND "Script Editor" — both must have notifications enabled
- **Do Not Disturb blocking**: Add "Script Editor" to Focus exceptions, or disable DND
- **Listener not running**: `launchctl list | grep claude` and `lsof -i :19876`
- **Restart listener**: `launchctl unload ~/Library/LaunchAgents/com.claude.notify-listener.plist && launchctl load ~/Library/LaunchAgents/com.claude.notify-listener.plist`
- **Listener logs**: `cat ~/.claude/logs/notify-listener.log`
- **Test locally**: `python3 ~/.claude/scripts/notify.py "test" --title "Claude Code" --sound "Glass"`
- **Test devpod tunnel**: From devpod tmux session: `python3 ~/.claude/scripts/notify.py "test" --title "Claude Code" --sound "Glass"`

Session Persistence

Medium
Category
Rogue Agent
Confidence
88% confidence
Finding

The explicit use of launchctl load demonstrates the skill is enabling persistence immediately, not just documenting a passive file location. In the context of this skill, the danger is elevated because the listener is part of an SSH reverse-tunnel notification path, so persistence combines with network-reachable behavior and could expand the attack surface if misconfigured.

Content

Scanner excerpt · SKILL.md (reported line 85)May include surrounding context.

md
- **No notifications showing**: Check System Settings > Notifications for both your terminal app AND "Script Editor" — both must have notifications enabled
- **Do Not Disturb blocking**: Add "Script Editor" to Focus exceptions, or disable DND
- **Listener not running**: `launchctl list | grep claude` and `lsof -i :19876`
- **Restart listener**: `launchctl unload ~/Library/LaunchAgents/com.claude.notify-listener.plist && launchctl load ~/Library/LaunchAgents/com.claude.notify-listener.plist`
- **Listener logs**: `cat ~/.claude/logs/notify-listener.log`
- **Test locally**: `python3 ~/.claude/scripts/notify.py "test" --title "Claude Code" --sound "Glass"`
- **Test devpod tunnel**: From devpod tmux session: `python3 ~/.claude/scripts/notify.py "test" --title "Claude Code" --sound "Glass"`

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/notify-listener.py (reported line 48)May include surrounding context.

python
if bundle_id:
                cmd.extend(["-activate", bundle_id])

            result = subprocess.run(cmd, timeout=5, capture_output=True)
            if result.returncode != 0:
                print(f"terminal-notifier failed: {result.stderr.decode()}", file=sys.stderr, flush=True)
                self.send_response(500)

Internal Network Request

Medium
Category
Server-Side Request Forgery
Confidence
70% confidence
Finding

Code issues a request to a loopback, link-local, or private-range host. This can reach internal services not meant to be exposed and is a common SSRF pivot.

Content

Scanner excerpt · scripts/notify.py (reported line 46)May include surrounding context.

python
debug(f"Sending via reverse tunnel on port {port}")

    try:
        req = urllib.request.Request(
            f"http://127.0.0.1:{port}",
            data=payload.encode("utf-8"),
            headers={"Content-Type": "application/json"},

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/notify.py (reported line 123)May include surrounding context.

python
if bundle_id:
        cmd.extend(["-activate", bundle_id])

    subprocess.run(cmd, check=True)


def notify(message: str, title: str = "Claude Code", sound: str = "Glass") -> None:

External Transmission

Medium
Category
Data Exfiltration
Confidence
70% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/notify.sh (reported line 32)May include surrounding context.

sh
local payload
    payload=$(python3 -c "import json,sys;print(json.dumps({'message':sys.argv[1],'title':sys.argv[2],'sound':sys.argv[3]}))" "$MESSAGE" "$TITLE" "$SOUND")
    debug "Sending via reverse tunnel on port $NOTIFY_PORT"
    if curl -s -m 3 -X POST "http://127.0.0.1:${NOTIFY_PORT}" \
        -H "Content-Type: application/json" \
        -d "$payload" >/dev/null 2>&1; then
        debug "Notification sent via tunnel"

Session Persistence

Medium
Category
Rogue Agent
Confidence
75% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · scripts/setup-notifications.sh (reported line 17)May include surrounding context.

sh
CLAUDE_DIR="$HOME/.claude"
SCRIPTS_DIR="$CLAUDE_DIR/scripts"
LOGS_DIR="$CLAUDE_DIR/logs"
PLIST_PATH="$HOME/Library/LaunchAgents/com.claude.notify-listener.plist"
PYTHON3="$(command -v python3 || true)"
[[ -z "$PYTHON3" ]] && error "python3 is required but not found. Install via: brew install python3"

Session Persistence

Medium
Category
Rogue Agent
Confidence
75% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · scripts/setup-notifications.sh (reported line 48)May include surrounding context.

sh
CLAUDE_DIR="$HOME/.claude"
SCRIPTS_DIR="$CLAUDE_DIR/scripts"
LOGS_DIR="$CLAUDE_DIR/logs"
PLIST_PATH="$HOME/Library/LaunchAgents/com.claude.notify-listener.plist"
PYTHON3="$(command -v python3 || true)"
[[ -z "$PYTHON3" ]] && error "python3 is required but not found. Install via: brew install python3"

Session Persistence

Medium
Category
Rogue Agent
Confidence
75% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · scripts/setup-notifications.sh (reported line 50)May include surrounding context.

sh
CLAUDE_DIR="$HOME/.claude"
SCRIPTS_DIR="$CLAUDE_DIR/scripts"
LOGS_DIR="$CLAUDE_DIR/logs"
PLIST_PATH="$HOME/Library/LaunchAgents/com.claude.notify-listener.plist"
PYTHON3="$(command -v python3 || true)"
[[ -z "$PYTHON3" ]] && error "python3 is required but not found. Install via: brew install python3"

Session Persistence

Medium
Category
Rogue Agent
Confidence
75% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · scripts/setup-notifications.sh (reported line 77)May include surrounding context.

sh
CLAUDE_DIR="$HOME/.claude"
SCRIPTS_DIR="$CLAUDE_DIR/scripts"
LOGS_DIR="$CLAUDE_DIR/logs"
PLIST_PATH="$HOME/Library/LaunchAgents/com.claude.notify-listener.plist"
PYTHON3="$(command -v python3 || true)"
[[ -z "$PYTHON3" ]] && error "python3 is required but not found. Install via: brew install python3"

Session Persistence

Medium
Category
Rogue Agent
Confidence
75% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · scripts/setup-notifications.sh (reported line 81)May include surrounding context.

sh
CLAUDE_DIR="$HOME/.claude"
SCRIPTS_DIR="$CLAUDE_DIR/scripts"
LOGS_DIR="$CLAUDE_DIR/logs"
PLIST_PATH="$HOME/Library/LaunchAgents/com.claude.notify-listener.plist"
PYTHON3="$(command -v python3 || true)"
[[ -z "$PYTHON3" ]] && error "python3 is required but not found. Install via: brew install python3"

Session Persistence

Medium
Category
Rogue Agent
Confidence
75% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · scripts/test_setup_notifications.py (reported line 99)May include surrounding context.

python
CLAUDE_DIR="$HOME/.claude"
SCRIPTS_DIR="$CLAUDE_DIR/scripts"
LOGS_DIR="$CLAUDE_DIR/logs"
PLIST_PATH="$HOME/Library/LaunchAgents/com.claude.notify-listener.plist"
PYTHON3="$(command -v python3 || true)"
[[ -z "$PYTHON3" ]] && error "python3 is required but not found. Install via: brew install python3"

Session Persistence

Medium
Category
Rogue Agent
Confidence
75% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · scripts/test_setup_notifications.py (reported line 235)May include surrounding context.

python
CLAUDE_DIR="$HOME/.claude"
SCRIPTS_DIR="$CLAUDE_DIR/scripts"
LOGS_DIR="$CLAUDE_DIR/logs"
PLIST_PATH="$HOME/Library/LaunchAgents/com.claude.notify-listener.plist"
PYTHON3="$(command -v python3 || true)"
[[ -z "$PYTHON3" ]] && error "python3 is required but not found. Install via: brew install python3"

Session Persistence

Medium
Category
Rogue Agent
Confidence
75% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · scripts/test_setup_notifications.py (reported line 282)May include surrounding context.

python
CLAUDE_DIR="$HOME/.claude"
SCRIPTS_DIR="$CLAUDE_DIR/scripts"
LOGS_DIR="$CLAUDE_DIR/logs"
PLIST_PATH="$HOME/Library/LaunchAgents/com.claude.notify-listener.plist"
PYTHON3="$(command -v python3 || true)"
[[ -z "$PYTHON3" ]] && error "python3 is required but not found. Install via: brew install python3"

Session Persistence

Medium
Category
Rogue Agent
Confidence
75% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · scripts/setup-notifications.sh (reported line 17)May include surrounding context.

sh
CLAUDE_DIR="$HOME/.claude"
SCRIPTS_DIR="$CLAUDE_DIR/scripts"
LOGS_DIR="$CLAUDE_DIR/logs"
PLIST_PATH="$HOME/Library/LaunchAgents/com.claude.notify-listener.plist"
PYTHON3="$(command -v python3 || true)"
[[ -z "$PYTHON3" ]] && error "python3 is required but not found. Install via: brew install python3"

Session Persistence

Medium
Category
Rogue Agent
Confidence
75% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · scripts/setup-notifications.sh (reported line 50)May include surrounding context.

sh
CLAUDE_DIR="$HOME/.claude"
SCRIPTS_DIR="$CLAUDE_DIR/scripts"
LOGS_DIR="$CLAUDE_DIR/logs"
PLIST_PATH="$HOME/Library/LaunchAgents/com.claude.notify-listener.plist"
PYTHON3="$(command -v python3 || true)"
[[ -z "$PYTHON3" ]] && error "python3 is required but not found. Install via: brew install python3"

Session Persistence

Medium
Category
Rogue Agent
Confidence
75% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · scripts/setup-notifications.sh (reported line 51)May include surrounding context.

sh
CLAUDE_DIR="$HOME/.claude"
SCRIPTS_DIR="$CLAUDE_DIR/scripts"
LOGS_DIR="$CLAUDE_DIR/logs"
PLIST_PATH="$HOME/Library/LaunchAgents/com.claude.notify-listener.plist"
PYTHON3="$(command -v python3 || true)"
[[ -z "$PYTHON3" ]] && error "python3 is required but not found. Install via: brew install python3"

Session Persistence

Medium
Category
Rogue Agent
Confidence
75% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · scripts/setup-notifications.sh (reported line 76)May include surrounding context.

sh
CLAUDE_DIR="$HOME/.claude"
SCRIPTS_DIR="$CLAUDE_DIR/scripts"
LOGS_DIR="$CLAUDE_DIR/logs"
PLIST_PATH="$HOME/Library/LaunchAgents/com.claude.notify-listener.plist"
PYTHON3="$(command -v python3 || true)"
[[ -z "$PYTHON3" ]] && error "python3 is required but not found. Install via: brew install python3"

Session Persistence

Medium
Category
Rogue Agent
Confidence
75% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · scripts/setup-notifications.sh (reported line 78)May include surrounding context.

sh
CLAUDE_DIR="$HOME/.claude"
SCRIPTS_DIR="$CLAUDE_DIR/scripts"
LOGS_DIR="$CLAUDE_DIR/logs"
PLIST_PATH="$HOME/Library/LaunchAgents/com.claude.notify-listener.plist"
PYTHON3="$(command -v python3 || true)"
[[ -z "$PYTHON3" ]] && error "python3 is required but not found. Install via: brew install python3"

Session Persistence

Medium
Category
Rogue Agent
Confidence
75% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · scripts/test_setup_notifications.py (reported line 86)May include surrounding context.

python
CLAUDE_DIR="$HOME/.claude"
SCRIPTS_DIR="$CLAUDE_DIR/scripts"
LOGS_DIR="$CLAUDE_DIR/logs"
PLIST_PATH="$HOME/Library/LaunchAgents/com.claude.notify-listener.plist"
PYTHON3="$(command -v python3 || true)"
[[ -z "$PYTHON3" ]] && error "python3 is required but not found. Install via: brew install python3"