Is Storybloq safe?
- 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
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.
Behavioral traits
How this component maps to the CSA agentic threat model. Descriptive — it never affects the risk score.
Findings (10)
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.
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";const path = execSync(cmd, {import { spawn, execFileSync, type ChildProcess } from "node:child_process";child = spawn(
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.
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.
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.
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());
fs.unlinkSync(tmp);
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, "");
fs.unlinkSync(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.
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())
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())
raw = prep.read_bytes()
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())
text = toml.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]})"data = path.read_bytes()
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.
The component writes or deletes files on disk.
out.write_bytes(body.encode())
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")
Path(a.out).write_text(md)
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.
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.
The component makes outbound network requests.
import urllib.request
urllib.request.urlretrieve(base + name, d / name)
sums = urllib.request.urlopen(base + "SHASUMS256.txt").read().decode()
import urllib.request
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.
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 componentHow we determine this: deterministic static analysis (regex + AST), evidence-anchored, no code execution. Methodology →