Is safari-mcp safe?
- Node.js shell/command execution
- npm install-time lifecycle hook
- Dangerous MCP tool capability
safari-mcp 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: delegated authentication, install time execution, 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).
safari-mcp 2.22.4
Automated static-analysis result. It can contain false positives and false negatives, and is not a claim about the intent of safari-mcp'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)
package.json runs scripts automatically when the package is installed.
"postinstall": "node scripts/postinstall.cjs || true"
Why it matters: Install scripts are a favorite supply-chain foothold — they execute on every machine that installs the package.
Fix: Inspect the hook command. Install-time scripts are a common supply chain execution vector; ensure they do nothing beyond a documented build step.
An MCP tool exposes a powerful capability (files, shell, network, browser, or credentials).
server.tool( "safari_navigate",
server.tool( "safari_navigate_and_read",
server.tool( "safari_screenshot",
server.tool( "safari_screenshot_element",
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 { execFile, execFileSync } from "node:child_process";import { execFile, spawn, spawnSync } from "node:child_process";_helperProc = spawn(helperPath, [], { stdio: ["pipe", "pipe", "ignore"] });const proc = spawn("pbcopy", [], { stdio: ["pipe", "ignore", "ignore"] });// window. Uses spawnSync (blocking) because we're on the exit path and can't await a Promise.
spawnSync("pbcopy", [], { input: content });const { execFileSync, spawnSync } = require("child_process");const result = spawnSync("codesign", ["-d", "--verbose=2", "--", helper], { encoding: "utf8" });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 makes outbound network requests.
// Safari terminates idle service workers after ~30s, so we keep an active fetch() going
_bridgeAuthTokenPromise = fetch(browser.runtime.getURL("bridge-auth-token"), {return fetch(url, { ...init, headers });// The response is here, so a reconnect must no longer abort this fetch (that would
// 1. Active fetch() in pollForCommands() keeps the worker alive while connected
const response = await fetch(`${_mcpContentWakeBridgeUrl}/content-wakeup`, {import { createServer } from "node:http";const res = await fetch(`http://127.0.0.1:${HTTP_PORT}/proxy-check?profile=${profile}`, {const res = await fetch(`http://127.0.0.1:${HTTP_PORT}/proxy-command`, {// Async iff the *result* is a promise to wait on. `fetch(` alone is NOT async —
// Patch fetch (once)
import { createServer } from "node:http";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 OAuth 2.0 / OpenID Connect delegated-authentication flow was detected (authorization-code / refresh-token / token-exchange grant, an OIDC authorize/discovery endpoint or id_token, or a delegation library). Tools authenticate with the end user's delegated, scoped credentials rather than a long-lived embedded service credential.
!/[?&#](code|token|access_token|id_token|state|session_state)=/i.test(payload.url || "");
Fix: Delegated auth is a lower-blast-radius execution context than an embedded static credential. Confirm the requested scopes are minimal and that tokens are never logged or forwarded off-host.
An MCP tool surface (manifest or tool definitions) was found.
const server = new McpServer({server.tool( "safari_navigate",
server.tool( "safari_go_back",
server.tool( "safari_go_forward",
server.tool( "safari_reload",
server.tool( "safari_read_page",
server.tool( "safari_get_source",
server.tool( "safari_snapshot",
server.tool( "safari_navigate_and_read",
server.tool( "safari_click",
server.tool( "safari_click_and_read",
server.tool( "safari_double_click",
server.tool( "safari_right_click",
server.tool( "safari_native_click",
server.tool( "safari_native_hover",
server.tool( "safari_native_keyboard",
server.tool( "safari_native_type",
server.tool( "safari_fill",
server.tool( "safari_clear_field",
server.tool( "safari_verify_state",
server.tool( "safari_select_option",
server.tool( "safari_react_select_set",
server.tool( "safari_react_select_list_options",
server.tool( "safari_fill_form",
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 →