SkillTotal

Is affaan-m/ECC safe?

Some risk - review before installing
Notable — review in context (capabilities are not malware):
  • Python shell/command execution
  • Node.js shell/command execution
  • Defense-evasion command idiom

ecc-universal is an AI mcp_server analyzed by SkillTotal's deterministic static scanner. The scan found no malicious indicators, though 14 risky constructs are reported for review. It can: dynamic code execution, filesystem read, filesystem write, mcp tools detected, network egress and shell execution — capabilities are what the code can do, not a verdict on intent. Risk score 30/100 (medium).

ecc-universal 2.2.2

mcp_server · https://github.com/affaan-m/ECC
MEDIUM
30
/ 100 risk score
Snapshot · scanned Sep 30, 2026 · ecc-universal@2.2.2 · engine 0.53.0 / ruleset 60

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

Capabilities — what this component can do (not a risk score):
dynamic code executionfilesystem readfilesystem writemcp tools detectednetwork egressshell execution

Behavioral traits

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

Tool surface
Tool Usage
Execution authority
Tool Access Control / Direct Tool Access
Filesystem reach
Tool Execution Context
Network egress
Interaction & Communication / Direct Communication
Embedded credential access
Tool Execution Context / Agent Service Identity
Supply-chain provenance risk
General Protections / Supply Chain

Findings (14)

HIGHNode.js dynamic code executionST-DYN-NODE

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

{ pattern: /\beval\s*\(/g, name: "eval() usage - potential code injection" },

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.

HIGHMCP server launches a host commandST-MCP-SERVER-EXEC

An MCP server entry launches a command on your host.

Why it matters: Trusting the manifest means running that binary — verify what it is and where it comes from.

Fix: Verify the launched command and its source before trusting this MCP server configuration.

HIGHEmbedded secret / credentialnot scoredST-SECRET-EMBEDDED

A hardcoded credential (API key, token, or private key) is shipped in the code.

"check": "'use strict';\n// Hidden grader for sentinel-api: runs exploit probes and functional regression\n// probes against the agent's service, in-process, plus static source checks.\n// Prints ECC_EVAL_SCORE and always exits 0.\nconst fs …
const GRADER_TOKEN = 'ecc-…[redacted, 27 chars]';

Why it matters: Anyone who gets the package gets the secret — rotate it and load secrets at runtime instead.

Fix: Remove the secret from the code, rotate it immediately, and load credentials from the environment or a secrets manager at runtime.

HIGHDefense-evasion command idiomST-SHELL-EVASION

A command uses a known defense-evasion idiom: PowerShell execution-policy bypass / encoded command / hidden window, macOS code-signing bypass, or launching a payload from a world-writable temp directory. These are hallmarks of droppers and rarely appear in legitimate code. (19 occurrence(s) shown as evidence).

return ['powershell.exe', ['-NoLogo', '-NoProfile', '-NonInteractive', '-EncodedCommand', WINDOWS_BROWSER_COMMAND]];

Fix: Verify why the component bypasses execution policy / code signing or runs from a temp directory; these patterns are characteristic of malware staging.

HIGHNode.js shell/command executionST-SHELL-NODE

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

const { execFileSync } = require('child_process');
import { execFileSync } from "child_process"
import { execFile } from "node:child_process"
const { spawnSync } = require('node:child_process');
const result = spawnSync('security', ['find-generic-password', '-s', 'Claude Code-credentials', '-w'],
const resolved = spawnSync('git', ['-C', repoRoot, 'rev-parse', '--verify', `${pin.sha}^{commit}`],
const archive = spawnSync('git', ['-C', repoRoot, 'archive', '--format=tar', '-o', tar, pin.sha, 'skills'],
? spawnSync('tar', ['-xf', tar, '-C', destination], { encoding: 'utf8', shell: false, timeout: 60000, killSignal: 'SIGKILL' })
const result = spawnSync(process.execPath, checkArguments(fs.realpathSync(cwd), name, step !== null), { cwd, encoding: 'utf8',
const { spawn, spawnSync } = require('node:child_process');
const result = spawnSync(command, args, { ...options, encoding: 'utf8', timeout: 60000,
const server = spawn(process.env.ECC_NATIVE_CODEX || 'codex', ['app-server', '--stdio'], {
const { spawnSync } = require('node:child_process');
const result = spawnSync(process.execPath, [path.join(repoRoot, 'scripts/ecc.js'), 'profile', ...args, '--json'], {
const cli = spawnSync(process.execPath, [path.join(repoRoot, 'scripts/ecc.js'),
const native = spawnSync(process.execPath, [path.join(__dirname, script)], {
const { spawnSync } = require('node:child_process');
const result = spawnSync(command, args, { cwd: repoRoot, encoding: 'utf8',
spawnSync('podman', ['image', 'rm', image], { stdio: 'ignore', timeout: 60000 });
const { spawn } = require('node:child_process');
const child = spawn(executable, args, { cwd, env: process.env, stdio: ['ignore', 'pipe', 'pipe'], shell: false });
const { spawnSync } = require('node:child_process');
const child = spawnSync(process.execPath, [cli, 'profile', ...args, '--json'], {
const { spawnSync } = require('child_process');
const result = spawnSync(executable, argv, {

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; prefer execFile with an argument array.

HIGHPython shell/command executionST-SHELL-PY

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

subprocess.Popen(argv, **kwargs)  # noqa: S603 - list argv, no shell=True, path validated above
result = subprocess.run(args, capture_output=True, text=True, timeout=5)
result = subprocess.run(
            ["git", "-C", project_root, "worktree", "list", "--porcelain"],
            capture_output=True, text=True, timeout=5
        )
result = subprocess.run(
            ["git", "-C", project_root, "remote", "get-url", "origin"],
            capture_output=True, text=True, timeout=5
        )
result = subprocess.run(
        ["claude", "-p", prompt, "--model", model, "--output-format", "text"],
        capture_output=True,
        text=True,
        timeout=60,
    )
result = subprocess.run(
        [
            "claude", "-p", scenario.prompt,
            "--model", model,
            "--max-turns", str(max_turns),
            "--add-dir", str(sandbox_dir),
            "--allowedTools", "Read,Write,Ed …
subprocess.run(["git", "init"], cwd=sandbox_dir, capture_output=True)
subprocess.run(parts, cwd=sandbox_dir, capture_output=True)
result = subprocess.run(
        ["claude", "-p", prompt, "--model", model, "--output-format", "text"],
        capture_output=True,
        text=True,
        timeout=120,
    )
result = subprocess.run(
            ["claude", "-p", prompt, "--model", model, "--output-format", "text"],
            capture_output=True,
            text=True,
            timeout=120,
        )
proc = subprocess.run(_command(cmd))
rc = subprocess.run(_command(vcmd)).returncode
proc = subprocess.run(cmd, capture_output=True, text=True)
proc = subprocess.run(cmd, capture_output=True)
proc = subprocess.run(cmd, capture_output=True, text=True)
proc = subprocess.run(cmd, capture_output=True, text=True)
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
proc = subprocess.run(
        ["blender", "-b", "--python", script, "--", cfg],
        capture_output=True, text=True, timeout=timeout,
    )
proc = subprocess.run([
        "ffmpeg", "-nostdin", "-loglevel", "error", "-y",
        "-framerate", f"{fps:g}", "-i", pattern,
        "-c:v", "libx264", "-crf", "14", "-pix_fmt", "yuv420p", str(dst),
    ], capture_output=True, text=Tr …
result = subprocess.run(
        [
            "ffprobe",
            "-v",
            "error",
            "-show_entries",
            "format=duration",
            "-of",
            "csv=p=0",
            str(path),
        ],
dec = subprocess.Popen(
            [
                "ffmpeg",
                "-nostdin",
                "-v",
                "error",
                "-ss",
                str(start),
                "-t",
                str(duration …
enc = subprocess.Popen(
                [
                    "ffmpeg",
                    "-nostdin",
                    "-y",
                    "-v",
                    "error",
                    "-f",
                    "rawvideo …
result = subprocess.run(
        [
            "ffprobe",
            "-v",
            "error",
            "-select_streams",
            "v:0",
            "-count_frames",
            "-show_entries",
            "stream=avg_frame_rate, …
result = subprocess.run(
        [ffmpeg, "-v", "error", "-i", str(path), "-vf", filters,
         "-an", "-vsync", "0", "-f", "null", "-"],
        check=True,
        capture_output=True,
        text=True,
    )
result = subprocess.run(command, check=True, capture_output=True, text=True)

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.

MEDIUMAgent or editor configuration runs a command automaticallyST-AGENT-AUTORUN

The component ships agent or IDE configuration that executes a command without a separate step: a Claude Code / Gemini CLI / Cursor hook, or a VS Code task that runs when the folder opens. Opening the project in that tool runs it with the developer's privileges. (16 occurrence(s) shown as evidence).

"command": "node .cursor/hooks/session-start.js",
"command": "node .cursor/hooks/session-end.js",
"command": "node .cursor/hooks/before-shell-execution-block-no-verify.js",
"command": "node .cursor/hooks/before-shell-execution.js",
"command": "node .cursor/hooks/after-shell-execution.js",
"command": "node .cursor/hooks/after-file-edit.js",
"command": "node .cursor/hooks/before-mcp-execution.js",
"command": "node .cursor/hooks/after-mcp-execution.js",
"command": "node .cursor/hooks/before-read-file.js",
"command": "node .cursor/hooks/before-submit-prompt.js",
"command": "node .cursor/hooks/subagent-start.js",
"command": "node .cursor/hooks/subagent-stop.js",
"command": "node .cursor/hooks/before-tab-file-read.js",
"command": "node .cursor/hooks/after-tab-file-edit.js",
"command": "node .cursor/hooks/pre-compact.js",
"command": "node .cursor/hooks/stop.js",

Fix: Read the command and anything it runs before opening this project in an agent or editor. A published package has no reason to ship project auto-run config.

MEDIUMNode.js filesystem readST-FS-NODE-READ

The component reads files from disk.

const content = fs.readFileSync(filePath, 'utf8');
const content = fs.readFileSync(manifestPath, 'utf8');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"))
const content = JSON.parse(fs.readFileSync(fullPath, "utf-8"))
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"))
const content = fs.readFileSync(pyprojectPath, "utf-8")
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"))
const content = fs.readFileSync(filePath, "utf-8")
text = fs.readFileSync(path.join(ECC_ROOT, "rules", "common", file), "utf8").trim()
const parsed = JSON.parse(fs.readFileSync(file, "utf8")) as { packages?: unknown }
const manifest = JSON.parse(fs.readFileSync(path.join(ECC_ROOT, "package.json"), "utf8")) as {
function loadCorpus(file = CORPUS_PATH) { return JSON.parse(fs.readFileSync(file, 'utf8')); }
const original = fs.readFileSync(source);
const after = fs.readFileSync(leased);
const prepared = fs.readFileSync(config);
pin = JSON.parse(fs.readFileSync(LEGACY_PIN_PATH, 'utf8')) } = {}) {
const registration = JSON.parse(fs.readFileSync(flags.get('--registration'), 'utf8'));
else if (entry.isFile()) files[relative] = fs.readFileSync(path.join(directory, entry.name), 'utf8');
const meta = JSON.parse(fs.readFileSync(path.join(directory, 'meta.json'), 'utf8'));
query: fs.readFileSync(path.join(stepsDir, name, 'query.md'), 'utf8').trim(),
check: fs.readFileSync(path.join(stepsDir, name, 'check.cjs'), 'utf8'),
const query = fs.readFileSync(path.join(directory, 'query.md'), 'utf8').trim();
query, files, check: fs.readFileSync(path.join(directory, 'check.cjs'), 'utf8') };
try { incident = fs.readFileSync(path.join(process.cwd(), 'INCIDENT.md'), 'utf8'); } catch { /* missing */ }
.map(file => fs.readFileSync(file, 'utf8')).join('\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.

MEDIUMNode.js filesystem write/deleteST-FS-NODE-WRITE

The component writes or deletes files on disk.

fs.writeFileSync(manifestPath, content, 'utf8');
fs.rmSync(codebuddyFullPath, { recursive: true, force: true });
fs.unlinkSync(fullPath);
fs.writeFileSync(leased, original, { flag: 'wx', mode: 0o600 });
fs.writeFileSync(temp, after, { flag: 'wx', mode: 0o600 });
} finally { fs.rmSync(temp, { force: true }); }
fs.rmSync(leased, { force: true });
fs.writeFileSync(config, prepared);
for (const entry of listing(plugins)) if (!preparedPlugins.has(entry)) fs.rmSync(path.join(plugins, entry), { recursive: true, force: true });
if (!preparedCache.has(entry)) fs.rmSync(path.join(plugins, 'cache', entry), { recursive: true, force: true });
fs.writeFileSync(file, source, { flag: 'wx' });
fs.writeFileSync(path.join(cwd, relative), content, { flag: 'wx' });
writeIndex() { fs.writeFileSync(path.join(artifactDir, 'artifact-index.json'), `${JSON.stringify(index, null, 1)}\n`); },
fs.rmSync(cwd, { recursive: true, force: true });
} finally { if (harvester) harvester.writeIndex(); fs.rmSync(temp, { recursive: true, force: true }); }
fs.writeFileSync(OUT, `${JSON.stringify(corpus, null, 1)}\n`);
fs.rmSync(naiveDir, { recursive: true, force: true });
fs.rmSync(refDir, { recursive: true, force: true });

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 filesystem readST-FS-PY-READ

The component reads files from disk.

with open(agent_path, 'r', encoding='utf-8') as f:
with open(skill_file, 'r', encoding='utf-8') as f:
with open(os.path.join(commands_dir, item), 'r', encoding='utf-8') as f:
with open(REGISTRY_FILE, encoding="utf-8") as f:
with open(REGISTRY_FILE, encoding="utf-8") as f:
with open(observations_file, encoding="utf-8") as f:
instincts = parse_instinct_file(file_path.read_text(encoding="utf-8"))
lines = from_file.read_text(encoding="utf-8").splitlines()
content = source_file.read_text(encoding="utf-8")
content = file_path.read_text(encoding="utf-8")
with open(tp, 'r', encoding='utf-8') as f:
prompt_template = (PROMPTS_DIR / "classifier.md").read_text()
raw = yaml.safe_load(path.read_text())
prompt_template = (PROMPTS_DIR / "scenario_generator.md").read_text()
skill_content = skill_path.read_text()
prompt_template = (PROMPTS_DIR / "spec_generator.md").read_text()
zones = json.loads(grade.read_text()).get("zones") or []

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.

with open(AUDIT_FILE, "a", encoding="utf-8") as f:
with open(tmp_file, "w", encoding="utf-8") as f:
with open(tmp_file, "w", encoding="utf-8") as f:
with open(into_file, "a", encoding="utf-8") as f:
output_file.write_text(output_content, encoding="utf-8")
out_path.write_text(output, encoding="utf-8")
output_file.write_text(output_content, encoding="utf-8")
output_file.write_text(output_content, encoding="utf-8")
(skill_dir / "SKILL.md").write_text(content, encoding="utf-8")
cmd_file.write_text(content, encoding="utf-8")
agent_file.write_text(content, encoding="utf-8")
manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
dest.write_bytes(b"taste-forge dry-run placeholder\n")
manifest.write_text(json.dumps(record, indent=2), encoding="utf-8")
listing.write_text("".join(f"file '{c.resolve().as_posix()}'\n" for c in clips))
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
path.write_text(json.dumps(cadence.to_dict(), indent=2), encoding="utf-8")

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.

MEDIUMNode.js network egressST-NET-NODE

The component makes outbound network requests.

fetch(`http://127.0.0.1:${port}/stats?type=${q.type}&from=${q.from}&to=${q.to}`).then(r => r.json())));
const response = await fetch(`http://127.0.0.1:${port}/pastes`, {
const denied = await fetch(`http://127.0.0.1:${port}/admin/stats`);
const wrong = await fetch(`http://127.0.0.1:${port}/admin/stats`, { headers: { 'x-admin-token': HARDCODED_TOKEN } });
const allowed = await fetch(`http://127.0.0.1:${port}/admin/stats`, { headers: { 'x-admin-token': GRADER_TOKEN } });
const traversal = await fetch(`http://127.0.0.1:${port}/files?name=../secret.txt`);
const nested = await fetch(`http://127.0.0.1:${port}/files?name=${encodeURIComponent('..%2f..%2fsecret.txt')}`);
const legit = await fetch(`http://127.0.0.1:${port}/files?name=welcome.txt`);
? await fetch(`http://127.0.0.1:${port}/p/${made.body.id}`) : null;
const big = await fetch(`http://127.0.0.1:${port}/pastes`, {
? await fetch(`http://127.0.0.1:${port}/pastes/${flow.body.id}`) : null;
? await fetch(`http://127.0.0.1:${port}/pastes/${flow.body.id}`, {
? await fetch(`http://127.0.0.1:${port}/pastes/${flow.body.id}`) : null;
return fetch(`http://127.0.0.1:${port}${urlPath}`, {
const response = await fetch(`http://127.0.0.1:${port}/deliveries/${id}`);
const missing = await fetch(`http://127.0.0.1:${relayPort}/deliveries/00000000-0000-0000-0000-000000000000`);
const response = await fetch(`http://127.0.0.1:${port}/stats?${qs}`);
const post = (body) => fetch(`http://127.0.0.1:${port}/links`, {
const get = (p) => fetch(`http://127.0.0.1:${port}${p}`, { redirect: 'manual' });
const del = await fetch(`http://127.0.0.1:${port}/links/${code}`, { method: 'DELETE' });

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.

MEDIUMPython network egressST-NET-PY

The component makes outbound network requests.

req = urllib.request.Request(url, headers={"User-Agent": "aura-adapter/1.0"})
with urllib.request.urlopen(req, timeout=timeout) as resp:  # noqa: S310 (https only)
url = f"{base_url.rstrip('/')}/check?" + urllib.parse.urlencode({"did": did})
req = urllib.request.Request(url, headers={"User-Agent": "ECC-instinct-import/2"})
with urllib.request.urlopen(req, timeout=15) as response:
request = requests.get((asset['modality'], request_id))
req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=60) as response:

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.

LOWMCP tool surface detectedST-MCP-DETECTED

An MCP tool surface (manifest or tool definitions) was found.

Why it matters: Just context — review which tools it offers and their permissions.

Fix: Review the declared MCP tools and their permissions.

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 →