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

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
assets/docker-compose.yml:8
Finding

PostgreSQL Is Exposed on All Interfaces with Publicly Known Credentials

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/setup.py:83
Finding

Embedding API Keys and Database Credentials Are Persisted Without Enforced File Permissions

Content
View full analysis
Remediation
View remediation

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

Content
View full analysis
Remediation
View remediation

T06 · System Persistence

Warning
Location
scripts/setup.py:751
Finding

Unquoted Paths in Persistent Cron Entry Permit Shell Command Injection

Content
View full analysis
> /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
View remediation

T01 · Skill Instruction Hijacking

Warning
Location
scripts/setup.py:606
Finding

Setup Persistently Modifies Agent Instruction Files and Makes Skill Execution Mandatory

Content
View full analysis
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
View remediation

T08 · Insecure Dependencies

Warning
Location
requirements.txt:1
Finding

Runtime and Container Dependencies Are Not Reproducibly Pinned

Content
View full analysis
=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
View remediation
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
Confidence
90% confidence
Finding

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

Content

Scanner excerpt · scripts/query_memory.py (reported line 41)May include surrounding context.

python
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,

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

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

Content

Scanner excerpt · scripts/query_memory.py (reported line 46)May include surrounding context.

python
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,

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

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

Content

Scanner excerpt · scripts/query_memory.py (reported line 51)May include surrounding context.

python
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

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

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

Content

Scanner excerpt · scripts/setup.py (reported line 272)May include surrounding context.

python
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"

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

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

Content

Scanner excerpt · scripts/setup.py (reported line 279)May include surrounding context.

python
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"

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

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

Content

Scanner excerpt · scripts/setup.py (reported line 286)May include surrounding context.

python
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"

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

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

Content

Scanner excerpt · scripts/write_memory.py (reported line 36)May include surrounding context.

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

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

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

Content

Scanner excerpt · scripts/write_memory.py (reported line 42)May include surrounding context.

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

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

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

Content

Scanner excerpt · scripts/write_memory.py (reported line 48)May include surrounding context.

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

Hidden Instructions

High
Category
Prompt Injection
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.

Content

Scanner excerpt · CHANGELOG.md (reported line 22)May include surrounding context.

md
- **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

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.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

High
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

High
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Tool Parameter Abuse

High
Category
Tool Misuse
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.

Content

Scanner excerpt · scripts/setup.py (reported line 477)May include surrounding context.

python
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","")

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
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.

Content

Scanner excerpt · README.md (reported line 53)May include surrounding context.

md
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" \

Undeclared Tool Scope

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.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
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.

Content

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

Core commands

Write a memory

bash
python3 ~/.openclaw/skills/pgmemory/scripts/write_memory.py \

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
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.

Content

Scanner excerpt · scripts/query_memory.py (reported line 39)May include surrounding context.

python
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":

External Transmission

Medium
Category
Data Exfiltration
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.

Content

Scanner excerpt · scripts/query_memory.py (reported line 44)May include surrounding context.

python
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":

Static analysis

No suspicious patterns detected.