SkillTotal

Is Obsidian Second Brain safe?

Some risk - review before installing
Notable — review in context (capabilities are not malware):
  • Python shell/command execution
  • Remote pipe-to-shell execution
  • Dangerous MCP tool capability

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 40/100 (medium).

obsidian-second-brain-research 0.17.0

python_package · https://github.com/eugeniughelbur/obsidian-second-brain
MEDIUM
40
/ 100 risk score
Snapshot · scanned Sep 29, 2026 · obsidian-second-brain-research@0.17.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 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)

HIGHCode launches an AI coding CLI with its permission checks disabledST-AGENT-CLI-BYPASS

Code runs a local AI coding agent (claude, gemini, codex, q …) with a flag that turns off its approval prompts. The agent then acts with the user's credentials and file access and nobody confirms what it does; the s1ngularity and Shai-Hulud npm campaigns used exactly this to search disks for secrets.

claude --dangerously-skip-permissions --strict-mcp-config \

Fix: Confirm why the component drives an AI agent unattended and what prompt it sends. Prefer explicit, narrow tool allowlists over disabling approvals.

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.

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. (4 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.

p = subprocess.run(argv, cwd=str(vault), capture_output=True, text=True, timeout=timeout)
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(
                parts + [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"],
        # git writes commit messages as UTF-8; decoding them with the locale
        # codepa …
return subprocess.run(cmd, capture_output=True, timeout=timeout, check=True)
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.

text = claude_md.read_text(encoding="utf-8-sig")
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]
index = json.loads(index_path.read_text(encoding="utf-8"))
with (vault / "log.md").open("a", encoding="utf-8") as fh:
text = idx.read_text(encoding="utf-8-sig")
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")
body = (vault / path).read_text(encoding="utf-8", errors="ignore")
rows = [json.loads(x) for x in cases_path.read_text(encoding="utf-8").splitlines() if x.strip()]
cases_path.read_text(encoding="utf-8").splitlines() if x.strip()]
body = md.read_text(encoding="utf-8", errors="ignore")

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.

f.write_text(f"---\ntype: log\ndate: {day}\nai-first: true\n---\n\n# {day}\n\n", encoding="utf-8")
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 agent\n\n"
        f"Daily note for {day} ({dow}). Journal entries captured via the Telegram jo …
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 agent\n\n{body}\n",
        encoding="utf-8")
CATCHUP.write_text(
            "---\ntype: catchup-queue\nai-first: true\n---\n\n"
            "## For future agent\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, ensure_ascii=False), 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)
with requests.get(audio_url, stream=True, timeout=60) as resp:
resp = requests.post(
            GROQ_TRANSCRIPTION_URL,
            headers={"Authorization": f"Bearer {api_key}"},
            files={"file": (audio_path.name, f, "audio/mpeg")},
            data={"model": GROQ_MODEL, "response_format": …
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)

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 →