Back to skill

Security audit

Event-Watcher

Security checks for vulnerabilities and agentic risk

Overview

This skill has a coherent event-watcher purpose, but it can automatically route untrusted events into OpenClaw sessions while reading broad local session metadata and storing sensitive payloads in plaintext logs.

Install only if you are comfortable running a long-lived watcher that can send event-derived text into OpenClaw sessions and configured chat destinations. Prefer explicit session_id or an explicitly scoped OPENCLAW_SESSION_STORE, set disable_session_store_lookup: true when possible, restrict Redis/webhook sources and reply targets, keep add_source_preamble enabled, avoid aggregate templates that include raw payloads, store logs/state in a private directory, redact payloads, and pin reviewed dependency versions before production use.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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
Findings (5)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/watcher.py:314
Finding

Aggregate delivery bypasses the prompt-injection safety preamble

Content
View full analysis
str: if not template: return f"Aggregated {len(events)} events. Last: {events[-1].get('event_id')}" out = template out = out.replace("{{count}}", str(len(events))) out = out.replace("{{first_event_id}}", str(events[0].get("event_id"))) out = out.replace("{{last_event_id}}", str(events[-1].get("event_id"))) out = out.replace("{{last_payload}}", str(events[-1].get("payload"))) return out def flush_aggregate(w, state, metrics, state_path: str): agg = w.get("aggregate", {}) window = int(agg.get("window_seconds", 0)) if window <= 0: return name = w.get("name") store = state.setdefault("aggregates", {}) entry = store.get(name) if not entry: return now = time.time() if now - entry.get("window_start", now) < window: return events = entry.get("events", []) if not events: return wake = w.get("wake", {}) session_key = wake.get("session_key") or OPENCLAW_SESSION_KEY if not session_key: return message = render_aggregate_template(agg.get("message_template", ""), events) timeout = int(w.get("ack_timeout_seconds", 30)) ok = send_to_openclaw(session_key, message, timeout) ``` Regular event processing applies a safety preamble, but the aggregate path does not: ```python message = render_template(resolve_message_template(wake), event) if wake.get("add_source_preamble", True): message = build_source_preamble(event) + message ``` ### Technical Analysis The aggregate renderer directly replaces `{{last_payload}}` with the string representation of event-controlled content. The resulting message is passed to `openclaw a ...[truncated 1739 chars]
Remediation
View remediation

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/watcher.py:41
Finding

Watcher enumerates and reads session stores belonging to other local agents

Content
View full analysis
list[str]: override = os.environ.get("OPENCLAW_SESSION_STORE") if override: return [override] home = os.path.expanduser("~") candidates = [ os.path.join(home, ".openclaw", "sessions", "sessions.json"), ] # dynamic agent session stores (e.g., ~/.openclaw/agents/*/sessions/sessions.json) agents_dir = os.path.join(home, ".openclaw", "agents") if os.path.isdir(agents_dir): try: for agent in os.listdir(agents_dir): candidate = os.path.join(agents_dir, agent, "sessions", "sessions.json") candidates.append(candidate) except Exception: pass return candidates def _select_session_store_path() -> str | None: paths = _candidate_session_store_paths() existing = [p for p in paths if os.path.exists(p)] if not existing: return None # prefer most recently updated file try: existing.sort(key=lambda p: os.path.getmtime(p), reverse=True) except Exception: pass return existing[0] def load_session_store() -> dict: store_path = _select_session_store_path() if not store_path: return {} try: with open(store_path, "r") as f: return json.load(f) or {} except Exception: return {} def resolve_session_id(wake: dict) -> str | None: # explicit override if wake.get("session_id"): return wake.get("session_id") # optional: disable session store lookup for privacy if wake.get("disable_session_store_lookup") is True: return None store = load_session_store() # session_key lookup key = wake.get("session_key") if key and key in store: re ...[truncated 2318 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/watcher.py:254
Finding

Sensitive event payloads, prompts, and session metadata are persisted in plaintext

Content
View full analysis
None: entry = { "event_id": event.get("event_id"), "reason": reason, "last_attempt": utc_now(), "payload": event, } with open(DEAD_LETTER, "a") as f: f.write(json.dumps(entry) + "\n") ``` Prompt previews are retained in the event log: ```python def message_preview(message: str, limit: int = 500) -> dict: if message is None: return {"message_preview": "", "message_len": 0, "message_truncated": False} msg = str(message) truncated = len(msg) > limit preview = msg[:limit] + ("…" if truncated else "") return {"message_preview": preview, "message_len": len(msg), "message_truncated": truncated} def log_event(watcher: str, stage: str, event: dict, extra: dict | None = None) -> None: entry = { "ts": utc_now(), "watcher": watcher, "stage": stage, "event_id": event.get("event_id"), "source": event.get("source"), "topic": event.get("topic"), } if extra: entry.update(extra) with open(EVENT_LOG, "a") as f: f.write(json.dumps(entry) + "\n") ``` Complete aggregate events are saved in the state file: ```python def add_aggregate(w, state, event, state_path: str) -> None: agg = w.get("aggregate", {}) window = int(agg.get("window_seconds", 0)) if window <= 0: return name = w.get("name") store = state.setdefault("aggregates", {}) now = time.time() entry = store.get(name) if not entry or now - entry.get("window_start", now) >= window: entry = {"window_ ...[truncated 2399 chars]
Remediation
View remediation

T08 · Insecure Dependencies

Warning
Location
scripts/requirements.txt:1
Finding

Python dependencies are unpinned and lack integrity verification

Content
View full analysis
Remediation
View remediation

other

Note
Location
scripts/watcher.py:649
Finding

Webhook processing uses the delivery method variable before initialization

Content
View full analysis
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (23)

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

The declared description promises a concrete event-watching capability with multiple behaviors and integrations. The supplied code chunk does not implement any of those functions; it is only an empty package initializer containing a comment. Because the actual code shown lacks the primary functionality described, the description does not accurately represent the supplied code chunk.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding

The declared description presents this skill as an event watcher/dispatcher that monitors event sources, matches relevant events, and wakes or routes agents accordingly. The provided code instead acts as a webhook event emitter/logger: it builds an event object from provided input and appends it to a JSONL file. It does not subscribe to Redis Streams, monitor webhook logs, filter events, deduplicate, retry, or interact with session routing mechanisms. This is a material description-behavior mismatch in primary purpose and implemented capabilities.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding

The skill advertises operational behavior that inherently requires file, environment, and shell-adjacent actions, but it declares no explicit tool scope or permissions boundary. In an agent ecosystem, that increases the chance the agent will invoke broader capabilities than intended, enabling unintended file access, process execution, or environment exposure during setup or runtime.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
65% 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 · references/CONFIG.md (reported line 186)May include surrounding context.

macOS / Linux

bash
nohup python3 {baseDir}/scripts/watcher.py --config {baseDir}/config/event_watcher.yaml \
  > {baseDir}/logs/watcher.log 2>&1 &

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

The skill scans OpenClaw session-store files in the user's home directory and automatically selects recent sessions for routing. That creates cross-session data access and action-routing risk, because an event source can influence which existing conversation receives data without explicit user consent, potentially exposing private context or causing actions in the wrong session.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

The skill reads session-store data and persists watcher state without any disclosure or consent mechanism. While not an exploit primitive by itself, in an agent environment this is a real privacy and transparency issue because prior-session metadata can be silently consumed to influence routing decisions and persisted state may reveal operational history.

Content

No source excerpt is available for this finding.

Tainted flow: 'path' from os.environ.get (line 406, credential/environment) → open (file write)

Medium
Category
Data Flow
Confidence
82% confidence
Finding

The state file path is controllable via environment-derived input and is written without validation, allowing arbitrary file overwrite within the permissions of the running user. In an agent-skill context, this can be abused to clobber user files, alter application state, or redirect writes into sensitive locations via symlinks or attacker-set paths.

Content

Scanner excerpt · scripts/watcher.py (reported line 142)May include surrounding context.

python
def save_state(path: str, state: dict) -> None:
    with open(path, "w") as f:
        json.dump(state, f, indent=2)

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The skill transmits event-derived content to external OpenClaw messaging/agent interfaces without any user-facing warning or approval boundary. In this specific watcher context, automated forwarding of untrusted and potentially sensitive event data increases the risk of unintended disclosure, cross-channel propagation, and unsafe downstream processing.

Content

No source excerpt is available for this finding.

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/watcher.py (reported line 155)May include surrounding context.

python
if reply_to:
            cmd += ["--reply-to", reply_to]
    try:
        subprocess.run(cmd, check=True, timeout=timeout + 5)
        return True
    except Exception:
        return False

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/watcher.py (reported line 200)May include surrounding context.

python
"--json",
    ]
    try:
        proc = subprocess.run(
            cmd,
            check=True,
            timeout=timeout + 10,

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
86% confidence
Finding

This code adds outbound messaging capability beyond passive event watching by sending agent-generated replies to arbitrary channels and targets. Because event content can trigger agent execution and reply delivery, the skill can become an exfiltration or spam bridge if filters, sessions, or destinations are misconfigured or attacker-influenced.

Content

No source excerpt is available for this finding.

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/watcher.py (reported line 248)May include surrounding context.

python
"--json",
    ]
    try:
        subprocess.run(cmd, check=True, timeout=timeout + 10)
        return True
    except Exception:
        return False

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

Dead-letter logging stores full event payloads locally, and the surrounding code also logs event metadata and message previews. Since event payloads may contain secrets, identifiers, or user content, silent persistence to local JSONL files creates a meaningful confidentiality risk and expands the impact of any local compromise.

Content

No source excerpt is available for this finding.

Ssd 3

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The code persists payload-bearing dead-letter entries, logs message previews derived from event content, and can include event payload content in aggregate templates and downstream agent messages. This creates multiple avenues for sensitive user-provided data to be stored, summarized, and retransmitted beyond the original event source, magnifying confidentiality exposure.

Content

No source excerpt is available for this finding.

Tainted flow: 'DEAD_LETTER' from os.environ.get (line 22, credential/environment) → open (file write)

Medium
Category
Data Flow
Confidence
84% confidence
Finding

The dead-letter log path comes from an environment variable and is opened for append without validation or confinement. An attacker controlling the environment or launch context could redirect sensitive event payloads into unintended files or poison user/application files with appended JSONL content.

Content

Scanner excerpt · scripts/watcher.py (reported line 261)May include surrounding context.

python
"last_attempt": utc_now(),
        "payload": event,
    }
    with open(DEAD_LETTER, "a") as f:
        f.write(json.dumps(entry) + "\n")

Tainted flow: 'EVENT_LOG' from os.environ.get (line 23, credential/environment) → open (file write)

Medium
Category
Data Flow
Confidence
84% confidence
Finding

The event log destination is environment-controlled and receives event metadata and message previews, creating an arbitrary file-write and data-leak surface. In this skill, events are untrusted and may contain sensitive content, so redirectable logging materially increases confidentiality and integrity risk.

Content

Scanner excerpt · scripts/watcher.py (reported line 299)May include surrounding context.

python
}
    if extra:
        entry.update(extra)
    with open(EVENT_LOG, "a") as f:
        f.write(json.dumps(entry) + "\n")

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

The skill supports prompt-file based instruction indirection, allowing configuration to inject extra handling instructions from arbitrary file paths into agent-facing messages. In this context, that expands the trust boundary from event routing into local file-driven prompt manipulation, which can quietly alter downstream agent behavior and facilitate prompt injection or disclosure of local path information.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
85% confidence
Finding

The script appends arbitrary webhook payloads directly to a local JSONL file, which can expose sensitive data contained in webhook bodies such as tokens, personal data, or internal event contents. In this skill context, the behavior is core to the feature, but the absence of any minimization, redaction, retention control, or clear disclosure increases the risk of unintended data leakage through local logs.

Content

No source excerpt is available for this finding.

Vague Triggers

Low
Category
Not specified by scanner
Confidence
89% confidence
Finding

The statement "Natural language can be translated by the agent into JSON" describes an activation/behavior path using unrestricted natural language without specifying allowed phrases, scope, or exclusion conditions. In a markdown spec, this can lead to ambiguous invocation conditions and unintended translations from ordinary user text.

Content

No source excerpt is available for this finding.

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
98% confidence
Finding

The dependency 'redis' is unpinned, so installs may resolve to different versions over time, including versions with known security defects or breaking behavior. In a skill that subscribes to external event sources and likely maintains network connections, this increases supply-chain and patch-state uncertainty.

Content

Scanner excerpt · scripts/requirements.txt (reported line 1)May include surrounding context.

text
redis
pyyaml

Unverifiable Dependency: redis has 4 known advisory(ies) (CVE-2023-28858 (redis-py Race Condition vulnerability); CVE-2023-28859 (redis-py Race Condition due to incomplete fix); CVE-2023-28858 (redis-py before 4.5.3, as used in ChatGPT and other products, leaves a connectio) +1 more), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
94% confidence
Finding

The manifest does not specify which 'redis' version is installed, so the package may resolve to a release affected by known redis-py advisories. Because this skill appears to manage event subscriptions and connection handling, use of a vulnerable client library could expose reliability or security issues in long-lived connections or request routing.

Content

No source excerpt is available for this finding.

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
99% confidence
Finding

The dependency 'pyyaml' is unpinned, making builds non-reproducible and leaving it unclear whether a vulnerable release could be installed. This is especially relevant because PyYAML has a history of unsafe parsing issues, and this skill handles webhook/JSONL-style event data where configuration or payload parsing mistakes could become security-relevant.

Content

Scanner excerpt · scripts/requirements.txt (reported line 2)May include surrounding context.

text
redis
pyyaml

Unverifiable Dependency: pyyaml has 8 known advisory(ies) (CVE-2019-20477 (Deserialization of Untrusted Data in PyYAML); CVE-2020-1747 (Improper Input Validation in PyYAML); CVE-2020-14343 (Improper Input Validation in PyYAML) +5 more), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
98% confidence
Finding

The manifest leaves the installed 'pyyaml' version unverifiable despite multiple known PyYAML advisories, including historically serious deserialization issues. In a component that processes external event-driven inputs, an unsafe or vulnerable YAML library version can become more dangerous if any untrusted YAML configuration or payloads are ever parsed.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.