SkillTotal

Is Storybloq safe?

No malicious indicators - review capabilities before installing
Notable — review in context (capabilities are not malware):
  • Python shell/command execution
  • Node.js shell/command execution
  • Dangerous MCP tool capability

@storybloq/storybloq is an AI npm_package analyzed by SkillTotal's deterministic static scanner. The scan found no malicious indicators, though 10 risky constructs are reported for review. It can: 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 0/100 (low).

@storybloq/storybloq 1.16.0

npm_package · https://github.com/Storybloq/storybloq
LOW
0
/ 100 risk score
Snapshot · scanned Sep 29, 2026 · @storybloq/storybloq@1.16.0 · 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 Storybloq's authors. Report a false positive.

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

Findings (10)

HIGHDangerous MCP tool capabilityST-MCP-DANGEROUS-TOOL

An MCP tool exposes a powerful capability (files, shell, network, browser, or credentials).

server.registerTool("storybloq_register_subprocess", {
server.registerTool("storybloq_unregister_subprocess", {

Why it matters: Wired into an agent, these grant it real access to your machine — confirm each is required.

Fix: Confirm each powerful tool is required and constrained; broad MCP tools (shell/filesystem/network) grant an agent significant host access.

HIGHNode.js shell/command executionST-SHELL-NODE

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

const { spawnSync } = require("node:child_process");
const r = spawnSync("storybloq", ["ticket", "create", "--type", "task", "--title", title, "--description", text, "--format", "json"], {
import { spawn as nodeSpawn, type ChildProcess } from "node:child_process";
import { spawn as nodeSpawn, execFileSync, type ChildProcess } from "node:child_process";
import { execFileSync } from "node:child_process";
import { execFile } from "node:child_process";
import { spawnSync } from "node:child_process";
const head = spawnSync("git", ["rev-parse", "HEAD"], { cwd: PKG_ROOT, encoding: "utf-8" });
const dirty = spawnSync("git", ["status", "--porcelain", "--", "src", "scripts"], { cwd: PKG_ROOT, encoding: "utf-8" });
const r = spawnSync(bin, args, { cwd: ctx.project, env: ctx.env, encoding: "utf-8", timeout: o.timeoutMs, maxBuffer: 512 * 1024 * 1024 });
const effective = spawnSync("/bin/sh", ["-c", 'printf "%s\\n%s\\n%s\\n" "$HOME" "${CODEX_HOME-}" "${CLAUDE_CONFIG_DIR-}"'], { env, encoding: "utf-8" }).stdout.split("\n");
const probe = spawnSync("/bin/sh", ["-c", "command -v codex"], { env, encoding: "utf-8" });
const resolvedCli = spawnSync("/bin/sh", ["-c", "command -v storybloq"], { env, encoding: "utf-8" }).stdout.trim();
import { execFile as execFileCb } from "node:child_process";
import { execFileSync } from "node:child_process";
import { execFileSync, spawn } from "node:child_process";
const child = spawn("claude", args, {
import { execFile } from "node:child_process";
import { execFileSync, execSync } from "node:child_process";
import { spawn, execFileSync, type ChildProcess } from "node:child_process";
import { execFileSync } from "node:child_process";
import { execFile, spawn } from "node:child_process";
const child = spawn("git", ["cat-file", "blob", oid], { cwd, stdio: ["ignore", "pipe", "pipe"] });

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.

r = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
if subprocess.run(["git", "cat-file", "-e", f"{commit}:{name}/task.toml"], cwd=tasks_repo, capture_output=True).returncode != 0:
ar = subprocess.run(["git", "archive", "--format=tar", commit, name], cwd=tasks_repo, capture_output=True)
blob = subprocess.run(["git", "show", f"{commit}:{t['name']}/task.toml"], cwd=tasks_repo, capture_output=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.

MEDIUMNode.js filesystem readST-FS-NODE-READ

The component reads files from disk.

const text = fs.readFileSync(path, "utf8");
return await fs.readFile(abs(p), "utf8");
const raw = fs.readFileSync(`/proc/${pid}/stat`, "utf-8");
fs.readFileSync(join(telemetryDirPath(sessionDir), "lastMcpCall"), "utf-8").trim() || null
const val = fs.readFileSync(join(tDir, "alive"), "utf-8").trim();
raw = fs.readFileSync(join(sessionDir, "state.json"), "utf-8");
content = fs.readFileSync(alivePath, "utf-8");
raw = fs.readFileSync(p, "utf-8").trim();
const buf = fs.readFileSync(p);
existing = fs.readFileSync(gitignorePath, "utf-8");
if (fs.readFileSync(entryPath(root, pid), "utf-8") !== payload) return false;
const onDisk = parseEntry(pid, fs.readFileSync(entryPath(root, pid), "utf-8"));
entry = parseEntry(pid, fs.readFileSync(join(dir, name), "utf-8"));
const raw = await import("node:fs/promises").then((fs) => fs.readFile(`/proc/${pid}/stat`, "utf-8"));
command = fs.readFileSync(`/proc/${entry}/cmdline`).toString("utf-8").replace(/\0/g, " ");
raw = fs.readFileSync(join(sessionDir, "state.json"), "utf-8");
const raw = fs.readFileSync(`/proc/${pid}/stat`, "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.appendFileSync(path.join(dest, "copier-errors.log"), `${now()} ${phase} failed: ${exc && exc.name}: ${exc && exc.message}\n`);
fs.rmSync(target, { recursive: true, force: true });
"  if(process.ppid!==ppid){try{fs.writeFileSync(alive,\"0\")}catch{}process.exit(0)}",
"  if(fs.existsSync(shut)){try{fs.writeFileSync(alive,\"0\")}catch{}process.exit(0)}",
"  try{fs.writeFileSync(alive,String(Date.now()))}catch{}",
try { fs.unlinkSync(lockPath); return { unlinked: true }; }
fs.writeFileSync(tmpPath, JSON.stringify(body), { mode: 0o600 });
try { fs.unlinkSync(tmpPath); }
try { fs.unlinkSync(handle.tmpPath); }
fs.writeFileSync(tmp, String(pid), { mode: 0o600 });
try { fs.unlinkSync(tmp); } catch { /* ignore */ }
try { fs.unlinkSync(join(tDir, PID_BASENAME)); }
fs.unlinkSync(join(tDir, "shutdown"));
fs.writeFileSync(join(dir, "shutdown"), "1");
fs.writeFileSync(join(dir, "alive"), "0");
fs.writeFileSync(tmp, new Date().toISOString());
try { fs.unlinkSync(join(dir, name)); } catch { /* already gone, or not a file */ }
fs.writeFileSync(gitignorePath, content, "utf-8");
fs.writeFileSync(entryPath(root, pid), payload);
fs.unlinkSync(entryPath(root, pid));
fs.writeFileSync(probe, "");
try { fs.unlinkSync(join(dir, name)); } catch { /* another process reaped it */ }
fs.rmSync(scratch, { 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.

INSTRUCTION_SUFFIX = (BENCH_ROOT / "agents" / "instruction.txt").read_text(encoding="utf-8").strip()
return hashlib.sha256(p.read_bytes()).hexdigest()
data = json.loads(p.read_text())
AUTO_INSTRUCTION_SUFFIX = (BENCH_ROOT / "agents" / "instruction-auto.txt").read_text(encoding="utf-8").strip()
if not (node.get("version") and node.get("sha256") and Path(node.get("path", "")).exists() and hashlib.sha256(Path(node["path"]).read_bytes()).hexdigest() == node["sha256"]):
if not f.exists() or hashlib.sha256(f.read_bytes()).hexdigest() != pb["sha256"]:
adapter = {rel: hashlib.sha256((bench_root / rel).read_bytes()).hexdigest() for rel in ADAPTER_FILES}
prices = json.loads(prices_path.read_text())
return hashlib.sha256(p.read_bytes()).hexdigest()
pkg = json.loads((repo / "package.json").read_text())
lock = json.loads((proj / "package-lock.json").read_text())
got = base64.b64encode(hashlib.sha512(tgz.read_bytes()).digest()).decode()
lock = json.loads((proj / "package-lock.json").read_text())
seed = json.loads(seed_file.read_text())
manifest = json.loads(manifest_path.read_text())
manifest_sha = hashlib.sha256(manifest_path.read_bytes()).hexdigest()
prices_sha = hashlib.sha256(prices_path.read_bytes()).hexdigest()
instructions[task] = f.read_text(encoding="utf-8")
header["prices"] = f"{prices_path.name} dated {json.loads(prices_path.read_text()).get('date', 'UNDATED')} (sha {prices_sha[:12]})"
tree[".story/" + str(p.relative_to(root))] = p.read_bytes()
v = json.loads(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.

shutil.move(out / tgz_name, target)
(proj / "package.json").write_text(json.dumps({"name": "storybloq-bench-claude", "private": True, "dependencies": {"@anthropic-ai/claude-code": claude_version}}, indent=2) + "\n")
(proj / "package.json").write_text(json.dumps({"name": f"storybloq-bench-install-{arm.lower()}", "private": True, "dependencies": deps}, indent=2) + "\n")
shutil.copy(artifacts[n]["path"], proj / f"{n}.tgz")
toml.write_text(text.replace(f'"{image}"', f'"{digest}"'))
(out / "prepare-manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
out.write_text(json.dumps(draw(root, n, seed), indent=2) + "\n")

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.

import http from "node:http";
const req = http.request({
* Privacy opt-out for the registry fetch (ISS-777): NO_UPDATE_NOTIFIER or CI
const res = await fetch(NPM_REGISTRY_URL, { signal: controller.signal });
response = await fetch(url, {

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.

urllib.request.urlretrieve(base + name, d / name)
sums = urllib.request.urlopen(base + "SHASUMS256.txt").read().decode()
urllib.request.urlretrieve(url, d / fname)

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.

const server = new McpServer({ name: "storybloq", version: "0.0.0" });
const server = new McpServer({ name: "storybloq", version: "0.0.0" });
const server = new McpServer({ name: "storybloq", version: "0.0.0" });
const server = new McpServer({ name: "storybloq", version: "0.0.0" });
server.registerTool("storybloq_bus_send", {
server.registerTool("storybloq_bus_redeliver", {
server.registerTool("storybloq_bus_poll", {
server.registerTool("storybloq_bus_ack", {
server.registerTool("storybloq_bus_thread_get", {
server.registerTool("storybloq_bus_thread_update", {
const degradedStatus = server.registerTool("storybloq_status", {
const degradedInit = server.registerTool("storybloq_init", {
const server = new McpServer(
server.registerTool("storybloq_status", {
server.registerTool("storybloq_phase_list", {
server.registerTool("storybloq_phase_current", {
server.registerTool("storybloq_ticket_next", {
server.registerTool("storybloq_ticket_blocked", {
server.registerTool("storybloq_handover_list", {
server.registerTool("storybloq_handover_latest", {
server.registerTool("storybloq_blocker_list", {
server.registerTool("storybloq_validate", {
server.registerTool("storybloq_phase_tickets", {
server.registerTool("storybloq_ticket_list", {
server.registerTool("storybloq_ticket_get", {

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 →