Back to skill

Security audit

agent-notify

Security checks for vulnerabilities and agentic risk

Overview

This skill does what it says, but it runs a persistent background notifier that reads agent session logs and sends task text plus device and host metadata to a WebSocket backend.

Review before installing. Use only with a trusted backend, prefer wss://, avoid putting long-lived auth codes in shell history, and understand that the daemon keeps watching local agent session files until stopped. Do not run selftest on a shared multi-user machine unless the temp-file issue is fixed.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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
Findings (4)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
notify.py:205
Finding
Excessive Collection and Transmission of Agent Conversation Data<![CDATA[ ## Vulnerability Details **File Location**: `notify.py:205-212`, `notify.py:271-282`, `notify.py:389-400`, and `notify.py:469-480` **Vulnerability Type**: Excessive access to conversation records and transmission to an unrestricted endpoint **Risk Level**: High ### Vulnerable Code ```python ADAPTERS = { "claude-code": {"glob": "~/.claude/projects/*/*.jsonl", "parse": _parse_claude}, "hermes": {"glob": "~/.hermes/sessions/session_*.json", "parse": _parse_hermes}, "codex": {"glob": "~/.codex/sessions/**/*.jsonl", "parse": _parse_jsonl_generic}, "openclaw": {"glob": "~/.openclaw/sessions/**/*.json*", "parse": _parse_jsonl_generic}, } ``` ```python ws = await asyncio.wait_for( websockets.connect(cfg["url"], proxy=cfg.get("proxy")), timeout ) hello = { "mac": cfg["device_id"], "code": cfg["auth_code"], "agent": cfg.get("agent", "unknown"), "host": os.uname().nodename, "v": 1, } await ws.send(json.dumps(hello)) ``` ```python for ev in changed: await ws.send(json.dumps(ev)) _log(f"推送 {ev['status']} | {ev['text']}") ``` ```python return { "type": "task_status", "agent": agent, "task": task, "status": status, "time": dt.isoformat(timespec="seconds"), "detail": detail, "text": TEMPLATE.format( agent=AGENT_CN.get(agent, agent), task=task, status=STATUS_CN.get(status, status), time=_spoken_time(dt), ), } ``` ### Technical Analysis The daemon recursively monitors session records belonging to supported AI agents and extracts recent user text as the task name. It then sends the extracted task text, task state, timestamp, agent identity, hostname, device identifier, and authorization code to the WebSocket URL supplied during setup. The destination is not restricted through an allowlist, trust policy, or certificate-pinning mechanism. There is also no content redaction or option to exclude task text from notifications. Although transmission is part of ...[truncated 1699 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Make transcript monitoring an explicit, separate opt-in capability rather than enabling it implicitly with daemon startup. 2. Default to manual notifications or status-only messages that do not contain conversation text. 3. Allow users to select the exact agent, project, and session files that may be monitored. 4. Display a clear confirmation identifying the directories read and the fields transmitted before starting the daemon. 5. Add a configuration option to omit `task`, `detail`, hostname, device identifier, and other unnecessary metadata. 6. Implement secret and sensitive-data redaction before constructing network payloads. 7. Restrict notification destinations through an approved-host policy or require explicit confirmation when the hostname changes. 8. Support certificate pinning or another strong endpoint-authentication mechanism for managed deployments. 9. Provide a preview mode showing the exact outbound payload before persistent monitoring begins. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
SKILL.md:21
Finding
Authorization Code and Task Data May Be Exposed Through Plaintext Transport and Process Arguments<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:21-33` and `notify.py:778` **Vulnerability Type**: Plaintext sensitive-data transmission and command-line secret exposure **Risk Level**: High ### Vulnerable Code ```bash python3 <SKILL_DIR>/notify.py setup \ --url ws://192.168.1.10:9004/agent/notify \ --device-id aa:bb:cc:dd:ee:ff \ --auth-code XXXX ``` ```python s.add_argument("--auth-code", required=True, help="后台签发的授权码") ``` The authorization code is subsequently included in the initial WebSocket frame: ```python hello = { "mac": cfg["device_id"], "code": cfg["auth_code"], "agent": cfg.get("agent", "unknown"), "host": os.uname().nodename, "v": 1, } await ws.send(json.dumps(hello)) ``` ### Technical Analysis The documented setup procedure recommends a `ws://` URL. Unlike `wss://`, plain WebSocket transport provides no TLS confidentiality, server authentication, or transport integrity. The authorization code, device identifier, hostname, and subsequent task notifications can therefore traverse the network in plaintext. The authorization code is also supplied through the `--auth-code` command-line option. Command-line arguments may be visible in process listings while the command runs and may be retained in shell history, terminal logs, automation logs, or support transcripts. Restricting the saved configuration file to mode `0600` protects the stored copy but does not address network interception or command-line disclosure. ### Attack Path **Network interception path:** 1. The user follows the documented example and configures a remote `ws://` endpoint. 2. The client sends the authorization code in the initial unencrypted WebSocket frame. 3. An attacker with access to the same wireless network, local network segment, router, proxy, or another observation point captures the frame. 4. The attacker obtains the authorization code, device metadata, and task messages. 5. Depending on server-side controls, the attack ...[truncated 945 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Require `wss://` for every non-loopback endpoint. 2. Reject remote `ws://` URLs during setup unless the user supplies an explicit unsafe-development override. 3. Retain `ws://` support only for clearly identified loopback self-tests, such as `127.0.0.1` or `::1`. 4. Validate TLS certificates and hostnames using the platform trust store. 5. Consider certificate or public-key pinning for managed notification servers. 6. Replace `--auth-code` with a hidden interactive prompt using `getpass.getpass()`. 7. For automation, accept the secret through a protected file descriptor, mode-`0600` secret file, or operating-system credential store rather than a command-line argument. 8. Warn users to rotate credentials previously supplied through shell history. 9. Document backend replay protection, credential revocation, and credential-rotation procedures. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
notify.py:681
Finding
Predictable Shared Temporary Script Enables Symlink Attacks and Local Code Execution<![CDATA[ ## Vulnerability Details **File Location**: `notify.py:681-684` **Vulnerability Type**: Insecure temporary-file creation and execution **Risk Level**: High ### Vulnerable Code ```python srv_py = os.path.join(tempfile.gettempdir(), "agent_notify_fake_server.py") with open(srv_py, "w") as f: f.write(code) srv = subprocess.Popen([sys.executable, srv_py, str(port)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) ``` ### Technical Analysis The self-test creates a Python file at a fixed, predictable name in the system-wide temporary directory and then executes that path. On typical Unix systems, `/tmp` is writable by every local user. The file is opened with ordinary write mode rather than exclusive creation. Consequently, an attacker can pre-create the path as a symbolic link, causing the caller to overwrite a different file that the caller is permitted to modify. There is also a race between closing the file and executing it: an attacker can replace the path after the legitimate content is written but before `subprocess.Popen()` opens it. The use of `tempfile.gettempdir()` does not make a fixed filename safe. Secure temporary-file handling requires an unpredictable private directory or atomic exclusive creation with appropriate permissions. ### Attack Path **Symlink overwrite path:** 1. A local attacker predicts the fixed path `/tmp/agent_notify_fake_server.py`. 2. The attacker creates that path as a symbolic link to another file writable by the victim. 3. The victim runs `python3 notify.py selftest`. 4. The `open(..., "w")` call follows the symbolic link and truncates or overwrites the target with the embedded test-server code. 5. The victim's file is corrupted and may later execute the injected content if it is a Python or startup-related file. **Code-execution race path:** 1. The victim runs the self-test. 2. After the application writes and closes the temporary script, a local attacker replaces the pr ...[truncated 743 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Create a private temporary directory using `tempfile.TemporaryDirectory()` and place the script inside it. 2. Ensure the temporary directory is accessible only to the current user, normally with mode `0700`. 3. Create the script with exclusive semantics and mode `0600`; do not reuse a predictable path in a shared directory. 4. Avoid closing and reopening a security-sensitive path where practical. On supported systems, execute through a safely retained file descriptor or eliminate the generated script entirely. 5. Prefer running the fake server in the existing process or through a multiprocessing target rather than writing executable source code to disk. 6. Ensure cleanup occurs in a `finally` block and that no executable temporary artifact remains after testing. 7. Add a regression test that pre-creates the historical path as a symlink and verifies that the self-test neither follows nor overwrites it. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
notify.py:488
Finding
Unverified PID File Can Cause Termination of an Unrelated Process<![CDATA[ ## Vulnerability Details **File Location**: `notify.py:488-500`, `notify.py:552-558`, and `notify.py:568-583` **Vulnerability Type**: Unsafe PID-file lifecycle and process identity validation **Risk Level**: Medium ### Vulnerable Code ```python def _pid_alive(pid): try: os.kill(pid, 0) return True except OSError: return False def read_pid(): try: with open(PIDFILE) as f: pid = int(f.read().strip()) return pid if _pid_alive(pid) else None except Exception: return None ``` ```python p = subprocess.Popen( [sys.executable, os.path.abspath(__file__), "_daemon"], stdout=log, stderr=log, stdin=subprocess.DEVNULL, start_new_session=True, ) with open(PIDFILE, "w") as f: f.write(str(p.pid)) ``` ```python pid = read_pid() if not pid: print("守护进程本来就没在跑") if os.path.exists(PIDFILE): os.remove(PIDFILE) return 0 os.kill(pid, signal.SIGTERM) for _ in range(20): if not _pid_alive(pid): break time.sleep(0.1) if _pid_alive(pid): os.kill(pid, signal.SIGKILL) if os.path.exists(PIDFILE): os.remove(PIDFILE) ``` ### Technical Analysis The process-control logic treats a PID as sufficient proof of daemon identity. `_pid_alive()` only verifies that a process with the recorded number exists; it does not verify the process executable, command line, start time, owner, or possession of a daemon lock. If the daemon exits unexpectedly, the PID file can remain behind. Operating systems eventually reuse process identifiers. Once the recorded PID belongs to a different process, `cmd_stop()` sends SIGTERM and, after a short wait, SIGKILL to that unrelated process. A forged PID file can produce the same condition when an attacker has write access to the configuration directory or PID file. The normal directory is under the user's home directory, which limits the most direct attack to actors with that user's filesystem access, but stale PID reuse ...[truncated 1263 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Use an advisory lock file held open for the entire daemon lifetime rather than relying only on a PID file. 2. Record the daemon's PID, executable path, command-line identity, and process start time. 3. Before sending a signal, verify that all recorded identity attributes still match the live process. 4. On Linux, compare `/proc/<pid>/exe`, `/proc/<pid>/cmdline`, and `/proc/<pid>/stat` start time against the stored daemon identity. 5. Delete the PID file in guaranteed daemon cleanup using `try/finally` and signal-aware shutdown. 6. Write the PID metadata atomically and ensure the containing directory and file are not writable by other users. 7. Refuse to send SIGKILL if daemon identity cannot be conclusively verified. 8. Consider using a local control socket with ownership checks for daemon shutdown instead of signaling a PID obtained from a file. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (5)

subprocess module call

Medium
Category
Dangerous Code Execution
Content
srv_py = os.path.join(tempfile.gettempdir(), "agent_notify_fake_server.py")
    with open(srv_py, "w") as f:
        f.write(code)
    srv = subprocess.Popen([sys.executable, srv_py, str(port)],
                           stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
    time.sleep(1.2)
    try:
Confidence
79% confidence
Finding
The selftest writes Python code to a predictable path in the system temp directory and then executes it. On a multi-user system, an attacker who can pre-create or race-modify that file could cause arbitrary code execution when selftest runs.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill instructs the agent to run shell commands, read environment-related state (for proxy troubleshooting), and write persistent configuration under ~/.agent-notify, but it does not declare permissions or clearly bound those capabilities. Undeclared capabilities increase the chance of users or host platforms authorizing the skill without understanding that it can persist state and launch a background process, which is a real transparency and consent issue even if the functionality is expected for this skill.

Vague Triggers

Medium
Confidence
88% confidence
Finding
The trigger list includes broad natural phrases such as '干完告诉我', '通知机器人', and '语音播报', which could match ordinary conversation and cause the skill to activate unintentionally. In this skill, unintended activation is more sensitive because activation can lead to persistent monitoring, configuration prompts, and starting a long-lived notifier daemon rather than a one-shot action.

Missing User Warnings

Medium
Confidence
97% confidence
Finding
Although the body later explains that the daemon keeps running and reads local session files, the skill description does not prominently warn users up front that it continuously monitors local conversation logs and persists after the agent exits. That omission undermines informed consent and can surprise users with ongoing local surveillance of session content and continued outbound notifications.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The skill transmits task names, status text, and host identity to a backend automatically once configured, without per-send confirmation or a clear runtime warning. Because task names are derived from conversation content, this can leak sensitive prompts, project names, or internal context to an external service.

Static analysis

Detected: suspicious.dynamic_code_execution

Dynamic code execution detected.

Critical
Code
suspicious.dynamic_code_execution
Location
test_scan.py:11