SkillTotal

Is FoundationAgents/MetaGPT safe?

Some risk - review before installing
Notable — review in context (capabilities are not malware):
  • Python shell/command execution
  • Python dynamic code execution
  • Possible command injection (shell + dynamic command)

What to do: Read the findings below before installing: each one opens the exact line of code it was found on.

FoundationAgents/MetaGPT is a PyPI package analyzed by SkillTotal's deterministic static scanner. The scan found no malicious indicators, though 2 risky constructs are reported for review. It can: dynamic code execution, filesystem read, filesystem write, install time execution, network egress and shell execution — capabilities are what the code can do, not a verdict on intent. Risk score 40/100 (medium).

repo

python_package · https://github.com/FoundationAgents/MetaGPT
MEDIUM
40
/ 100 risk score
Snapshot · scanned Oct 8, 2026 · repo@11cdf46 · engine 0.56.4 / ruleset 62

Automated static-analysis result. It can contain false positives and false negatives, and is not a claim about the intent of FoundationAgents/MetaGPT's authors. Report a false positive.

Capabilities — what this component can do (not a risk score):
dynamic code executionfilesystem readfilesystem writeinstall time executionnetwork egressshell execution

Behavioral traits

How this component maps to the CSA agentic threat model. Descriptive — it never affects the risk score.

Execution authority
Tool Access Control / Direct Tool Access
Filesystem reach
Tool Execution Context
Network egress
Interaction & Communication / Direct Communication
Supply-chain provenance risk
General Protections / Supply Chain
Unsafe deserialization
General Protections / Input Validation

Findings (10)

HIGHPossible command injection (shell + dynamic command)ST-CMDI-PY

The code builds an OS command out of values that can change at runtime, then runs it through a shell.

res = subprocess.run(adb_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
result = subprocess.run(command, shell=True, check=True, cwd=str(output_dir))
result = os.system(check_command)
process = await asyncio.create_subprocess_shell(
                " ".join(commands), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
            )

Why it matters: If any of those values come from untrusted input, an attacker can run their own commands on the machine.

Fix: Pass arguments as a list without shell=True (e.g. subprocess.run(['git', 'checkout', branch])); never build a shell string from external input. If a shell is unavoidable, quote with shlex.quote.

HIGHUnsafe deserializationST-DESERIALIZE-PY

It loads data with a format that can rebuild arbitrary objects (e.g. pickle, or unsafe YAML).

Why it matters: Feeding such a loader untrusted data can execute code hidden inside that data.

Fix: Deserialize untrusted data with a safe format/loader: JSON, or yaml.safe_load / Loader=SafeLoader. Reserve pickle/marshal for data you fully control.

HIGHNode.js dynamic code executionST-DYN-NODE

The code turns strings into live code at runtime (eval / new Function / exec).

await eval("(async () => {" + programs + "\n" + code + "})()");

Why it matters: If those strings aren't fixed and trusted, they become a way to run arbitrary code.

Fix: Avoid evaluating dynamically constructed code; if unavoidable, ensure the input is a trusted constant and never derived from external data.

HIGHPython dynamic code executionST-DYN-PY

The code turns strings into live code at runtime (eval / new Function / exec).

extracted_data[field_name] = eval(raw_value)
extracted_data[field_name] = eval(raw_value)
groundingdino_model = load_model(file_path, device=device).eval()
thoughts = eval(thoughts)
compile(code, fname, "exec")  # USE TRACEBACK BELOW HERE
new_mapping[key] = eval(value)  # `"'(list[str], Ellipsis)"` to `(list[str], ...)`

Why it matters: If those strings aren't fixed and trusted, they become a way to run arbitrary code.

Fix: Avoid evaluating dynamically constructed code; if unavoidable, ensure the input is a trusted constant and never derived from external data.

HIGHPython install/build-time execution hookST-INSTALL-PY

The Python build/install configuration runs code at install time.

"""A custom command to run `npm install -g @mermaid-js/mermaid-cli` via a subprocess."""
subprocess.check_call(["npm", "install", "-g", "@mermaid-js/mermaid-cli"])
except subprocess.CalledProcessError as e:

Why it matters: Code that runs during pip install is a common supply-chain execution point.

Fix: Verify the build hook performs only a legitimate build step and does not execute commands or reach the network during installation.

HIGHPython shell/command executionST-SHELL-PY

The component can run operating-system commands or spawn processes.

process = subprocess.Popen(
            command, cwd=working_directory, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env
        )
return subprocess.run(cmd, check=check, cwd=cwd, env=env)
res = subprocess.run(adb_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
result = subprocess.run(command, shell=True, check=True, cwd=str(output_dir))
process = subprocess.Popen(cmd, cwd=self.root, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
result = subprocess.run(command, cwd=cwd, capture_output=True, text=True, env=env, timeout=timeout, shell=shell)
self.process = await asyncio.create_subprocess_exec(
            *self.shell_command,
            stdin=PIPE,
            stdout=PIPE,
            stderr=STDOUT,
            executable=self.executable,
            env=os.environ.copy(), …
process = await asyncio.create_subprocess_exec(
            sys.executable,
            "-m",
            "playwright",
            "install",
            *browsers,
            # "--with-deps",
            stdout=asyncio.subprocess.PIPE, …
result = os.system(check_command)
from git.repo.fun import is_git_dir
process = await asyncio.create_subprocess_shell(
                " ".join(commands), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
            )
subprocess.check_call(["npm", "install", "-g", "@mermaid-js/mermaid-cli"])

Why it matters: Powerful and often legitimate — confirm the commands aren't built from untrusted input.

Fix: Confirm the command and its arguments are fully controlled and not derived from untrusted input; avoid shell=True.

MEDIUMNode.js filesystem readST-FS-NODE-READ

The component reads files from disk.

const f = fs.readFileSync(file, "utf8").split("\n");

Why it matters: Usually legitimate, but worth confirming it can't be steered into reading sensitive files.

Fix: Confirm which files are read and that paths cannot be influenced by untrusted input to reach sensitive locations.

MEDIUMPython filesystem readST-FS-PY-READ

The component reads files from disk.

content = path.read_text()
content = data_path.read_text()
repo._set(file_path.read_text(), file_path)
with open(prompt_file_path, "r", encoding="utf-8") as file:
with open(graph_file_path, "r", encoding="utf-8") as file:
with open(file_path, "r") as file:
task_desc = self.task_desc_path.read_text()
with open(self.record_path, "r") as record_file:
doc_content = ast.literal_eval(doc_path.read_text())
doc_content = ast.literal_eval(doc_path.read_text())
doc_content = ast.literal_eval(doc_path.read_text())
with open(os.path.join(dataset_dir, task_file), encoding="utf-8") as f:
with open(f"{state['custom_dataset_dir']}/description.md", "r", encoding="utf-8") as file:
with open(dataset_info_path, "r") as file:
with open(os.path.join(self.state["node_dir"], f"Node-{self.id}.pkl"), "rb") as f:
with open(os.path.join(self.root_node.state["node_dir"], "node_order.json"), "r") as f:
with open(os.path.join(self.root_node.state["node_dir"], "Node-0.pkl"), "rb") as f:
with open(file_path, "r") as stream:
with open(template_path, "r", encoding="utf-8") as f:
return json.loads(result_path.read_text())
data = json.loads(result_file.read_text(encoding="utf-8"))

Why it matters: Usually legitimate, but worth confirming it can't be steered into reading sensitive files.

Fix: Confirm which files are read and that paths cannot be influenced by untrusted input to reach sensitive locations.

MEDIUMPython filesystem write/deleteST-FS-PY-WRITE

The component writes or deletes files on disk.

self.path.write_text(self.content, encoding="utf-8")
with open(filename, "wb") as file:
with open("tester.txt", "a") as f:
with open("tester.txt", "a") as f:
with open(output_path, "w", encoding="utf-8") as outfile:
with open(os.path.join(directory, "graph.py"), "w", encoding="utf-8") as file:
with open(os.path.join(directory, "prompt.py"), "w", encoding="utf-8") as file:
with open(os.path.join(directory, "__init__.py"), "w", encoding="utf-8") as file:
with open(patch_file, "w", encoding="utf-8") as file:
with open(Path(self.dataset_dir, self.name, "dataset_info.json"), "w", encoding="utf-8") as file:
shutil.copy(node_nb_dir, copy_nb_dir)

Why it matters: Usually legitimate, but worth confirming the paths can't be controlled by untrusted input.

Fix: Confirm which files are written/deleted and that paths cannot be influenced by untrusted input.

MEDIUMPython network egressST-NET-PY

The component makes outbound network requests.

res = requests.post(f"{self.server}/stop")
res = requests.post(
                f"{self.server}/start",
                json=self.reset_options,
                timeout=self.request_timeout,
            )
res = requests.post(f"{self.server}/step", json=data, timeout=self.request_timeout)
res = requests.post(f"{self.server}/pause")
res = requests.post(f"{self.server}/pause")
response = requests.get(url, stream=True)
requests.adapters.HTTPAdapter(max_retries=MAX_CONNECTION_RETRIES),
timeout = aiohttp.ClientTimeout(
                connect=request_timeout[0],
                total=request_timeout[1],
            )
timeout = aiohttp.ClientTimeout(total=request_timeout or TIMEOUT_SECS)
data, content_type = requests.models.RequestEncodingMixin._encode_files(files, data)  # type: ignore
async with aiohttp.ClientSession() as session:
from aiohttp import ClientSession
with requests.Session() as session:

Why it matters: Usually legitimate, but confirm the destinations are expected and no sensitive data leaves.

Fix: Confirm the destination hosts are expected and that no sensitive data is sent off-host.

How attackers abuse these capabilities

Interactive labs on the attack class behind the rules above. They show the technique, not anything found in FoundationAgents/MetaGPT.

Check your own component

Run the same evidence-backed scan on any MCP server, agent skill, or package.

Scan your own component

How we determine this: deterministic static analysis (regex + AST), evidence-anchored, no code execution. Methodology →