Back to skill

Security audit

Somnia

Security checks for vulnerabilities and agentic risk

Overview

Somnia is a mostly disclosed nightly skill-review tool, but it needs review because it can create a recurring macOS job, optionally send review summaries to Telegram, and has an uncontained proposal-file write risk.

Install only if you want a local scheduled OpenClaw skill review tool. Before enabling it, keep the default plan-only behavior, review any LaunchAgent --apply action, avoid Telegram reporting unless you accept sending review metadata to Telegram, and fix or constrain proposal output paths so skill names cannot write outside the intended proposals directory.

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 (1)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/nightly_skill_review.py:178
Finding

Arbitrary Proposal-Path Write Through Unsanitized Skill Names

Content
View full analysis
dict[str, Path]: result = {} if scope == "all" and target_root.exists(): for path in sorted(target_root.iterdir()): if path.name.startswith("."): continue if (path / "SKILL.md").exists(): result[path.name] = path.resolve() if scope == "managed": for name, record in manifest.get("installs", {}).items(): target = Path(record.get("target", "")).expanduser() source = Path(record.get("source", "")).expanduser() for candidate in [target, source, target_root / name]: if candidate and (candidate / "SKILL.md").exists(): result.setdefault(name, candidate.resolve()) break own_skill = target_root / "skill-forge" if (own_skill / "SKILL.md").exists(): result.setdefault("skill-forge", own_skill.resolve()) if scope == "feedback": for entry in feedback_entries: name = entry.get("skill") if not name or name in result: continue target = target_root / name if (target / "SKILL.md").exists(): result[name] = target.resolve() return result ``` The unvalidated name is then used as a path component for proposal output: ```python def write_update_proposal(skill: str, skill_path: Path, issues: list[str], feedback: dict, args: argparse.Namespace) -> dict: if not args.propose_updates: return {"status": "disabled"} if not issu ...[truncated 3577 chars]
Remediation
View remediation
str: if not isinstance(name, str) or not SAFE_SKILL_NAME.fullmatch(name): raise ValueError("Invalid skill name") if name in {".", ".."}: raise ValueError("Invalid skill name") return name ``` 2. Reject names that are absolute or contain path separators, `.` components, or `..` components. 3. Resolve the final destination and verify containment before creating directories or writing files: ```python output_root = Path(args.update_output).expanduser().resolve() proposal_root = (output_root / date_stamp() / validated_skill).resolve() if proposal_root != output_root and output_root not in proposal_root.parents: raise ValueError("Proposal path escapes the configured output root") ``` 4. Apply validation consistently to names from both feedback entries and manifest keys. 5. Use exclusive creation or an atomic temporary-file replacement strategy where overwriting an existing proposal is not required. 6. Record rejected skill identifiers in the local report without reproducing potentially sensitive or malicious values in externally delivered Telegram messages. 7. Add regression tests covering absolute paths, `../` traversal, nested separators, empty values, non-string JSON values, and valid identifiers. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (33)

Tainted flow: 'request' from os.getenv (line 243, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/nightly_skill_review.py (reported line 249)May include surrounding context.

python
headers={"Content-Type": "application/json"},
        method="POST",
    )
    with urllib.request.urlopen(request, timeout=30) as response:
        payload = json.loads(response.read().decode("utf-8"))
    return {"sent": True, "status": "sent", "message_id": payload.get("result", {}).get("message_id")}

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
- `scripts/nightly_skill_review.py`

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · references/schedule-and-policy.md (reported line 22)May include surrounding context.

md
## Reporting Policy

User-facing reports should summarize outcomes and paths. They should not include raw hidden tests, simulated prompts, real secrets, access tokens, or unredacted feedback text.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding

The skill references executable scripts and describes operational behaviors that imply shell, filesystem, environment, and possibly network access, but it does not declare any explicit tool scope such as permissions or allowed-tools. That creates an authorization gap: a runtime may grant broader capabilities than intended, and reviewers or policy engines cannot reliably enforce least privilege for this skill.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The manifest describes local nightly health reviews, replay availability checks, feedback triage, and proposal-only maintenance reports. This function reads Telegram credentials from environment variables and performs an outbound HTTP POST to the Telegram Bot API, which adds an external messaging capability not stated in the skill purpose and not required for generating safe maintenance proposals.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The Telegram report sends generated review content off-host to an external service, but the code provides no inline disclosure, consent prompt, or content minimization beyond truncation. Because the markdown report includes per-skill findings, feedback totals, and issue summaries, enabling this flag can leak operational or potentially sensitive review metadata to a third-party chat destination.

Content

No source excerpt is available for this finding.

Ssd 3

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The markdown report contains per-skill status, validation scores, issue labels, and feedback summaries, and this content is transmitted to Telegram when enabled. Even if details are partially hidden for replay data, the report still exports internal review metadata and user-derived feedback aggregates to an external service, creating a real data-disclosure risk.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% 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/nightly_skill_review.py (reported line 244)May include surrounding context.

python
text = markdown if len(markdown) <= 3900 else markdown[:3800].rstrip() + "\n\n[truncated]"
    body = json.dumps({"chat_id": chat_id, "text": text, "disable_web_page_preview": True}).encode("utf-8")
    request = urllib.request.Request(
        f"https://api.telegram.org/bot{token}/sendMessage",
        data=body,
        headers={"Content-Type": "application/json"},
        method="POST",

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

python
#!/usr/bin/env python3
import argparse
import json
import plistlib
import subprocess
import sys
from pathlib import Path

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

python
#!/usr/bin/env python3
import argparse
import json
import plistlib
import subprocess
import sys
from pathlib import Path

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

python
#!/usr/bin/env python3
import argparse
import json
import plistlib
import subprocess
import sys
from pathlib import Path

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

python
#!/usr/bin/env python3
import argparse
import json
import plistlib
import subprocess
import sys
from pathlib import Path

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

python
#!/usr/bin/env python3
import argparse
import json
import plistlib
import subprocess
import sys
from pathlib import Path

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

python
#!/usr/bin/env python3
import argparse
import json
import plistlib
import subprocess
import sys
from pathlib import Path

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

python
#!/usr/bin/env python3
import argparse
import json
import plistlib
import subprocess
import sys
from pathlib import Path

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

python
#!/usr/bin/env python3
import argparse
import json
import plistlib
import subprocess
import sys
from pathlib import Path

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

python
#!/usr/bin/env python3
import argparse
import json
import plistlib
import subprocess
import sys
from pathlib import Path

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

python
#!/usr/bin/env python3
import argparse
import json
import plistlib
import subprocess
import sys
from pathlib import Path

Session Persistence

Medium
Category
Rogue Agent
Confidence
90% confidence
Finding

The default PLIST constant is part of a built-in persistence mechanism targeting the user's LaunchAgents directory. Hardcoding a ready-to-install persistence location lowers friction for creating recurring execution and is therefore security-relevant.

Content

Scanner excerpt · scripts/schedule_nightly_review.py (reported line 12)May include surrounding context.

python
BASE_DIR = Path(__file__).resolve().parent
DEFAULT_LABEL = "com.cheeseclaw.somnia-nightly"
DEFAULT_PLIST = Path.home() / "Library" / "LaunchAgents" / f"{DEFAULT_LABEL}.plist"


def program_arguments(args: argparse.Namespace) -> list[str]:

Session Persistence

Medium
Category
Rogue Agent
Confidence
90% confidence
Finding

The default PLIST constant is part of a built-in persistence mechanism targeting the user's LaunchAgents directory. Hardcoding a ready-to-install persistence location lowers friction for creating recurring execution and is therefore security-relevant.

Content

Scanner excerpt · scripts/schedule_nightly_review.py (reported line 12)May include surrounding context.

python
BASE_DIR = Path(__file__).resolve().parent
DEFAULT_LABEL = "com.cheeseclaw.somnia-nightly"
DEFAULT_PLIST = Path.home() / "Library" / "LaunchAgents" / f"{DEFAULT_LABEL}.plist"


def program_arguments(args: argparse.Namespace) -> list[str]:

Session Persistence

Medium
Category
Rogue Agent
Confidence
94% confidence
Finding

build_plist constructs the contents of a launchd job definition, directly enabling scheduled persistence. Because the generated ProgramArguments incorporate user-controlled paths and options, misuse could schedule repeated execution of unintended workloads under the user's account.

Content

Scanner excerpt · scripts/schedule_nightly_review.py (reported line 52)May include surrounding context.

python
return command


def build_plist(args: argparse.Namespace) -> dict:
    log_dir = Path(args.log_dir).expanduser().resolve()
    log_dir.mkdir(parents=True, exist_ok=True)
    return {

Session Persistence

Medium
Category
Rogue Agent
Confidence
96% confidence
Finding

write_plist writes the LaunchAgent definition to disk, which is the concrete step that enables persistence once loaded. Writing scheduler configuration in a user context can be abused to establish recurring execution without continued user awareness.

Content

Scanner excerpt · scripts/schedule_nightly_review.py (reported line 66)May include surrounding context.

python
}


def write_plist(path: Path, payload: dict) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("wb") as handle:
        plistlib.dump(payload, handle, sort_keys=False)

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The code directly manages launchd jobs via launchctl, giving the skill the ability to register persistent scheduled tasks on the host. Even without shell injection, that capability is more privileged than simple maintenance reporting and increases the blast radius if the skill is misused or invoked with unsafe parameters.

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

python
def launchctl(args: list[str]) -> dict:
    completed = subprocess.run(["launchctl", *args], text=True, capture_output=True)
    return {
        "returncode": completed.returncode,
        "stdout": completed.stdout.strip(),

Session Persistence

Medium
Category
Rogue Agent
Confidence
92% confidence
Finding

The unload helper manages an existing LaunchAgent, confirming that the script administers persistent scheduler state on the host. Even though unloading can remove persistence, the capability to manage these artifacts is itself beyond a simple reporting skill and increases host interaction risk.

Content

Scanner excerpt · scripts/schedule_nightly_review.py (reported line 83)May include surrounding context.

python
def unload(path: Path) -> dict:
    if not path.exists():
        return {"returncode": 0, "stdout": "", "stderr": "plist not found"}
    return launchctl(["unload", str(path)])

Static analysis

No suspicious patterns detected.