Back to skill

Security audit

Pgmemory

Security checks for vulnerabilities and agentic risk

Overview

This memory skill is purpose-aligned overall, but its setup and defaults make broad, persistent system and data-access changes that users should review before installing.

Review this skill before installing. Prefer an existing secured local PostgreSQL or bind Docker PostgreSQL to localhost with a generated password, avoid running the automatic Docker installer, do not store secrets or production connection details as memories, use Ollama/local embeddings for sensitive data, keep API keys out of pgmemory.json unless file permissions are locked down, and inspect any AGENTS.md changes before accepting them.

Vulnerability Patterns
  • 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
  • System PersistenceInstalls backdoors, hooks, services, or scheduled tasks that survive the run
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
Findings (7)

T03 · Remote Payload Retrieval and Execution

Error
Location
scripts/setup.py:473
Finding
Unverified Remote Docker Installer Is Piped Directly into a Shell<![CDATA[ ## Vulnerability Details **File Location**: `scripts/setup.py:473-483` **Vulnerability Type**: Remote payload retrieval and execution with privilege escalation **Risk Level**: Critical ### Vulnerable Code ```python if system == "linux": if not yes and input(" Install Docker via get.docker.com? [y/n] ").lower() != "y": return False if subprocess.run("curl -fsSL https://get.docker.com | sh", shell=True).returncode != 0: err("Docker install failed"); return False subprocess.run(["sudo","systemctl","enable","--now","docker"], capture_output=True) user = os.environ.get("USER","") if user: subprocess.run(["sudo","usermod","-aG","docker",user], capture_output=True) warn(f"Added {user} to docker group — re-login for this to take effect") ``` ### Technical Analysis The setup wizard downloads the current response from `https://get.docker.com` and immediately executes it through a shell. The downloaded content is not pinned to a version, inspected, or verified with a cryptographic digest or signature. Consequently, the effective code executed by the reviewed package can change after publication. The risk depends on the security of the remote host, its delivery infrastructure, DNS, TLS trust chain, and the downloaded script itself. Compromise of any applicable delivery component could turn installation into arbitrary code execution. The setup then invokes `sudo` and adds the current user to the Docker group. Docker group membership is generally root-equivalent because a member can mount the host filesystem or start a privileged container. This permission substantially exceeds ordinary database client requirements. Interactive execution requires confirmation, but `--yes` accepts installation defaults without the inner confirmation. User consent reduces accidental activation but does not provide payload integrity. ### Attack Path 1. Docker is unavailable on the target Linux host. 2. The user selects automatic Doc ...[truncated 949 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove `curl | sh` installation from the Skill. - Direct users to the platform's official, documented package installation procedure. - If automated installation is indispensable: 1. Download a versioned artifact to a temporary file. 2. Restrict redirects and validate the final origin. 3. Verify a pinned SHA-256 digest or a trusted publisher signature. 4. Display the artifact and exact privileged operations before execution. 5. Execute it without `shell=True`. - Do not automatically add users to the Docker group. Explain that it is root-equivalent and require a separate, explicit confirmation. - Do not enable a system service automatically unless the user separately opts in. - Make `--yes` skip privileged installation rather than implicitly authorize it. - Prefer connecting to an existing PostgreSQL service or generate container configuration without installing host-level software. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
assets/docker-compose.yml:8
Finding
PostgreSQL Is Exposed on All Interfaces with Publicly Known Credentials<![CDATA[ ## Vulnerability Details **File Location**: `assets/docker-compose.yml:8-17`; duplicated in `scripts/setup.py:497-510` **Vulnerability Type**: Hardcoded credentials and unnecessarily broad network exposure **Risk Level**: High ### Vulnerable Code ```yaml services: pgmemory: image: pgvector/pgvector:pg17 container_name: pgmemory restart: unless-stopped environment: POSTGRES_USER: openclaw POSTGRES_PASSWORD: pgmemory POSTGRES_DB: openclaw ports: - "15432:5432" ``` The setup script generates equivalent configuration: ```python compose_path.write_text("""services: pgmemory: image: pgvector/pgvector:pg17 container_name: pgmemory restart: unless-stopped environment: POSTGRES_USER: openclaw POSTGRES_PASSWORD: pgmemory POSTGRES_DB: openclaw ports: - "15432:5432" ``` ### Technical Analysis The database password is the fixed, publicly visible value `pgmemory`. Docker port publishing with `"15432:5432"` normally binds the host port on all host interfaces rather than only loopback. If host firewall or cloud network rules allow access to port 15432, any reachable party knows the username, password, and database name. The database contains persistent agent memories, including infrastructure facts, decisions, constraints, preferences, and potentially credentials mistakenly recorded by an agent. The generated PostgreSQL role is also used for migrations and normal application access. There is no separation between schema-administration privileges and runtime read/write privileges. ### Attack Path 1. A user accepts the Docker database setup. 2. Docker publishes port 15432 on all interfaces. 3. The host is connected to an untrusted LAN, cloud network, or internet-facing environment where the port is reachable. 4. An attacker connects using: `postgresql://openclaw:pgmemory@TARGET:15432/openclaw`. 5. The attacker reads existing memories or inserts and modifies records. ...[truncated 530 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Bind PostgreSQL exclusively to loopback by default: ```yaml ports: - "127.0.0.1:15432:5432" ``` - Generate a cryptographically random password during setup. - Store the password in a permission-restricted environment file or secret manager rather than embedding it in generated Compose YAML. - Require users to opt in explicitly before exposing PostgreSQL beyond localhost. - Document firewall and TLS requirements for remote deployments. - Use separate database roles: - A migration role allowed to alter the schema. - A runtime role limited to necessary `SELECT`, `INSERT`, `UPDATE`, and `DELETE` operations on pgmemory tables. - Consider using a Unix socket or an internal Docker network when external port publishing is unnecessary. - Pin the container image by version and digest. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/setup.py:83
Finding
Embedding API Keys and Database Credentials Are Persisted Without Enforced File Permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/setup.py:83-86`, with secret assignment at `scripts/setup.py:685-717` **Vulnerability Type**: Plaintext sensitive credential storage **Risk Level**: High ### Vulnerable Code ```python def save_config(config: dict, path: Path): path.parent.mkdir(parents=True, exist_ok=True) with open(path, "w") as f: json.dump(config, f, indent=2) print(f"\n{green('✓')} Config saved to {path}") ``` The wizard places the API key directly into this file: ```python stored_key = existing.get("embeddings", {}).get("api_key", "") api_key = stored_key or os.environ.get(key_env, "") if not api_key and not yes: entered = input(f" Paste {provider} API key (or leave blank to set ${key_env} later): ").strip() if entered: api_key = entered config = {"db": {"uri": db_uri}, "embeddings": {"provider": provider, "dimensions": dims}, "agent": {"name": agent_name}} if key_env: config["embeddings"]["api_key_env"] = key_env if api_key: config["embeddings"]["api_key"] = api_key save_config(config, config_path) ``` ### Technical Analysis The configuration contains the embedding provider API key and may contain a PostgreSQL URI with an embedded username and password. It is written as plaintext using the process's default `umask`; the code does not create the file with mode `0600`, verify ownership, or correct permissions on an existing file. With a common `022` umask, a newly created file can receive mode `0644`. A restrictive parent directory may mitigate access, but the program does not verify that protection and supports arbitrary `--config` locations. Secrets may therefore be exposed to other local users, backups, support bundles, or processes that collect configuration files. The behavior is intentional according to the changelog, but plaintext API-key persistence is not required: all scripts already support resolving keys from environment variables. ### Attack Path 1. A user pastes ...[truncated 752 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Prefer environment variables, OS keychains, or a dedicated secret manager. - Do not store `embeddings.api_key` by default. - If file-based secret storage is explicitly selected: - Create the file atomically with mode `0600`. - Verify the file is owned by the current user. - Reject symlinks and unexpectedly permissive existing files. - Ensure the parent directory is mode `0700`. - Use a temporary file with restrictive permissions, `fsync` it, and atomically replace the target. - Warn users if a database URI embeds a password. - Add a validation check that reports and optionally fixes unsafe permissions. - Provide key rotation guidance for users who have already generated plaintext configurations. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/write_memory.py:23
Finding
Memory Content and Search Terms Are Disclosed to External Embedding Providers Without a Data-Sensitivity Guard<![CDATA[ ## Vulnerability Details **File Location**: `scripts/write_memory.py:23-43`; equivalent query transmission at `scripts/query_memory.py:29-46` **Vulnerability Type**: External transmission of potentially sensitive agent data **Risk Level**: Medium ### Vulnerable Code ```python def get_embed(text: str, config: dict): provider = config.get("embeddings", {}).get("provider", "voyage") key_env = config.get("embeddings", {}).get("api_key_env", "VOYAGE_API_KEY") # Prefer api_key stored directly in config; fall back to env var api_key = config.get("embeddings", {}).get("api_key") or os.environ.get(key_env, "") model = config.get("embeddings", {}).get("model") import urllib.request try: if provider == "voyage": data = json.dumps({"input": [text], "model": model or "voyage-3"}).encode() req = urllib.request.Request("https://api.voyageai.com/v1/embeddings", data=data, headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}) resp = json.loads(urllib.request.urlopen(req, timeout=15).read()) return resp["data"][0]["embedding"] elif provider == "openai": data = json.dumps({"input": text, "model": model or "text-embedding-3-small"}).encode() req = urllib.request.Request("https://api.openai.com/v1/embeddings", data=data, headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}) resp = json.loads(urllib.request.urlopen(req, timeout=15).read()) return resp["data"][0]["embedding"] ``` ### Technical Analysis When a remote provider is selected, `write_memory.py` sends the complete `--content` value to Voyage AI or OpenAI. Semantic searches similarly send the complete query text. The declared use cases explicitly include infrastructure facts, constraints, and decisions, which are likely to contain private hostnames, network addre ...[truncated 1612 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Add a prominent setup warning that the full memory text and semantic query are sent to the selected cloud provider. - Require explicit informed consent before enabling a remote provider. - Make local Ollama embedding the privacy-preserving default where practical. - Add configurable secret detection and redaction for API keys, passwords, private keys, connection strings, and common credential formats. - Support categories or tags that must never leave the host. - Add a `local_only` policy that rejects remote embedding rather than silently falling back. - Encourage users to store references to secrets rather than the secret values themselves. - Document provider retention, regional processing, and contractual requirements. - Preserve `--no-embed` and expose an equivalent clear option for searches that must remain local. ]]>

T06 · System Persistence

Warning
Location
scripts/setup.py:751
Finding
Unquoted Paths in Persistent Cron Entry Permit Shell Command Injection<![CDATA[ ## Vulnerability Details **File Location**: `scripts/setup.py:751-764` **Vulnerability Type**: Persistent scheduled-command injection **Risk Level**: Medium ### Vulnerable Code ```python # ── Cron ────────────────────────────────────────────────────────────────── hdr("Step 7: Decay schedule") decay_cmd = f"0 3 * * * python3 {SKILL_DIR}/scripts/setup.py --decay --config {config_path} >> /tmp/pgmemory-decay.log 2>&1" if yes: info(f"Add cron for daily decay:\n {decay_cmd}") elif input(" Add daily decay cron (3am)? [Y/n] ").lower() in ("","y"): try: existing_cron = subprocess.run(["crontab","-l"], capture_output=True, text=True).stdout if "pgmemory" not in existing_cron: proc = subprocess.run(["crontab","-"], input=existing_cron.rstrip()+"\n"+decay_cmd+"\n", text=True, capture_output=True) if proc.returncode == 0: ok("Cron job added") else: warn(f"Could not add cron: {proc.stderr}") ``` ### Technical Analysis `SKILL_DIR` and the user-controlled `--config` path are interpolated directly into a crontab command without shell quoting. Cron executes command fields through a shell. Spaces or shell metacharacters in either path can alter command parsing. A malicious or misleading configuration filename containing characters such as `;`, command substitution, or redirection can append an arbitrary command to the cron entry. The injected command then runs daily as the account that installed the crontab. The scheduled decay task itself is declared functionality and is installed only after interactive confirmation. In `--yes` mode the code prints the entry instead of installing it. The security defect is the unsafe construction of the persistent shell command, not the legitimate decay scheduling objective. ### Attack Path 1. An attacker causes the user to invoke setup with a valid configuration file whose path contains shell metacharacters, for example a path that ...[truncated 697 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Quote every filesystem path with `shlex.quote` before writing a crontab command. - Prefer a small wrapper executable at a fixed, validated path rather than composing a complex shell command. - Reject config and Skill paths containing newline or carriage-return characters. - Use a per-user systemd timer with an argument array where available, while still requiring consent. - Write logs under a private state directory rather than the predictable shared path `/tmp/pgmemory-decay.log`. - Mark the cron entry with a precise unique comment and update only that exact entry. - Display the exact quoted command and require explicit approval. - Validate after installation that the parsed crontab contains only the intended executable and arguments. ]]>

T01 · Skill Instruction Hijacking

Warning
Location
scripts/setup.py:606
Finding
Setup Persistently Modifies Agent Instruction Files and Makes Skill Execution Mandatory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/setup.py:606-632`, invoked at `scripts/setup.py:727-749` and `scripts/setup.py:896-924` **Vulnerability Type**: Persistent agent instruction modification **Risk Level**: Medium ### Vulnerable Code ```python def inject_startup_into_agents_md(agents_md: Path, agent_name: str, config_path: Path) -> bool: content = agents_md.read_text() # Already injected? if PGMEMORY_STARTUP_MARKER in content: return True pattern = r'((?:^[ \t]*\d+\. .+\n)+)' matches = list(re.finditer(pattern, content, re.MULTILINE)) if not matches: return False # Use the last numbered list (most likely to be the startup steps) last_match = matches[-1] insert_pos = last_match.end() step = startup_steps(agent_name, config_path) new_content = content[:insert_pos] + step + "\n" + content[insert_pos:] agents_md.write_text(new_content) return True ``` The inserted content is explicitly mandatory: ```python return f"""\ **Query pgmemory instead of reading markdown files** — faster, focused, survives compaction. ... python3 {SKILL_DIR}/scripts/query_memory.py --importance 3 --limit 20 python3 {SKILL_DIR}/scripts/query_memory.py "brief description of current work" ``` ### Technical Analysis The setup wizard and `--sync-agents` modify `AGENTS.md`, a persistent instruction source used by OpenClaw agents. The injected text changes future startup behavior, declares pgmemory the default memory system, and directs agents to execute the Skill before tasks. The injection algorithm does not actually identify an “Every Session” section. It selects the last contiguous numbered list anywhere in the file, so the mandatory instruction can be inserted into an unrelated instruction block. The full reference section is then appended separately. This modification is documented and normally user-approved. It does not insert attacker-supplied instruction text, and no direc ...[truncated 1442 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Keep AGENTS.md integration optional and disabled under unattended `--yes` setup. - Require separate, explicit consent for every workspace modified by `--sync-agents`. - Generate a standalone integration snippet for manual review instead of editing agent instructions automatically. - If automatic integration remains: - Locate a precise, explicitly marked section rather than the last numbered list. - Show a diff before writing. - Create a timestamped backup. - Use an atomic write. - Reject symlink targets and paths outside approved workspace roots. - Avoid wording that overrides existing memory policy, such as “mandatory” or “instead of reading markdown files.” - Provide a supported `--remove-agent-integration` operation that cleanly removes both markers and restores the prior file. ]]>

T08 · Insecure Dependencies

Warning
Location
requirements.txt:1
Finding
Runtime and Container Dependencies Are Not Reproducibly Pinned<![CDATA[ ## Vulnerability Details **File Location**: `requirements.txt:1`; container references at `assets/docker-compose.yml:9` and `scripts/setup.py:499` **Vulnerability Type**: Unbounded dependency and mutable container resolution **Risk Level**: Medium ### Vulnerable Code ```text psycopg2-binary>=2.9 ``` ```yaml image: pgvector/pgvector:pg17 ``` ### Technical Analysis The Python dependency specifies only a minimum version, allowing future releases to be selected automatically. No lock file or hash verification is supplied. The Docker image uses a mutable tag rather than an immutable digest. These dependencies are recognizable legitimate projects; the audit found no evidence that the currently named packages are malicious. The weakness is that future installation can resolve to code different from what was reviewed. An upstream account compromise, malicious release, registry compromise, or unexpected incompatible release could introduce code into the Python process or database container. This is especially significant because `psycopg2-binary` includes native binary components and the container stores sensitive persistent memory. ### Attack Path 1. An upstream package or container publishing account is compromised, or a harmful future release is published. 2. A user installs dependencies with `pip install -r requirements.txt` or starts the Compose service later. 3. The package manager resolves a newer Python artifact or changed `pg17` container image. 4. Unreviewed code runs in the agent user's Python environment or in the database container. 5. The code can access database credentials, memory content, mounted volumes, and available network resources. ### Impact Assessment A compromised Python dependency executes with the installing or runtime user's privileges. A compromised database image can access or corrupt the complete pgmemory volume and may attack the Docker host through exposed interfaces or runtime vulnerabilities. Exploitation requires ...[truncated 111 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Pin Python dependencies to reviewed exact versions. - Publish a lock file containing hashes for every supported platform. - Install with hash enforcement, such as `pip install --require-hashes`. - Use a dependency update process that reviews release notes and reruns security tests. - Pin the Docker image to a version and immutable digest, for example `image: repository:version@sha256:...`. - Use automated vulnerability scanning for both Python wheels and container images. - Document the tested Python, PostgreSQL, pgvector, and psycopg2 version matrix. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Rogue AgentSelf-Modification, Session Persistence
Findings (56)

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

Critical
Category
Data Flow
Content
data = json.dumps({"input": [text], "model": model or "voyage-3"}).encode()
            req  = urllib.request.Request("https://api.voyageai.com/v1/embeddings", data=data,
                       headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"})
            return json.loads(urllib.request.urlopen(req, timeout=15).read())["data"][0]["embedding"]
        elif provider == "openai":
            data = json.dumps({"input": text, "model": model or "text-embedding-3-small"}).encode()
            req  = urllib.request.Request("https://api.openai.com/v1/embeddings", data=data,
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

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

Critical
Category
Data Flow
Content
data = json.dumps({"input": [text], "model": model or "voyage-3"}).encode()
            req  = urllib.request.Request("https://api.voyageai.com/v1/embeddings", data=data,
                       headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"})
            return json.loads(urllib.request.urlopen(req, timeout=15).read())["data"][0]["embedding"]
        elif provider == "openai":
            data = json.dumps({"input": text, "model": model or "text-embedding-3-small"}).encode()
            req  = urllib.request.Request("https://api.openai.com/v1/embeddings", data=data,
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

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

Critical
Category
Data Flow
Content
data = json.dumps({"model": model or "nomic-embed-text", "prompt": text}).encode()
            req  = urllib.request.Request("http://localhost:11434/api/embeddings", data=data,
                       headers={"Content-Type": "application/json"})
            return json.loads(urllib.request.urlopen(req, timeout=15).read())["embedding"]
    except Exception as e:
        print(f"Embedding failed: {e}", file=sys.stderr)
        return None
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

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

Critical
Category
Data Flow
Content
data = json.dumps({"input": [text], "model": m}).encode()
            req  = urllib.request.Request("https://api.voyageai.com/v1/embeddings", data=data,
                       headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"})
            resp = json.loads(urllib.request.urlopen(req, timeout=15).read())
            return resp["data"][0]["embedding"]
        elif provider == "openai":
            m = model or "text-embedding-3-small"
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

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

Critical
Category
Data Flow
Content
data = json.dumps({"input": [text], "model": m}).encode()
            req  = urllib.request.Request("https://api.voyageai.com/v1/embeddings", data=data,
                       headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"})
            resp = json.loads(urllib.request.urlopen(req, timeout=15).read())
            return resp["data"][0]["embedding"]
        elif provider == "openai":
            m = model or "text-embedding-3-small"
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

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

Critical
Category
Data Flow
Content
data = json.dumps({"input": [text], "model": m}).encode()
            req  = urllib.request.Request("https://api.voyageai.com/v1/embeddings", data=data,
                       headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"})
            resp = json.loads(urllib.request.urlopen(req, timeout=15).read())
            return resp["data"][0]["embedding"]
        elif provider == "openai":
            m = model or "text-embedding-3-small"
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

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

Critical
Category
Data Flow
Content
data = json.dumps({"input": [text], "model": model or "voyage-3"}).encode()
            req  = urllib.request.Request("https://api.voyageai.com/v1/embeddings", data=data,
                       headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"})
            resp = json.loads(urllib.request.urlopen(req, timeout=15).read())
            return resp["data"][0]["embedding"]
        elif provider == "openai":
            data = json.dumps({"input": text, "model": model or "text-embedding-3-small"}).encode()
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

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

Critical
Category
Data Flow
Content
data = json.dumps({"input": [text], "model": model or "voyage-3"}).encode()
            req  = urllib.request.Request("https://api.voyageai.com/v1/embeddings", data=data,
                       headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"})
            resp = json.loads(urllib.request.urlopen(req, timeout=15).read())
            return resp["data"][0]["embedding"]
        elif provider == "openai":
            data = json.dumps({"input": text, "model": model or "text-embedding-3-small"}).encode()
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

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

Critical
Category
Data Flow
Content
data = json.dumps({"input": [text], "model": model or "voyage-3"}).encode()
            req  = urllib.request.Request("https://api.voyageai.com/v1/embeddings", data=data,
                       headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"})
            resp = json.loads(urllib.request.urlopen(req, timeout=15).read())
            return resp["data"][0]["embedding"]
        elif provider == "openai":
            data = json.dumps({"input": text, "model": model or "text-embedding-3-small"}).encode()
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Hidden Instructions

High
Category
Prompt Injection
Content
- **pgmemory replaces markdown file reads at startup** — loading MEMORY.md + daily files dumps thousands of lines into context; pgmemory returns the 20 most relevant memories via semantic search, far faster and more focused. AGENTS.md startup now says "query pgmemory instead of reading markdown files"
- **AGENTS.md integration is now startup-mandatory** — setup and `--sync-agents` inject pgmemory startup steps directly into the "Every Session" numbered list in AGENTS.md, so agents run memory queries before every task (not just when they remember to)
- **Smarter injection** — `inject_startup_into_agents_md()` finds the last numbered startup list in AGENTS.md and appends pgmemory as the next step in-place, rather than only appending to end of file
- **Section marker** — pgmemory blocks in AGENTS.md are tagged with `<!-- pgmemory:section -->` and `<!-- pgmemory:startup -->` so re-runs are idempotent and the section is easy to find/update
- **Clearer instructions** — startup block now explicitly states "This is not optional" and explains why (decisions, constraints, infrastructure facts)
- **`scaffold_agents_md` generates reference + startup separately** — `startup_steps()` for injection, `scaffold_agents_md()` for the full reference section
Confidence
92% confidence
Finding
The skill is designed to inject persistent startup instructions and hidden marker comments into `AGENTS.md`, including language that the behavior is 'not optional'. This is dangerous because it modifies agent control flow and context-loading behavior in a durable, hard-to-notice way, creating a prompt-injection/persistence mechanism that can influence future sessions without clear per-session user approval.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The declared purpose frames the skill as persistent memory, but the documented behavior expands into host modification, Docker/container management, crontab changes, workspace enumeration, file modification, and outbound API calls. That mismatch is dangerous because users may consent to a storage feature without realizing the skill can alter the system and exfiltrate data to third-party embedding providers.

Description-Behavior Mismatch

High
Confidence
98% confidence
Finding
This setup flow performs host software installation, starts services, and alters user/group configuration, which is materially broader than configuring a semantic memory skill. Such scope expansion is dangerous because a user may run the script expecting app setup, yet it can make persistent privileged changes to the machine.

Context-Inappropriate Capability

High
Confidence
99% confidence
Finding
This code both installs software from a remote script and changes host service/user configuration, including Docker-group membership that can enable root-equivalent container control. For a semantic memory skill, these actions are unjustified and create a disproportionate host-compromise and privilege-escalation risk.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
system = platform.system().lower()
    if system == "linux":
        if not yes and input("  Install Docker via get.docker.com? [y/n] ").lower() != "y": return False
        if subprocess.run("curl -fsSL https://get.docker.com | sh", shell=True).returncode != 0:
            err("Docker install failed"); return False
        subprocess.run(["sudo","systemctl","enable","--now","docker"], capture_output=True)
        user = os.environ.get("USER","")
Confidence
98% confidence
Finding
Using `subprocess.run(..., shell=True)` with a pipeline to execute a downloaded remote script creates a powerful code-execution primitive and bypasses normal package integrity controls. Even though the command string is static, the shell plus remote content makes the host dependent on external script behavior and network trust.

Context-Inappropriate Capability

Medium
Confidence
96% confidence
Finding
The changelog explicitly introduces storing the embedding API key directly in `pgmemory.json` and prefers config over environment variables. Persisting secrets in a regular config file materially increases the risk of credential exposure through source control, workspace sync, backups, logs, or accidental sharing, and this is not necessary for the core purpose of semantic memory.

Natural-Language Policy Violations

Medium
Confidence
83% confidence
Finding
The changelog states the startup block explicitly says "This is not optional," which is a natural-language instruction removing user discretion. This can be a policy concern because it imposes mandatory behavior on users/agents without offering opt-in or contextual choice.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The README explicitly supports third-party embedding providers and presents them as normal configuration options, but it does not clearly warn that memory contents may be transmitted to external services for embedding. Because this skill is designed to persist agent decisions, constraints, and infrastructure facts, users may store sensitive operational data and unintentionally disclose it to vendors when semantic indexing occurs.

Session Persistence

Medium
Category
Rogue Agent
Content
clawhub install pgmemory
python3 ~/.openclaw/skills/pgmemory/scripts/setup.py

# 2. Write a memory
python3 ~/.openclaw/skills/pgmemory/scripts/write_memory.py \
  --key "infra.db.prod" \
  --content "Production DB at 10.10.0.1:5432, pgbouncer on same host" \
Confidence
97% confidence
Finding
The README encourages persisting long-lived semantic memory and even gives an example of storing internal database connection details in memory. In this skill’s context, session persistence is not inherently unsafe, but persisting sensitive infrastructure knowledge across sessions materially increases the blast radius of prompt injection, memory exfiltration, lateral movement, and accidental disclosure—especially when combined with searchable retrieval and optional third-party embeddings.

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill exposes capabilities consistent with shell, file, environment, and network access, but the manifest does not declare any tool scope or permission boundaries. That makes the operational trust model unclear and can lead users or agents to invoke a skill that performs privileged actions without explicit authorization expectations.

Missing User Warnings

Medium
Confidence
87% confidence
Finding
The skill encourages storing decisions, constraints, infrastructure facts, and discoveries across sessions, but does not warn that these memories may include secrets, sensitive architecture details, or private user data. In the context of semantic memory backed by PostgreSQL and external embedding providers, this creates a realistic risk of long-term retention and possible third-party disclosure.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The documentation tells users to run a setup wizard that 'handles everything' including Docker, migrations, scaffolding, and cron, but it does not clearly warn that this will change the host system and user configuration. A one-command setup with undisclosed side effects increases the chance of unintended persistence, service installation, or scheduled-task creation.

Session Persistence

Medium
Category
Rogue Agent
Content
## Core commands

### Write a memory

```bash
python3 ~/.openclaw/skills/pgmemory/scripts/write_memory.py \
Confidence
83% confidence
Finding
This skill is explicitly designed for session persistence, so the persistence itself is intentional; however, it still creates a security risk because arbitrary user or agent-generated content can be stored indefinitely and resurfaced later. In a memory skill, that context makes persistence expected but also more dangerous if sensitive or poisoned content is written and later trusted automatically.

Description-Behavior Mismatch

Medium
Confidence
96% confidence
Finding
The file docstring frames this script as a tool to 'search and inspect memories', but the implemented CLI also restores archived records back into the active table and promotes memories from another agent namespace into the current namespace. Those are write and transfer operations, not merely inspection, so the documented behavior understates the script's actual scope.

External Transmission

Medium
Category
Data Exfiltration
Content
try:
        if provider == "voyage":
            data = json.dumps({"input": [text], "model": model or "voyage-3"}).encode()
            req  = urllib.request.Request("https://api.voyageai.com/v1/embeddings", data=data,
                       headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"})
            return json.loads(urllib.request.urlopen(req, timeout=15).read())["data"][0]["embedding"]
        elif provider == "openai":
Confidence
87% confidence
Finding
Semantic search sends the user's query text to a third-party embedding provider (Voyage). In a memory tool, queries may contain sensitive operational context, secrets, or personal data, so this external transmission creates a real confidentiality risk if users are not fully aware or if the deployment assumes data stays local.

External Transmission

Medium
Category
Data Exfiltration
Content
return json.loads(urllib.request.urlopen(req, timeout=15).read())["data"][0]["embedding"]
        elif provider == "openai":
            data = json.dumps({"input": text, "model": model or "text-embedding-3-small"}).encode()
            req  = urllib.request.Request("https://api.openai.com/v1/embeddings", data=data,
                       headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"})
            return json.loads(urllib.request.urlopen(req, timeout=15).read())["data"][0]["embedding"]
        elif provider == "ollama":
Confidence
87% confidence
Finding
This branch transmits query content to OpenAI for embeddings, which is a real data egress path. Given the skill's purpose as persistent memory search, the transmitted text can plausibly include sensitive internal knowledge, making the external call security-relevant even though it is intentional.

Static analysis

No suspicious patterns detected.