SkillTotal

Is affaan-m/ECC safe?

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

ecc-universal 2.0.0

mcp_server · https://github.com/affaan-m/ECC
CRITICAL
80
/ 100 risk score
Snapshot · scanned Jul 3, 2026 · ecc-universal@2.0.0 · engine 0.24.0 / ruleset 25
High-risk capabilities - review before installing
Notable — review in context (capabilities are not malware):
  • Sensitive-data access combined with network egress
  • Python shell/command execution
  • Node.js shell/command execution

No malicious indicators found by static analysis.

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 writeinstall time executionmcp tools detectednetwork egressshell execution

Findings (16)

CRITICALSensitive-data access combined with network egressST-COMBO-EXFIL

The component both reads secret locations (keys, tokens) and can send data over the network.

Why it matters: That combination is the classic path for quietly stealing credentials off your machine.

Fix: Verify that secrets read from disk are never transmitted off-host without explicit, auditable user consent.

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.

HIGHInstall-time hook paired with a dropper payloadST-INSTALL-DROPPER

The component executes code at install time (a package lifecycle / build hook) and also contains a decode-and-execute payload or accesses credential locations. This is the install-time dropper pattern behind recent supply-chain compromises.

"postinstall": "echo '\\n  ecc-universal installed!\\n  Run: npx ecc typescript\\n  Compat: npx ecc-install typescript\\n  Docs: https://github.com/affaan-m/ECC\\n'",

Fix: Inspect the install/build hook and the payload it runs; do not install until the install-time behavior is understood and trusted.

HIGHnpm install-time lifecycle hookST-INSTALL-NPM

package.json runs scripts automatically when the package is installed.

"postinstall": "echo '\\n  ecc-universal installed!\\n  Run: npx ecc typescript\\n  Compat: npx ecc-install typescript\\n  Docs: https://github.com/affaan-m/ECC\\n'",

Why it matters: Install scripts are a favorite supply-chain foothold — they execute on every machine that installs the package.

Fix: Inspect the hook command. Install-time scripts are a common supply chain execution vector; ensure they do nothing beyond a documented build step.

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.

HIGHSensitive path / secret-location referenceST-SENS-PATH

The component references credential locations like ~/.ssh or .aws/credentials.

Why it matters: Touching secret locations is a common first step before stealing them — confirm why it's needed.

Fix: Verify why the component references credential locations; reading these is a common precursor to secret exfiltration.

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"
const { spawnSync } = require('child_process');
const result = spawnSync(command, args, {
const { execFileSync } = require("node:child_process")
while ((match = indicatorPattern.exec(lowerText)) !== null) {
while ((match = pattern.exec(content)) !== null) {
const { spawnSync } = require('child_process');
// and Node's spawn() cannot resolve those wrappers via PATH without shell: true.
const result = spawnSync('claude', args, {
while ((match = regex.exec(safe)) !== null) {
const match = TOML_HEADER_RE.exec(raw);
const match = headerPattern.exec(raw);
const match = inlinePattern.exec(lines[index]);
const { execFileSync } = require('child_process');
const { spawn } = require('child_process');
const child = spawn('open', [url], {
try { const { spawn } = require('child_process'); const p = process.platform; const c = p === 'darwin' ? 'open' : p === 'win32' ? 'start' : 'xdg-open'; if (c === 'start') spawn('cmd', ['/c', 'start', `http://localhost:${PORT}`], { stdio: 'i …
const { spawnSync } = require('child_process');
const result = spawnSync(
const { spawnSync } = require('child_process');
const tmuxCheck = spawnSync('which', ['tmux'], { encoding: 'utf8' });
const { spawnSync, execFileSync } = require('child_process');
const result = spawnSync(path, ['-Command', 'exit 0'],
const result = spawnSync(pwshPath, ['-Command', command],

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

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 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")
pkgName = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')).name;
return fs.readFileSync(filePath, 'utf8');
text = fs.readFileSync(filePath, 'utf8');
const content = fs.readFileSync(path.join(root, relativePath), 'utf8');
current = fs.readFileSync(outputPath, 'utf8');
return fs.readFileSync(filePath, 'utf8');
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
content = fs.readFileSync(filePath, 'utf-8');
content = fs.readFileSync(filePath, 'utf-8');
data = JSON.parse(fs.readFileSync(HOOKS_FILE, 'utf-8'));
const schema = JSON.parse(fs.readFileSync(HOOKS_SCHEMA_PATH, 'utf-8'));
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
const content = fs.readFileSync(file, 'utf8');
const content = fs.readFileSync(filePath, 'utf-8');
content = fs.readFileSync(skillMd, 'utf-8');
const source = fs.readFileSync(filePath, 'utf8');
return fs.readFileSync(filePath, 'utf8');

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.rmSync(distDir, { recursive: true, force: true })
fs.writeFileSync(filePath, content, 'utf8');
fs.writeFileSync(filePath, sanitized, 'utf8');
fs.writeFileSync(outputPath, formatRegistry(registry), 'utf8');
fs.writeFileSync(absolutePath, `${JSON.stringify(report, null, 2)}\n`);
fs.appendFileSync(filePath, entry, 'utf8');
fs.writeFileSync(filePath, compactedHeader + compactedTurns, 'utf8');
fs.writeFileSync(out, history, 'utf8');
fs.writeFileSync(out, JSON.stringify({ session: sessionName, turns }, null, 2), 'utf8');
fs.writeFileSync(out, txt, 'utf8');
fs.writeFileSync(target, content, 'utf8');
fs.writeFileSync(sessionPath, '', 'utf8');
fs.writeFileSync(indexPath, indexContent, 'utf8');
fs.writeFileSync(outPath, content, 'utf8');
fs.writeFileSync(configPath, nextRaw, 'utf8');
fs.writeFileSync(configPath, cleaned + (toAppend.length > 0 ? appendText : ''), 'utf8');
fs.appendFileSync(configPath, appendText, 'utf8');
fs.writeFileSync(writePath, output, 'utf8');
fs.writeFileSync(filePath, adapted.text);
fs.writeFileSync(tty, `\x1b]9;${message}\x07`);
fs.writeFileSync(tmp, JSON.stringify(state), 'utf8');
try { fs.unlinkSync(tmp); } catch { /* ignore */ }

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 = file_path.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.

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

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.

MEDIUMMCP server requests over-broad permissions/scopeST-MCP-OVERBROAD-SCOPE

An MCP manifest declares a wildcard or over-broad permission/scope (e.g. '*', 'full_access', 'mail.full_access', 'read_write_all'). Over-broad scopes violate least privilege and widen the blast radius if the server is compromised.

"scope": [".github/workflows/**"],
"scope": [".github/workflows/**"],
"scope": [".github/workflows/**"],
"scope": [".github/workflows/**"],
"scope": [".github/workflows/**"],
"scope": [".github/workflows/**"],

Fix: Request the narrowest scopes the server needs (e.g. read-only instead of full access); avoid wildcard permissions.

MEDIUMNode.js network egressST-NET-NODE

The component makes outbound network requests.

const res = await fetch('https://api.github.com/repos/affaan-m/ECC/releases/latest', {
const gw = await fetch(`${API}/gateway/bot`, { headers: { Authorization: `Bot ${TOKEN}` } }).then(r => r.json());
const res = await fetch(`https://discord.com/api/v10/applications/${APP_ID}/guilds/${GUILD}/commands`, {
const res = await fetch(`https://discord.com/api/v10${path}`, {
const res = await fetch('https://api.github.com/graphql', {
fetch('/api/proximity').then(function (r) { return r.json(); }).then(function (data) {
const response = await fetch('/api/actions/' + encodeURIComponent(actionId), {
const response = await fetch(url);
const response = await fetch('/api/work-items/' + pathSuffix, {

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

Or get notified if this component's risk changes:

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