T03 · Remote Payload Retrieval and Execution
- Location
scripts/setup.py:473- Finding
Unverified Remote Docker Installer Is Piped Directly into a Shell
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
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.
scripts/setup.py:473Unverified Remote Docker Installer Is Piped Directly into a Shell
assets/docker-compose.yml:8PostgreSQL Is Exposed on All Interfaces with Publicly Known Credentials
scripts/setup.py:83Embedding API Keys and Database Credentials Are Persisted Without Enforced File Permissions
scripts/write_memory.py:23Memory Content and Search Terms Are Disclosed to External Embedding Providers Without a Data-Sensitivity Guard
scripts/setup.py:751Unquoted Paths in Persistent Cron Entry Permit Shell Command Injection
scripts/setup.py:606Setup Persistently Modifies Agent Instruction Files and Makes Skill Execution Mandatory
requirements.txt:1Runtime and Container Dependencies Are Not Reproducibly Pinned
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
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,
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
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,
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
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
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
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"
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
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"
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
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"
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
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()
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
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()
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
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()
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.
- **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
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.
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.
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.
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.
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","")
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.
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.
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.
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.
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" \
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.
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.
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.
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.
python3 ~/.openclaw/skills/pgmemory/scripts/write_memory.py \
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.
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.
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":
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.
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":
No suspicious patterns detected.