Is Storybloq safe?
- Node.js shell/command execution
- Dangerous MCP tool capability
- Node.js filesystem read
@storybloq/storybloq is an AI npm_package analyzed by SkillTotal's deterministic static scanner. The scan found no malicious indicators, though 6 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.9.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 (6)
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.
import { execFileSync, spawn } from "node:child_process";const child = spawn("claude", args, {while ((match = ENTITY_ID_REGEX.exec(branch)) !== null) {const m = TEMP_NAME.exec(name);
import { execFile } from "node:child_process";while ((match = pattern.exec(content)) !== null) {import { execFileSync, execSync } from "node:child_process";const path = execSync(cmd, {import { spawn, execFileSync, type ChildProcess } from "node:child_process";child = spawn(
const match = /^Session:\s*(\S+)\s*$/.exec(line);
import { execFileSync } from "node:child_process";import { spawn } from "node:child_process";const child = spawn(cmd, args, { cwd: opts.cwd, env: opts.env, detached: true, stdio: "ignore" });// The record moved between claim and spawn (interactive takeover, re-limit
const child = spawn(process.execPath, [bin, "waker-run", WAKER_ARGV_SENTINEL], {// honoring it and the next hook is allowed to spawn (the fresh child's try-lock reclaims).
import { spawn as nodeSpawn } from "node:child_process";import type { SpawnOptions } from "node:child_process";import { execFile } from "node:child_process";const match = ENTRY_FILENAME.exec(filename);
import { execFile } from "node:child_process";const match = ENTRY_FILENAME.exec(filename);
const match = ENDPOINT_FILENAME.exec(entry.name);
const match = ENTRY_FILENAME.exec(filename);
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 reads files from disk.
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"));
raw = fs.readFileSync(statePath, "utf-8");
const raw = await import("node:fs/promises").then((fs) => fs.readFile(`/proc/${pid}/stat`, "utf-8"));const raw = fs.readFileSync(`/proc/${pid}/stat`, "utf-8");raw = fs.readFileSync(join(sessionDir, "state.json"), "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.
" 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.unlinkSync(tmp);
fs.unlinkSync(wakeClaimPath(sessionDir));
fs.writeFileSync(tmp, body, { mode: 0o600 });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.
* 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.
An MCP tool surface (manifest or tool definitions) was found.
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", {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", {server.registerTool("storybloq_ticket_meta_get", {server.registerTool("storybloq_issue_list", {server.registerTool("storybloq_issue_get", {server.registerTool("storybloq_issue_meta_get", {server.registerTool("storybloq_handover_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 →