SkillTotal

Is Obsidian Second Brain 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
  • Remote pipe-to-shell execution

obsidian-second-brain-research is an AI python_package analyzed by SkillTotal's deterministic static scanner. The scan found no malicious indicators, though 9 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 20/100 (low).

obsidian-second-brain-research 0.14.0

python_package · https://github.com/eugeniughelbur/obsidian-second-brain
LOW
20
/ 100 risk score
Snapshot · scanned Aug 9, 2026 · obsidian-second-brain-research@0.14.0 · engine 0.38.1 / ruleset 42

Automated static-analysis result. It can contain false positives and false negatives, and is not a claim about the intent of Obsidian Second Brain'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
Supply-chain provenance risk
General Protections / Supply Chain

Findings (9)

HIGHDangerous MCP tool capabilityST-MCP-DANGEROUS-TOOL

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

@mcp.tool()
def obsidian_vault_health() -> str:

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.

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.

HIGHNode.js shell/command executionST-SHELL-NODE

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

const body = fm === null ? note.text : note.text.slice(FRONTMATTER.exec(note.text)![0].length);

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.

HIGHRemote pipe-to-shell executionST-SHELL-PIPE-EXEC

A remotely fetched payload is piped straight into a shell (e.g. `curl … | bash`). The component runs code downloaded at runtime, which is unreviewable and a common second-stage delivery vector. (2 occurrence(s) shown as evidence).

Linux)                uv_hint="curl -LsSf https://astral.sh/uv/install.sh | sh" ;;

Fix: Download to a file, inspect it, and run a pinned/verified copy instead of piping a network response directly into a shell.

HIGHPython shell/command executionST-SHELL-PY

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

subprocess.run(cmd, check=True, capture_output=True, text=True)
proc = subprocess.run(
            [UV_BIN, "run", "-m", module, url],
            cwd=str(SKILL_REPO), capture_output=True, text=True, timeout=300, env=os.environ,
        )
out = subprocess.run(["git", "-C", str(root), *args],
                                 capture_output=True, text=True, check=False)
result = subprocess.run(
        ["bash", "scripts/build.sh"],
        cwd=REPO_ROOT, capture_output=True, text=True,
        encoding="utf-8", errors="replace", check=False,
    )
proc = subprocess.run(
                shlex.split(cmd) + [q],
                capture_output=True, text=True, timeout=120,
            )
out = subprocess.run(
        ["git", "-C", repo, "log", f"-n{limit}", "--no-merges",
         "--pretty=format:%h%x1f%ad%x1f%s", "--date=short"],
        capture_output=True, text=True, check=False,
    )
subprocess.run(open_cmd, check=False, timeout=5)
subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                       check=False, timeout=600)
result = subprocess.run(
        ["ffprobe", "-v", "quiet", "-print_format", "json",
         "-show_format", "-show_streams", str(Path(video_path).resolve())],
        capture_output=True, text=True, timeout=120)
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
result = subprocess.run(
            # CLAUDE.md bans substitution Unicode in "source files, docs, or
            # commits" generally, but this only ever scanned md/py/sh, so YAML,
            # TOML, JSON, and dotfiles were ungated. There …

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.

MEDIUMPython filesystem readST-FS-PY-READ

The component reads files from disk.

return header + claude_md.read_text(encoding="utf-8")
with (d / f"recall-{datetime.now():%Y-%m-%d}.jsonl").open("a") as f:
head = (vault / r["path"]).read_text(encoding="utf-8-sig", errors="ignore")[:400]
for line in path.read_text(encoding="utf-8").splitlines():
index = json.loads(index_path.read_text(encoding="utf-8"))
return path.read_text(encoding="utf-8-sig", errors="replace")[:limit]
for line in CONFIG.read_text().splitlines():
return out.read_text().strip() if out.exists() else ""
data = json.loads(pkg.read_text(encoding="utf-8"))
text = pyproject.read_text(encoding="utf-8")
content = template_path.read_text(encoding="utf-8")
m = FM_RE.match(path.read_text(encoding="utf-8-sig"))
if not p.exists() or p.read_text(encoding="utf-8") != content:
"SKILL_ROOT" in p.read_text(encoding="utf-8", errors="ignore")
text = md.read_text(encoding="utf-8", errors="ignore")
text = readme.read_text(encoding="utf-8")
cases_path.read_text(encoding="utf-8").splitlines() if x.strip()]
body = md.read_text(encoding="utf-8", errors="ignore")
body = md.read_text(encoding="utf-8", errors="ignore")
cases = [json.loads(line) for line in cases_path.read_text(encoding="utf-8").splitlines() if line.strip()]
cache = json.loads(index_path.read_text())

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.

note.write_text(
        f"---\ntype: daily\ndate: {day}\nday-of-week: {dow}\ntags:\n  - daily\n"
        f"ai-first: true\n---\n\n## For future Claude\n\n"
        f"Daily note for {day} ({dow}). Journal entries captured via the Telegram j …
note.write_text(text.rstrip() + f"\n\n{header}\n\n{block}", encoding="utf-8")
note.write_text(new_text, encoding="utf-8")
note.write_text(fm + body.rstrip() + f"\n\n![[{fname}]]\n", encoding="utf-8")
note.write_text(
        f"---\ntype: {ntype}\ndate: {today}\ntags: [{ntype}, telegram-capture]\nai-first: true\n---\n\n"
        f"## For future Claude\n\n{body}\n",
        encoding="utf-8")
CATCHUP.write_text(
            "---\ntype: catchup-queue\nai-first: true\n---\n\n"
            "## For future Claude\n\nUnprocessed captures from the Telegram journal bot, "
            "newest at the bottom. Each line is `- [ ] date time …
path.write_text(content.strip() + "\n", encoding="utf-8")
target.write_text(content.replace(placeholder, folder), encoding="utf-8")
(d / ".gitkeep").write_text("", encoding="utf-8")
p.write_text(content, encoding="utf-8")
readme.write_text(updated, encoding="utf-8")
p.write_text(content, encoding="utf-8")
(cdir / f"{name}.jsonl").write_text(
            "\n".join(json.dumps(r, ensure_ascii=False) for r in rows) + "\n", encoding="utf-8")
index_path.write_text(json.dumps(out), encoding="utf-8")
dest.write_text(fmblock + convert_links(body, rel).lstrip("\n"), encoding="utf-8")
(out / "index.md").write_text("\n".join(idx), encoding="utf-8")
(out / "log.md").write_text(vlog.read_text(encoding="utf-8-sig", errors="replace"),
                                    encoding="utf-8")
path.write_text(json.dumps(results, default=encode_results))

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 network egressST-NET-PY

The component makes outbound network requests.

req = urllib.request.Request(url, data=body, headers=headers if _EMBED_BACKEND == "openai"
                                else {"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=10) as r:
r = requests.get(f"{API}/{method}", params=params, timeout=30)
data = requests.get(f"{FILE_API}/{path}", timeout=120).content
r = requests.post(
        "https://api.openai.com/v1/audio/transcriptions",
        headers={"Authorization": f"Bearer {OPENAI}"},
        files=files,
        timeout=120,
    )
r = requests.post(
        "https://api.anthropic.com/v1/messages",
        headers={"x-api-key": ANTHROPIC, "anthropic-version": "2023-06-01",
                 "content-type": "application/json"},
        json=body, timeout=90,
    )
with urllib.request.urlopen(f"{OLLAMA_URL}/api/tags", timeout=3) as r:
req = urllib.request.Request(url, data=body, headers=headers)
with urllib.request.urlopen(req, timeout=120) as r:
r = requests.post(API_URL.format(model=model), json=body, headers=headers, timeout=180)
r = requests.post(API_URL, json=body, headers=headers, timeout=180)
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
retry = Retry(
        total=retries,
        backoff_factor=backoff,
        status_forcelist=(500, 502, 503, 504),
        allowed_methods=("GET", "HEAD"),
        raise_on_status=False,
    )
adapter = HTTPAdapter(max_retries=retry)
r = requests.post(API_URL, json=body, headers=headers, timeout=timeout)

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.

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 →