Back to skill

Security audit

Guardian — OpenClaw 7x24 Watchdog & Auto-Healer

Security checks for vulnerabilities and agentic risk

Overview

This watchdog is mostly purpose-aligned, but it installs persistent recurring automation that can repeatedly execute a workspace script and restart the OpenClaw gateway without strong safeguards.

Install only if you explicitly want a persistent watchdog that runs every five minutes and can restart the OpenClaw gateway. Review and preferably harden the script path, cron job, restart thresholds, process matching, and log path before enabling it; also verify you know how to disable or remove the cron job.

Vulnerability Patterns
  • System PersistenceInstalls backdoors, hooks, services, or scheduled tasks that survive the run
  • 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)

T06 · System Persistence

Error
Location
SKILL.md:22
Finding

Persistent Recurring Agent and Script Execution Through a Scheduled Cron Job

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/watchdog.py:25
Finding

Incorrect Cross-Platform Paths Cause Fail-Open Disk Monitoring and Misplaced Logs

Content
View full analysis
dict: """Check free disk space on workspace drive.""" drive = os.path.expandvars(r"%USERPROFILE%") try: usage = shutil.disk_usage(drive) free_gb = usage.free / (1024**3) return {"free_gb": free_gb, "ok": free_gb >= MIN_DISK_GB} except Exception as e: return {"free_gb": -1, "ok": True, "error": str(e)} ``` ### Technical Analysis The implementation uses Windows-style `%USERPROFILE%` environment-variable syntax on every platform. On Unix-like systems, `os.path.expandvars()` ordinarily expands `$VAR` or `${VAR}`, not `%VAR%`. Consequently, `%USERPROFILE%` can remain literal. The log path also uses `.qclaw`, while the Skill documentation promises `.openclaw`. This discrepancy means operators may inspect the documented path and find no logs even when logging is occurring elsewhere. The disk check compounds the path problem by returning `"ok": True` whenever `shutil.disk_usage()` raises an exception. This is a fail-open design: an inability to perform a security or reliability check is treated as a healthy result. Although the caller logs a warning when `free_gb` is negative, no issue is included in the result summary and no low-disk alert is generated. Module-level `os.makedirs(LOG_DIR, exist_ok=True)` also creates the computed path immediately. On an unsupported platform, this may create a relative directory with a literal `%USERPROFILE%` component under the process working directory. ### Attack Path 1. The watchdog runs on Linux or macOS. 2. `%USERPROFILE%` is not expanded into the user's home directory. 3. The module creates or attempts to use an uninte ...[truncated 1020 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/watchdog.py:104
Finding

Overbroad Process Matching Can Trigger Repeated Unnecessary Gateway Restarts

Content
View full analysis
dict: """Check memory usage of OpenClaw-related processes.""" system = platform.system() proc_name = "openclaw" if system == "Windows" else "openclaw" if system == "Windows": # Use tasklist to find OpenClaw processes code, out, err = run(["tasklist", "/FI", "IMAGENAME eq node.exe", "/FO", "CSV", "/NH"], timeout=10) mem_total = 0.0 if code == 0 and out: for line in out.splitlines(): line = line.strip().strip('"') parts = line.split('","') if len(parts) >= 5: mem_str = parts[4].strip().replace(" K", "").replace(",", "") try: mem_kb = float(mem_str) mem_total += mem_kb / 1024.0 # KB -> MB except ValueError: pass return {"memory_mb": mem_total, "processes": out} else: # Linux/macOS code, out, err = run(["pgrep", "-f", "openclaw"], timeout=5) if code != 0: return {"memory_mb": 0, "processes": ""} pids = [p.strip() for p in out.splitlines() if p.strip()] mem_total = 0.0 for pid in pids: code2, out2, _ = run(["ps", "-o", "rss=", "-p", pid], timeout=5) if code2 == 0 and out2: try: mem_total += float(out2.strip()) / 1024.0 # KB -> MB except ValueError: pass return {"memory_mb": mem_total, "pids": pids} ``` ```python if mem["memory_mb"] > MEM_KILL_MB: msg = f"CRITICAL: Memory usage {mem['memory_mb']:.0f} MB exceeds kill threshold ({MEM_KILL_MB} MB)" issues.append(("oom_risk", msg)) log(msg) ...[truncated 2096 chars]
Remediation
View remediation

other

Note
Location
scripts/watchdog.py:84
Finding

Advertised Zombie-Session Monitoring and Auto-Healing Are Not Implemented

Content
View full analysis
500 MB, kill+restart > 1 GB) - Zombie/stuck sessions (idle > 30 min without response) - Disk space on workspace drive Auto-heals: - Restart gateway on hang/crash/OOM - Kill zombie sessions - Logs every check to guardian.log ``` However, the implementation never identifies a zombie session: ```python def check_session_health(cli: str) -> list[dict]: """Find zombie/stuck sessions.""" code, out, err = run([cli, "session", "status"], timeout=15) if code != 0: return [] zombies = [] try: # session_status output is plain text; try JSON parse if it looks like JSON if out.strip().startswith("{"): data = json.loads(out) else: # Fallback: rely on sessions_list for zombie detection code2, out2, _ = run([cli, "session", "list"], timeout=15) if code2 == 0 and out2: # Simple heuristic: if sessions exist with very old lastActive, flag them pass return zombies except Exception: pass return zombies ``` The main watchdog flow performs gateway, memory, and disk checks but never calls `check_session_health()`: ```python # 1. Gateway status gs = check_gateway_status(cli) if not gs["alive"]: msg = f"Gateway unresponsive: {gs.get('error', 'unknown')}" issues.append(("gateway_dead", msg)) log(f"ISSUE: {msg}") restart_gateway(cli) else: log("OK: Gateway is responsive.") # 2. Memory check mem = check_memory() if mem["memory_mb"] > MEM_KILL_MB: msg = f"CRITICAL: Memory usage {mem['memor ...[truncated 2709 chars]
Remediation
View remediation
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • 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
Findings (6)

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding

The code largely matches the watchdog/auto-healer description for gateway health, memory, disk checks, logging, and gateway restart. However, a material part of the declared behavior is missing: session health checking is effectively a stub that always returns no zombies, and no session-kill action exists. Also, the script does not itself run every 5 minutes; it is intended to be invoked by cron or CLI. Because the declared description explicitly includes zombie session monitoring/cleanup and a concrete periodic cadence, but the supplied code does not implement those behaviors, this is a description-to-behavior mismatch.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding

The skill instructs the agent to copy files into a persistent workspace and create a recurring cron-driven agentTurn that executes a Python script, but it declares no explicit tool scope or permissions. That makes powerful file-write and shell-like capabilities implicit rather than bounded, increasing the chance of overreach, unsafe execution, or unauthorized persistence.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The skill promotes a 'zero manual intervention' watchdog that runs every five minutes and performs auto-healing, including restart actions, without prominently warning users about ongoing autonomous system changes. This is dangerous because users may enable persistence and recurring restarts without understanding that the agent will continue making operational changes after the initial setup.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The trigger list includes broad, common phrases such as 'watchdog', 'guardian', 'health monitor', and 'memory monitor', making accidental or unintended invocation plausible. In this skill’s context, unintended invocation is risky because activation can lead to persistent scheduling and automated restart behavior that changes system state.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
97% confidence
Finding

The skill explicitly instructs creation of a recurring isolated agentTurn job that repeatedly executes a workspace script every five minutes. Persistent autonomous execution materially increases risk because any flaw, later modification, or abuse of the referenced script can be re-triggered indefinitely, and the announce-only reporting pattern may hide normal activity except on error.

Content

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

(Use cp -r / copy; adjust the destination as needed. Keep the guardian/ folder in workspace.)

  1. Schedule via cron (5-minute interval): Use the cron tool to create a recurring agentTurn job:
    json
    {
      "schedule": { "kind": "every", "everyMs": 300000 },
    

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

python
def run(cmd: list[str], timeout: int = 15) -> tuple[int, str, str]:
    try:
        p = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, shell=False)
        return p.returncode, p.stdout.strip(), p.stderr.strip()
    except subprocess.TimeoutExpired:
        return -1, "", f"TIMEOUT after {timeout}s"

Static analysis

No suspicious patterns detected.