Back to skill

Security audit

token-ledger

Security checks for vulnerabilities and agentic risk

Overview

This skill is a disclosed local token-usage ledger, but users should understand it can run persistently and records sensitive usage metadata.

Install only if you want a local usage ledger that reads OpenClaw session and cron metadata and may run as a persistent watcher. Keep LOCAL_API_HUB_URL pointed at a trusted loopback service, restrict ~/.openclaw permissions, and verify the LaunchAgent plist/template issue before trying to run the daemon.

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

Note
Location
scripts/ledger_watcher.py:484
Finding

Sensitive ledger and checkpoint files rely on ambient umask permissions

Content
View full analysis

Vulnerability Details

File Location: scripts/ledger_watcher.py:484-486, 528-529; sensitive fields defined in scripts/ledger_schema.sql:5-34
Vulnerability Type: Local sensitive-data exposure through insufficiently enforced file permissions
Risk Level: Low

Vulnerable Code

python
def get_db() -> sqlite3.Connection:
    LEDGER_DB.parent.mkdir(parents=True, exist_ok=True)
    db = sqlite3.connect(str(LEDGER_DB))
    db.execute("PRAGMA journal_mode=WAL")
python
def save_checkpoint(cp: dict):
    CHECKPOINT.write_text(json.dumps(cp, indent=2))

The database contains sensitive operational metadata:

sql
CREATE TABLE IF NOT EXISTS calls (
  call_id            TEXT PRIMARY KEY,
  session_key        TEXT NOT NULL,
  turn_hint          TEXT,
  ts                 TEXT NOT NULL,
  provider           TEXT,
  model              TEXT,
  model_raw          TEXT,
  call_reason        TEXT DEFAULT 'primary',

  input_tokens       INTEGER DEFAULT 0,
  output_tokens      INTEGER DEFAULT 0,
  cache_read_tokens  INTEGER DEFAULT 0,
  cache_write_tokens INTEGER DEFAULT 0,

  cost_input         REAL DEFAULT 0,
  cost_output        REAL DEFAULT 0,
  cost_cache_read    REAL DEFAULT 0,
  cost_cache_write   REAL DEFAULT 0,
  cost_total         REAL DEFAULT 0,
  cost_source        TEXT DEFAULT 'unknown',

  channel            TEXT,
  chat_id            TEXT,
  thread_id          TEXT,
  message_id         TEXT,
  source_kind        TEXT DEFAULT 'interactive',
  cron_job_id        TEXT,
  price_version      TEXT DEFAULT '2026-03-17-unified',
  usage_raw          TEXT,
  created_at         TEXT DEFAULT (datetime('now'))
);

Technical Analysis

The watcher creates ~/.openclaw, ledger.db, SQLite WAL/SHM files, and the main checkpoint without explicitly enforcing owner-only permissions. Their resulting modes therefore depend on the process umask an ...[truncated 1905 chars]

Remediation
View remediation

Remediation Suggestions

  1. Create the data directory with mode 0700 and verify its mode even when it already exists:

    python
    LEDGER_DB.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
    LEDGER_DB.parent.chmod(0o700)
    
  2. Explicitly set ledger.db to mode 0600 immediately after opening or creating it:

    python
    db = sqlite3.connect(str(LEDGER_DB))
    LEDGER_DB.chmod(0o600)
    
  3. Restrict SQLite sidecar files after enabling WAL mode, and consider using a temporary restrictive umask while opening the database:

    python
    old_umask = os.umask(0o077)
    try:
        db = sqlite3.connect(str(LEDGER_DB))
        db.execute("PRAGMA journal_mode=WAL")
    finally:
        os.umask(old_umask)
    
  4. Replace the main checkpoint atomically using the same pattern already used for the Spark checkpoint: create a temporary file in the protected directory, set it to 0600, flush it, and atomically replace the destination.

  5. On startup, validate ownership and permissions of the directory, database, WAL/SHM files, and checkpoints. Refuse to proceed or emit a prominent warning if they are writable or readable by unintended users.

  6. Document that the ledger contains potentially sensitive communication and financial metadata, along with recommended retention and secure-deletion practices.

Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (18)

Tainted flow: 'req' from os.environ.get (line 414, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
93% confidence
Finding

The watcher builds a request target from the LOCAL_API_HUB_URL environment variable and then performs an HTTP request without validating the scheme, host, or port. An attacker who can influence the process environment can redirect the watcher to an arbitrary service, causing unintended outbound requests and ingestion of attacker-controlled JSON into the ledger sync path; this is an SSRF-style/local trust-boundary issue even though the default target is loopback.

Content

Scanner excerpt · scripts/ledger_watcher.py (reported line 416)May include surrounding context.

python
url = f"{API_HUB_URL}/spark/token-log?limit=1000"
        req = urllib.request.Request(url, method="GET")
        req.add_header("Accept", "application/json")
        with urllib.request.urlopen(req, timeout=30) as resp:
            data = json.loads(resp.read().decode())
            if data.get("ok"):
                entries = data.get("entries", [])

Context Leakage

High
Category
Data Exfiltration
Confidence
75% confidence
Finding

Code or instructions that leak agent conversation context to external services, potentially exposing sensitive user interactions.

Content

Scanner excerpt · scripts/ledger_watcher.py (reported line 185)May include surrounding context.

python
m_chat = _re.search(r'discord:channel:(\d+)', sk)
        if m_chat: chat_id = m_chat.group(1)

        # Extract thread_id from session key like "...:topic:1479712989392273498"
        m_thread = _re.search(r':topic:(\d+)', sk)
        if m_thread: thread_id_meta = m_thread.group(1)

Context Leakage

High
Category
Data Exfiltration
Confidence
75% confidence
Finding

Code or instructions that leak agent conversation context to external services, potentially exposing sensitive user interactions.

Content

Scanner excerpt · scripts/ledger_watcher.py (reported line 213)May include surrounding context.

python
m_chat = _re.search(r'discord:channel:(\d+)', sk)
        if m_chat: chat_id = m_chat.group(1)

        # Extract thread_id from session key like "...:topic:1479712989392273498"
        m_thread = _re.search(r':topic:(\d+)', sk)
        if m_thread: thread_id_meta = m_thread.group(1)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding

The skill documentation describes capabilities that read local session files, write a SQLite database and checkpoint files, install a LaunchAgent, invoke shell commands, and optionally contact a local API endpoint, yet the manifest declares no explicit tool scope or permission boundaries. This creates a trust gap where operators may not realize the full file, shell, persistence, and network reach of the skill before use.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The skill instructs installation of a macOS LaunchAgent and creation of a plist under the user's LaunchAgents directory, which establishes persistence and ongoing background execution. Performing these system and user-environment changes without a prominent warning or consent flow can surprise users and increase the risk of unintended long-lived processes.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
89% confidence
Finding

Rendering a plist into ~/Library/LaunchAgents creates a persistence artifact that will cause the watcher to survive beyond the current session. In a security review, persistence is sensitive because it enables continued execution and ongoing access to user data sources if the underlying watcher is modified or misused.

Content

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

Install / start daemon (macOS LaunchAgent)

bash
python3 "{baseDir}/scripts/render_plist.py" \
  > ~/Library/LaunchAgents/com.openclaw.token-ledger-watcher.plist
launchctl load ~/Library/LaunchAgents/com.openclaw.token-ledger-watcher.plist
launchctl list | rg token-ledger-watcher

Session Persistence

Medium
Category
Rogue Agent
Confidence
88% confidence
Finding

The generated LaunchAgent plist is part of a persistence mechanism and should be treated as security-relevant because it defines recurring execution of the watcher. Without strong disclosure and integrity assurances, users may unknowingly install a long-running component that continually processes local session and billing data.

Content

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

bash
python3 "{baseDir}/scripts/render_plist.py" \
  > ~/Library/LaunchAgents/com.openclaw.token-ledger-watcher.plist
launchctl load ~/Library/LaunchAgents/com.openclaw.token-ledger-watcher.plist
launchctl list | rg token-ledger-watcher

Session Persistence

Medium
Category
Rogue Agent
Confidence
86% confidence
Finding

This finding also concerns the same LaunchAgent persistence path referenced on the load line, reinforcing that the skill both creates and activates a persistent agent. In this skill's context, persistence is expected for a watcher daemon, but it still expands the attack surface and duration of any future bug or misuse.

Content

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

bash
python3 "{baseDir}/scripts/render_plist.py" \
  > ~/Library/LaunchAgents/com.openclaw.token-ledger-watcher.plist
launchctl load ~/Library/LaunchAgents/com.openclaw.token-ledger-watcher.plist
launchctl list | rg token-ledger-watcher

Session Persistence

Medium
Category
Rogue Agent
Confidence
86% confidence
Finding

This finding also concerns the same LaunchAgent persistence path referenced on the load line, reinforcing that the skill both creates and activates a persistent agent. In this skill's context, persistence is expected for a watcher daemon, but it still expands the attack surface and duration of any future bug or misuse.

Content

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

bash
python3 "{baseDir}/scripts/render_plist.py" \
  > ~/Library/LaunchAgents/com.openclaw.token-ledger-watcher.plist
launchctl load ~/Library/LaunchAgents/com.openclaw.token-ledger-watcher.plist
launchctl list | rg token-ledger-watcher

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
87% confidence
Finding

The skill states that Spark local token logs are synchronized via an API Hub endpoint, but it does not prominently warn that usage metadata may leave the immediate file-only workflow and be transmitted over an API interface. Even if loopback/local, token usage logs can contain sensitive operational metadata such as model names, timestamps, and workload patterns.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The top-level docstring frames the script as watching ~/.openclaw/agents/main/sessions/.jsonl for new lines and writing them into ledger.db. In practice, the code also scans ~/.openclaw/cron/runs/**/.jsonl, reads Spark segment/NFS logs, and may call a local API endpoint for Spark data, which is materially broader than the stated behavior.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

The module docstring describes a filesystem watcher that parses session JSONL files and writes usage into SQLite. However, the fallback path fetches token-log data over HTTP from LOCAL_API_HUB_URL, introducing network access beyond the watcher behavior documented in this file.

Content

No source excerpt is available for this finding.

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

md
#!/usr/bin/env python3
"""Render the LaunchAgent plist with absolute home and installed script paths."""

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

python
#!/usr/bin/env python3
"""Render the LaunchAgent plist with absolute home and installed script paths."""

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

python
#!/usr/bin/env python3
"""Render the LaunchAgent plist with absolute home and installed script paths."""

from pathlib import Path

Description-Behavior Mismatch

Low
Category
Not specified by scanner
Confidence
82% confidence
Finding

The manifest description centers on recording model-call usage into SQLite, managing a watcher LaunchAgent, querying the local ledger, and generating reports. Elsewhere in the file, the skill is documented to synchronize DGX Spark usage by pulling from an API Hub endpoint, which adds a network/data-ingestion behavior not reflected in the top-level description.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
80% confidence
Finding

This code performs an HTTP request to a local API endpoint to retrieve Spark token log entries, which is a network operation covered by the missing-warning rule for code files. While the module docstring describes ledger watching generally, the specific fallback behavior of contacting an API is not disclosed near the operation with a prompt, warning, or explanatory comment.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
77% confidence
Finding

The function writes checkpoint state to ~/.openclaw/ledger-checkpoint.json, which is a file write operation. The module mentions writing to ledger.db, but it does not disclose that additional checkpoint metadata is also persisted, and there is no nearby comment or user-facing message warning about this state file.

Content

No source excerpt is available for this finding.

Static analysis

Detected: suspicious.dynamic_code_execution

Dynamic code execution detected.

Critical
Code
suspicious.dynamic_code_execution
Location
scripts/test_price_versions.py:16