Back to skill

Security audit

Session Distiller

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its memory-distillation purpose, but it can automatically copy private session content into long-term memory and model/Telegram channels beyond the stated live-session allowlist.

Review before installing. Use this only if you are comfortable granting it access to OpenClaw session transcripts, meeting notes, and long-term memory files. Disable or patch context-gate.py unless it enforces the same live-session allowlist as distill.py, and avoid Telegram alerts or redact session identifiers in sensitive environments. Treat the LiteLLM endpoint as a sensitive data processor, replace hardcoded/shared credentials, and require review before durable user preferences or instructions are written to memory.

Vulnerability Patterns
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • 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
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (4)

T02 · Agent Memory Poisoning

Error
Location
scripts/distill.py:265
Finding

Untrusted session content can poison persistent Agent memory

Content
View full analysis
str: """Call the LLM to distill the transcript. Returns markdown or NO_DISTILL.""" prompt_template = PROMPT_FILE.read_text(encoding="utf-8") full_prompt = f"""{prompt_template} --- EXISTING DAILY LOG (do not duplicate) --- {existing_log if existing_log else "(empty — no existing log for this date)"} --- SESSION TRANSCRIPT --- {transcript} """ payload = { "model": "claude-opus-4-6", "messages": [ {"role": "user", "content": full_prompt} ], "max_tokens": 2048, "temperature": 0.1, } try: result = subprocess.run( [ "curl", "-s", "-X", "POST", "http://localhost:4000/v1/chat/completions", "-H", "Content-Type: application/json", "-H", "Authorization: Bearer sk-1234", "-d", json.dumps(payload), ], capture_output=True, text=True, timeout=120, ) response = json.loads(result.stdout) content = response["choices"][0]["message"]["content"] return content.strip() except Exception as e: print(f" ERROR: Distillation failed: {e}") return None ``` The resulting model output is persisted without validation: ```python def append_to_daily_log(date_str: str, session_uuid: str, distilled: str, source_file: str): """Append distilled content to the daily memory file.""" daily_file = MEMORY_DIR / f"{date_str}.md" section = f""" ## Session Distillation — {session_uuid[:8]} - **Distilled:** {datetime.now().strftime("%Y-%m-%d %H:%M CST")} - ** ...[truncated 2737 chars]
Remediation
View remediation

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/context-gate.py:219
Finding

Context Gate bypasses the approved live-session allowlist

Content
View full analysis
= CONTEXT_HARD_PCT and not s["gated"]: log(f" 🛑 HARD GATE — {pct}% >= {CONTEXT_HARD_PCT}%") # Auto-distill distill_ok = run_distill_for_session(session, dry_run=dry_run) distill_status = "✅ Distilled to memory" if distill_ok else "⚠️ Distill failed — flush manually" alert = ( f"🛑 *Context Hard Gate*\n" f"Session: `{key}`\n" f"Usage: *{pct}%* ({total:,}/{ctx:,} tokens)\n" f"{distill_status}\n" f"Recommend `/new` now." ) send_telegram(alert, dry_run=dry_run) ``` The invoked `--live-session` path explicitly allows sessions outside the configured allowlist: ```python allowlist = resolve_live_allowlist() targets = {} if session_id: # On-demand: specific session — may be a session key OR a UUID resolved_id = session_id resolved_label = "On-demand (not in allowlist)" if ":" in session_id: # It's a session key — look it up in sessions.json directly if SESSIONS_INDEX.exists(): try: with open(SESSIONS_INDEX, "r", encoding="utf-8") as f: index = json.load(f) entry = index.get(session_id) if entry and entry.get("sessionId"): resolved_id = entry["sessionId" ...[truncated 2864 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/context-gate.py:83
Finding

Telegram bot token is exposed in child-process command-line arguments

Content
View full analysis
/sendMessage`. 5. The token is reused against Telegram's Bot API to impersonate or control the bot within the permissions Telegram grants it. ### Impact Assessment An exposed token can permit unauthorized Bot API requests, message sending, and access to bot-visible updates or chat metadata depending on the bot's configuration and Telegram ...[truncated 170 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/distill.py:285
Finding

Hardcoded shared credential and plaintext HTTP are used for the LiteLLM proxy

Content
View full analysis
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (26)

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

The manifest context says the skill is 'NOT for: real-time memory flush,' but the README explicitly says it 'ensures zero session knowledge is lost — even from short conversations that end before the real-time memory flush threshold.' That describes a compensating memory-preservation role directly tied to flush-threshold behavior, which blurs the stated exclusion and expands the apparent purpose beyond simple post-session distillation.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding

The skill documentation describes capabilities that read session files, write memory files, invoke shell-dependent tooling, and use environment variables, but it does not declare any explicit tool scope or permissions boundary. In an agent ecosystem, missing scope metadata can cause the runtime or operator to grant broader access than intended, increasing the blast radius if the skill is misused or compromised.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The skill states that distillation calls are sent to http://localhost:4000/v1/chat/completions but does not clearly warn users that potentially sensitive transcript, meeting-note, and context data are transmitted to an LLM service. Even if the endpoint is localhost, it may proxy to external models or services, so users may unknowingly expose private conversations, operational details, or personal data.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The manifest describes this component as a context usage monitor with auto-distill for sessions approaching limits, but it does not mention outbound messaging or third-party integrations. This code reads bot credentials from environment variables and posts session metadata to Telegram, which introduces an external communication capability beyond the core monitoring/distillation purpose.

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

python
"text": message,
            "parse_mode": "Markdown",
        })
        result = subprocess.run(
            [
                "curl", "-s", "-X", "POST",
                f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",

Tainted flow: 'payload' from os.environ.get (line 84, 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 · scripts/context-gate.py (reported line 89)May include surrounding context.

python
"text": message,
            "parse_mode": "Markdown",
        })
        result = subprocess.run(
            [
                "curl", "-s", "-X", "POST",
                f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",

External Transmission

Medium
Category
Data Exfiltration
Confidence
82% confidence
Finding

The script transmits session metadata to Telegram, including session key and token-usage details, to a third-party external service. In this skill context, monitoring and alerting are expected, but external transmission increases privacy and metadata-leak risk, especially if session keys contain user names, project identifiers, or sensitive chat labels.

Content

Scanner excerpt · scripts/context-gate.py (reported line 92)May include surrounding context.

python
result = subprocess.run(
            [
                "curl", "-s", "-X", "POST",
                f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
                "-H", "Content-Type: application/json",
                "-d", payload,
            ],

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

python
def get_session_status() -> list:
    """Call openclaw gateway call status --json and return session list."""
    try:
        result = subprocess.run(
            ["/opt/homebrew/bin/openclaw", "gateway", "call", "status", "--json"],
            capture_output=True,
            text=True,

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
73% confidence
Finding

The script launches distill.py using a session_id value obtained from external gateway output, creating a trust boundary crossing into another executable component. Even though shell injection is mitigated by argument-list usage, invoking a second script on attacker-influenced identifiers can amplify risk if distill.py mishandles that input or accesses unintended session data.

Content

Scanner excerpt · scripts/context-gate.py (reported line 143)May include surrounding context.

python
return True
    try:
        log(f"  Auto-distilling session {session_id[:8]}...")
        result = subprocess.run(
            ["python3", str(DISTILL_SCRIPT), "--live-session", session_id],
            capture_output=True,
            text=True,

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
98% confidence
Finding

The inline safety comment says only explicitly approved sessions receive live distillation, but the actual code later permits arbitrary on-demand sessions. That mismatch is dangerous because reviewers and operators may rely on the documented restriction and unintentionally expose unapproved live transcripts to distillation and downstream storage.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The script claims live distillation is limited to explicitly approved sessions, but on-demand mode will process arbitrary session IDs that are not in the allowlist. In this skill, that means an operator or another automation path can exfiltrate or summarize sensitive live chat content from any session into memory files and to the LLM backend, bypassing the stated approval boundary.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The script sends full session transcripts, including potentially sensitive user and assistant content, to an HTTP endpoint without explicit user disclosure or consent at runtime. Even though the target is localhost, the transport is unauthenticated beyond a placeholder bearer token and may forward data onward via the local proxy, creating privacy, policy, and unintended data-sharing risk.

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

python
}

    try:
        result = subprocess.run(
            [
                "curl", "-s", "-X", "POST",
                "http://localhost:4000/v1/chat/completions",

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

python
}

    try:
        result = subprocess.run(
            [
                "curl", "-s", "-X", "POST",
                "http://localhost:4000/v1/chat/completions",

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

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 · scripts/distill.py (reported line 318)May include surrounding context.

python
"""

    if daily_file.exists():
        with open(daily_file, "a", encoding="utf-8") as f:
            f.write("\n" + section)
    else:
        with open(daily_file, "w", encoding="utf-8") as f:

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

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 · scripts/distill.py (reported line 555)May include surrounding context.

python
"""

    if daily_file.exists():
        with open(daily_file, "a", encoding="utf-8") as f:
            f.write("\n" + section)
    else:
        with open(daily_file, "w", encoding="utf-8") as f:

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

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 · scripts/distill.py (reported line 762)May include surrounding context.

python
"""

    if daily_file.exists():
        with open(daily_file, "a", encoding="utf-8") as f:
            f.write("\n" + section)
    else:
        with open(daily_file, "w", encoding="utf-8") as f:

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

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 · scripts/distill.py (reported line 1001)May include surrounding context.

python
"""

    if daily_file.exists():
        with open(daily_file, "a", encoding="utf-8") as f:
            f.write("\n" + section)
    else:
        with open(daily_file, "w", encoding="utf-8") as f:

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

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 · scripts/distill.py (reported line 321)May include surrounding context.

python
with open(daily_file, "a", encoding="utf-8") as f:
            f.write("\n" + section)
    else:
        with open(daily_file, "w", encoding="utf-8") as f:
            f.write(f"# Captain's Log — {date_str}\n{section}")

    print(f"  ✅ Appended to {daily_file.name}")

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

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 · scripts/distill.py (reported line 558)May include surrounding context.

python
with open(daily_file, "a", encoding="utf-8") as f:
            f.write("\n" + section)
    else:
        with open(daily_file, "w", encoding="utf-8") as f:
            f.write(f"# Captain's Log — {date_str}\n{section}")

    print(f"  ✅ Appended to {daily_file.name}")

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

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 · scripts/distill.py (reported line 765)May include surrounding context.

python
with open(daily_file, "a", encoding="utf-8") as f:
            f.write("\n" + section)
    else:
        with open(daily_file, "w", encoding="utf-8") as f:
            f.write(f"# Captain's Log — {date_str}\n{section}")

    print(f"  ✅ Appended to {daily_file.name}")

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

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 · scripts/distill.py (reported line 1004)May include surrounding context.

python
with open(daily_file, "a", encoding="utf-8") as f:
            f.write("\n" + section)
    else:
        with open(daily_file, "w", encoding="utf-8") as f:
            f.write(f"# Captain's Log — {date_str}\n{section}")

    print(f"  ✅ Appended to {daily_file.name}")

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

python
def trash_file(filepath: Path):
    """Move file to macOS Trash."""
    try:
        subprocess.run(["trash", str(filepath)], check=True, capture_output=True)
        print(f"  🗑  Trashed: {filepath.name}")
    except FileNotFoundError:
        print(f"  ⚠️  'trash' command not found. Install: brew install trash")

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

Meeting notes are transmitted to the LLM service without a specific disclosure that their contents will be sent for model processing. Because meeting notes often contain business decisions, people, and technical details, this can leak sensitive organizational information beyond the expected local file-ingestion behavior.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
70% confidence
Finding

The documentation for live mode frames the capability as limited to approved group chats, but the configuration example includes a direct Telegram session key alongside a group chat key. This creates intent ambiguity in the documentation about whether live distill-in-place is restricted to approved group chats or broader live sessions, which contradicts the stated scope rather than merely omitting detail.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.