SkillTotal

Is Fission-AI/OpenSpec safe?

No malicious indicators - review capabilities before installing
Notable — review in context (capabilities are not malware):
  • Node.js shell/command execution
  • Node.js filesystem read
  • Node.js filesystem write/delete

What to do: Nothing here argues against installing it. Grant the capabilities it lists only if you expect the tool to need them.

Fission-AI/OpenSpec is an npm package analyzed by SkillTotal's deterministic static scanner. The scan found no malicious indicators. It can: 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 0/100 (low).

@fission-ai/openspec 1.14.1

npm_package · https://github.com/Fission-AI/OpenSpec
LOW
0
/ 100 risk score
Snapshot · scanned Oct 8, 2026 · @fission-ai/openspec@1.14.1 · 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 Fission-AI/OpenSpec's authors. Report a false positive.

Capabilities — what this component can do (not a risk score):
filesystem 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

Findings (5)

HIGHNode.js shell/command executionST-SHELL-NODE

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

import { execFileSync } from 'child_process';
import spawn from 'cross-spawn';
import type { ChildProcess, spawn as nodeSpawn } from 'node:child_process';
import { execFileSync } from 'child_process';
import type { spawn as nodeSpawn } from 'node:child_process';
cachedSpawn = require('cross-spawn') as typeof nodeSpawn;
import { execFile } from 'node:child_process';
import { execFile } from 'node:child_process';
cachedSpawn = require('cross-spawn') as typeof import('child_process').spawn;
} from 'node:child_process';
import { execFileSync } from 'node:child_process';

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.

MEDIUMNode.js filesystem readST-FS-NODE-READ

The component reads files from disk.

const contentForTitle = await fs.readFile(proposalPath, 'utf-8');
const content = await fs.readFile(proposalPath, 'utf-8');
const deltaContent = await fs.readFile(deltaSpecPath, 'utf-8');
mainContent = await fs.readFile(mainSpecPath, 'utf-8');
const content = await fs.readFile(proposalPath, 'utf-8');
const content = await fs.readFile(proposalPath, 'utf-8');
const parsed: unknown = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
const rawConfig = fs.readFileSync(configPath, 'utf-8');
content = fs.readFileSync(schemaPath, 'utf-8');
const contents = fs.readFileSync(abs);
const originalContent = fs.readFileSync(configPath);
fs.readFileSync(prepared.path).equals(prepared.originalContent)
fs.readFileSync(path.join(trustedSourceDir, 'schema.yaml'), 'utf-8')
const schemaContent = fs.readFileSync(stagedSchemaPath, 'utf-8');
parseSchema(fs.readFileSync(stagedSchemaPath, 'utf-8'));
const contents = await fs.readFile(claimPath, 'utf8');
createHash('sha256').update(await fs.readFile(filePath)).digest('hex');
.update(await fs.readFile(filePath))
.update(await fs.readFile(filePath))
content = await fs.readFile(update.target);
...(stat.isFile() ? { content: await fs.readFile(update.target) } : {}),
(await fs.readFile(snapshot.target)).equals(snapshot.content);
const currentContent = await fs.readFile(snapshot.target);
const content = await fs.readFile(specFile, 'utf-8');
return fs.readFileSync(fullPath, '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.

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

The component writes or deletes files on disk.

fs.writeFileSync(resolved, buildCodeWorkspaceJson(workingSet, rootName));
fs.writeFileSync(stagedSchemaPath, doc.toString());
fs.rmSync(backupDir, { recursive: true, force: true });
fs.rmSync(stagingDir, { recursive: true, force: true });
fs.writeFileSync(templatePath, createDefaultTemplate(artifact.id));
fs.writeFileSync(stagedConfigPath, preparedConfig.content);
fs.rmSync(preparedConfig.path, { force: true });
fs.rmSync(schemaDir, { recursive: true, force: true });
fs.rmSync(backup, { recursive: true, force: true });
fs.rmSync(schemaStagingDir, { recursive: true, force: true });
fs.rmSync(configStagingDir, { recursive: true, force: true });
await fs.writeFile(readmePath, `# ${name}\n\n${options.description}\n`, 'utf-8');
await fs.rm(claimed, { force: true });
await fs.rm(dest, { recursive: true, force: true }).catch(() => undefined);
await fs.rm(dest, { recursive: true, force: true }).catch(() => undefined);
await fs.unlink(claimPath);
await fs.rm(snapshot.target, { force: true });
await fs.writeFile(snapshot.target, snapshot.content!);
await fs.rm(referent, { force: true });
await fs.writeFile(snapshot.target, snapshot.content);
await fs.writeFile(bashrcPath, lines.join('\n'), 'utf-8');
await fs.writeFile(targetPath, completionScript, 'utf-8');
await fs.writeFile(targetPath, completionScript, '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.

MEDIUMnpm prepare hookST-INSTALL-NPM-PREPARE

package.json has a 'prepare' script (runs on git/local installs and before publishing).

"prepare": "node build.js",

Why it matters: Usually a build step, but confirm it doesn't fetch or run remote code.

Fix: Usually a legitimate build step; confirm it only builds and does not fetch or execute remote code.

MEDIUMNode.js network egressST-NET-NODE

The component makes outbound network requests.

const response = await fetch(url, options);
const response = await fetch(upstream.toString(), init);

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 Fission-AI/OpenSpec.

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 →