Back to skill

Security audit

Ops Framework

Security checks for vulnerabilities and agentic risk

Overview

This skill is a disclosed operations monitor, but it can repeatedly run arbitrary configured local commands and send Telegram alerts without a strong technical boundary around its claimed read-only mode.

Review before installing. Use this only if you are comfortable treating ops-jobs.json and every referenced script as trusted code. Keep autoResume off unless needed, restrict file permissions on ~/.openclaw/net/config and scripts, run the monitor under a low-privilege account, avoid sending secrets in status output, and prefer explicit stop commands over PID-based stopping.

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

T09 · Insecure Skill Coding Practices

Error
Location
ops-monitor.py:556
Finding

Self-Declared Risk Labels Do Not Enforce Read-Only Execution

Content
View full analysis

Vulnerability Details

File Location: ops-monitor.py:304-330, ops-monitor.py:556-600
Vulnerability Type: Arbitrary command execution through unenforced trust labels
Risk Level: High

Vulnerable Code

python
risk = raw.get("risk")
if not isinstance(risk, str) or risk not in ALLOWED_RISKS:
    raise ValueError(f"Invalid job {jid}: risk must be one of {sorted(ALLOWED_RISKS)}")

cwd_raw = raw.get("cwd")
if cwd_raw is None:
    cwd = OPENCLAW_HOME
elif isinstance(cwd_raw, str) and cwd_raw.strip():
    cwd = Path(cwd_raw).expanduser()
else:
    raise ValueError(f"Invalid job {jid}: cwd must be a non-empty string path")

commands_raw = raw.get("commands")
if not isinstance(commands_raw, dict):
    raise ValueError(f"Invalid job {jid}: commands must be an object")
commands: dict[str, list[str]] = {}
for k, v in commands_raw.items():
    if not isinstance(k, str):
        continue
    argv = _as_argv(v)
    if argv is None:
        raise ValueError(f"Invalid job {jid}: commands.{k} must be a non-empty argv list")
    commands[k] = argv
python
def maybe_autorun_start(
    *,
    job: JobConfig,
    status: JobStatus,
    now: float,
    state_job: dict[str, Any],
    defaults: JobDefaults,
    dry_run: bool,
) -> str | None:
    if job.kind != "long_running_read":
        return None
    if not job.enabled:
        return None
    if job.risk != "read_only":
        return "AUTORUN: blocked (risk != read_only)"
    if status.running or status.completed:
        return None
    if not _policy_bool(job, defaults, "autoResume"):
        return None

    last = state_job.get("lastAutoResumeAt")
    try:
        last_ts = float(last) if isinstance(last, (int, float)) else 0.0
    except Exception:
        last_ts = 0.0
    backoff = float(_policy_int(job, defaults, "autoResumeBackoffSeconds") or 0)
    if backoff and now - last_ts < ba
...[truncated 3138 chars]
Remediation
View remediation

Remediation Suggestions

  1. Explicitly document that the job configuration is executable trusted code, not a security policy boundary.
  2. Restrict ownership and permissions on the configuration, state directory, and invoked scripts so only the dedicated monitor administrator can modify them.
  3. Run the monitor under a dedicated, unprivileged operating-system account with access only to required paths.
  4. Replace arbitrary command arrays for automatic execution with an allowlist of audited executable paths and fixed argument schemas.
  5. Resolve executable paths to canonical absolute paths and reject writable, relative, unexpected, or symlink-substituted executables.
  6. Store approved command manifests or hashes and verify their integrity before automatic execution.
  7. Apply operating-system sandboxing to restrict filesystem writes, process access, and network destinations.
  8. Disable networking for genuinely local read jobs unless a reviewed job explicitly requires it.
  9. Keep autoResume disabled by default and require a separate, protected approval record rather than an approval field in the same mutable configuration.
  10. Add negative tests proving that mislabeled write commands cannot modify files or access the network.

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
ops-monitor.py:1049
Finding

Unverified Status PID Can Terminate an Unrelated Process

Content
View full analysis

Vulnerability Details

File Location: ops-monitor.py:416-444, ops-monitor.py:1049-1084
Vulnerability Type: Improper process authorization and PID validation
Risk Level: Medium

Vulnerable Code

python
def _status_from_json(obj: Any) -> JobStatus:
    if not isinstance(obj, dict):
        raise ValueError("status JSON must be an object")
    running = obj.get("running")
    completed = obj.get("completed")
    if not isinstance(running, bool) or not isinstance(completed, bool):
        raise ValueError("status JSON must include boolean fields: running, completed")
    pid = obj.get("pid")
    pid_i = int(pid) if isinstance(pid, int) and pid > 0 else None
    stop_reason = obj.get("stopReason")
    stop_reason = stop_reason if isinstance(stop_reason, str) and stop_reason.strip() else None
    level = obj.get("level")
    level = level if isinstance(level, str) and level.strip() else None
    message = obj.get("message")
    message = message if isinstance(message, str) and message.strip() else None
    progress = obj.get("progress")
    progress = progress if isinstance(progress, dict) else None
    progress_key = obj.get("progressKey")
    progress_key = progress_key if isinstance(progress_key, str) and progress_key.strip() else None
    if not progress_key and progress is not None:
        try:
            progress_key = json.dumps(progress, sort_keys=True, ensure_ascii=False)
        except Exception:
            progress_key = None
    return JobStatus(
        running=running,
        completed=completed,
        pid=pid_i,
        stop_reason=stop_reason,
        progress=progress,
        progress_key=progress_key,
        level=level,
        message=message,
    )
python
def cmd_stop(args: argparse.Namespace) -> int:
    now = _now_ts()
    defaults, jobs, state, state_jobs, queue, state_path = _load_for_cmd(args)
    _ = defaults

...[truncated 3403 chars]
Remediation
View remediation

Remediation Suggestions

  1. Record the PID immediately when a job is started instead of trusting a later status response as the sole source of identity.
  2. Record additional process identity attributes, including executable path, command line, owner, process start time, and a generated job-instance identifier.
  3. Before signaling, verify that all recorded identity attributes still match the live process to prevent PID-reuse attacks.
  4. Reject stop requests if process identity cannot be conclusively verified.
  5. Prefer a dedicated, audited commands.stop implementation or an operating-system process supervisor that tracks process handles.
  6. Run each job in a dedicated process group or service unit and stop that managed unit rather than an unverified numeric PID.
  7. Run the monitor without elevated privileges and isolate unrelated jobs under separate operating-system accounts where practical.
  8. Add tests covering malicious PIDs, stale PIDs, PID reuse, executable mismatches, and unrelated same-user processes.
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 (20)

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 · UPSTREAM.md (reported line 15)May include surrounding context.

md
## 提交前检查清单

- [ ] 不包含任何密钥(Telegram bot token、MCP access token、provider apiKey 等)
- [ ] 不包含任何个人路径(例如 `/Users/<name>/...`)与个人信息
- [ ] 示例配置使用占位路径(`/path/to/workdir`)与占位 chat id
- [ ] `python3 ops-monitor.py selftest` 通过

Session Persistence

Medium
Category
Rogue Agent
Confidence
60% 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 · OPS_FRAMEWORK.md (reported line 52)May include surrounding context.

md
- “别动网盘里其他文件 / 只能读不能写” →
  - 只允许 read 类 MCP/命令
  - 路径范围严格限制(只触达 allowlist)
  - 任何 rename/move/delete/copy/overwrite 一律阻断并 `ACTION REQUIRED`
- “只整理这批 / 其他不动” →
  - 明确 allowlist;allowlist 外一律不触达

Session Persistence

Medium
Category
Rogue Agent
Confidence
60% 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 · OPS_FRAMEWORK.md (reported line 52)May include surrounding context.

md
- “别动网盘里其他文件 / 只能读不能写” →
  - 只允许 read 类 MCP/命令
  - 路径范围严格限制(只触达 allowlist)
  - 任何 rename/move/delete/copy/overwrite 一律阻断并 `ACTION REQUIRED`
- “只整理这批 / 其他不动” →
  - 明确 allowlist;allowlist 外一律不触达

Session Persistence

Medium
Category
Rogue Agent
Confidence
75% confidence
Finding

The framework explicitly defines persistent components: job registry files, runtime state, and an optional launchd agent for scheduled monitoring. This creates a host-level persistence mechanism that can survive across sessions and repeatedly execute jobs, which is dangerous if a malicious or misconfigured job is registered because it enables unattended recurring execution and ongoing monitoring.

Content

Scanner excerpt · OPS_FRAMEWORK.md (reported line 108)May include surrounding context.

md
- Job Registry(配置):`~/.openclaw/net/config/ops-jobs.json`
- Runtime State(运行态):`~/.openclaw/net/state/ops-monitor.json`
- 工具脚本:`~/.openclaw/net/tools/ops-monitor.py`
- (可选)调度器(macOS launchd):`~/Library/LaunchAgents/ai.openclaw.ops-monitor.plist`

## Job Schema(v1)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding

The skill advertises operational capabilities involving local scripts, config/state files, shell commands, environment variables, and outbound Telegram messaging, but it declares no explicit tool scope or permissions boundary. That mismatch can cause an agent runtime or reviewer to underestimate the skill's effective access, increasing the risk of unintended file, shell, or network use.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
60% 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 · SKILL.md (reported line 6)May include surrounding context.

md
description: >-
  A 0-token jobs + monitoring framework for OpenClaw: run long-running read tasks
  via scripts, checkpoint/resume safely, and send periodic progress + immediate
  alerts to Telegram. Write jobs are blocked by default and must be explicitly
  approved and verified.
version: 0.1.0
author: Zjianru

Autonomous Decision Making

Medium
Category
Excessive Agency
Confidence
85% confidence
Finding

Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Content

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

md
- `long_running_read`
- `one_shot_read`
- `one_shot_write` (**never auto-executed by ops-monitor**)

`risk` is one of:

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

The manifest describes a framework centered on long-running read tasks where writes are blocked by default and require explicit approval and verification. However, the code formally includes one_shot_write and write_local/write_external as allowed job types/risks, validates write-job approval metadata, and exposes their approval status, showing that write operations are part of the supported framework semantics rather than being absent or universally blocked in code.

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 · ops-monitor.py (reported line 175)May include surrounding context.

python
def send_telegram_via_openclaw(*, openclaw_bin: str, target: str, message: str) -> None:
    subprocess.run(
        [openclaw_bin, "message", "send", "--channel", "telegram", "--target", target, "--message", message],
        check=True,
        capture_output=True,

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 · ops-monitor.py (reported line 191)May include surrounding context.

python
if not isinstance(token, str) or not token:
        raise RuntimeError("telegram.botToken missing; cannot send directly")

    url = f"https://api.telegram.org/bot{token}/sendMessage"
    payload = json.dumps({"chat_id": target, "text": message, "disable_web_page_preview": True}).encode("utf-8")
    req = Request(url, data=payload, headers={"Content-Type": "application/json"})
    with urlopen(req, timeout=20) as r:  # noqa: S310

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
92% confidence
Finding

This helper executes arbitrary argv supplied by job configuration, making the framework a general command runner for configured tasks. Because config and working directory are externally controlled inputs, anyone who can modify the ops-jobs file or referenced scripts can achieve code execution under the monitor's privileges.

Content

Scanner excerpt · ops-monitor.py (reported line 226)May include surrounding context.

python
def run_cmd(argv: list[str], *, cwd: Path, timeout_seconds: int) -> CmdResult:
    t0 = time.time()
    proc = subprocess.run(  # noqa: S603
        argv,
        cwd=str(cwd),
        capture_output=True,

Tainted flow: 'argv' from os.environ.get (line 1028, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · ops-monitor.py (reported line 226)May include surrounding context.

python
def run_cmd(argv: list[str], *, cwd: Path, timeout_seconds: int) -> CmdResult:
    t0 = time.time()
    proc = subprocess.run(  # noqa: S603
        argv,
        cwd=str(cwd),
        capture_output=True,

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
90% confidence
Finding

The status command is executed directly from job configuration, so a supposedly harmless monitoring action can run arbitrary local programs every polling cycle. In this framework context, that meaningfully expands risk because periodic monitoring may repeatedly execute attacker-controlled code if the config is tampered with.

Content

Scanner excerpt · ops-monitor.py (reported line 453)May include surrounding context.

python
if not argv:
        # Non-long-running jobs may not define status. Treat as not running.
        return JobStatus(running=False, completed=False, message="missing status command")
    proc = subprocess.run(  # noqa: S603
        argv,
        cwd=str(job.cwd),
        capture_output=True,

Tainted flow: 'argv' from os.environ.get (line 1028, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · ops-monitor.py (reported line 453)May include surrounding context.

python
if not argv:
        # Non-long-running jobs may not define status. Treat as not running.
        return JobStatus(running=False, completed=False, message="missing status command")
    proc = subprocess.run(  # noqa: S603
        argv,
        cwd=str(job.cwd),
        capture_output=True,

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
94% confidence
Finding

Auto-resume can launch configured start commands automatically without an interactive checkpoint, which turns a monitoring tick into autonomous code execution. If an attacker can alter job definitions or referenced scripts, they can get repeated execution with minimal operator involvement.

Content

Scanner excerpt · ops-monitor.py (reported line 593)May include surrounding context.

python
return "AUTORUN: would start (dry-run)"

    try:
        proc = subprocess.run(  # noqa: S603
            argv,
            cwd=str(job.cwd),
            capture_output=True,

Tainted flow: 'argv' from os.environ.get (line 1028, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · ops-monitor.py (reported line 593)May include surrounding context.

python
return "AUTORUN: would start (dry-run)"

    try:
        proc = subprocess.run(  # noqa: S603
            argv,
            cwd=str(job.cwd),
            capture_output=True,

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The report-building logic emits fixed Chinese strings such as "状态", "信息", and "需要你确认下一步", and later the tool also sends Chinese alert headers. This imposes a specific language on all users without opt-in or any documented region-specific justification, which violates the natural-language locale policy.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
98% confidence
Finding

The inline comment says one-shot jobs are executed explicitly or via a future queue/scheduler and are not polled here. In reality, earlier in the same tick function, the code processes queued one-shot read jobs immediately, updates queue state, executes commands, and reports results, so the comment actively misstates current behavior.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
94% confidence
Finding

The README's user-facing instructions and descriptions are written in Chinese, and there is no indication that alternative languages are available or that the locale restriction is intentional. Under the policy rule, forcing a specific language without user opt-in is a natural-language policy concern.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
96% confidence
Finding

Line L03 presents the core descriptive content in Chinese with no indication that alternate languages are supported or that the locale is intentionally restricted. This is a natural-language policy concern because it imposes a specific language on readers without opt-in or a documented regional justification.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.