If you find a security vulnerability, please report it privately.
Email: hello@studiomeyer.io
Please do not open a public GitHub issue for security vulnerabilities.
We will acknowledge your report within 48 hours and provide a fix timeline within 7 days.
local-memory-mcp is a single-user, local-only MCP server. It runs as a stdio process invoked by an MCP client on the same machine and stores all data in a single SQLite file under the user's local data directory.
- No network code in our source. No HTTP server, no fetch, no API calls in
src/. The process talks to its parent over stdio only. - No shell execution. No
child_process, nospawn, noexecSync, noexecFile. The codebase has zero subprocess invocations. See "Known SAST scanner false positives" below. - No telemetry. No analytics, no phone-home, no crash reporting.
- Local filesystem only. The SQLite file lives at
~/Library/Application Support/local-memory-mcp/memory.sqlite(macOS),~/.local/share/local-memory-mcp/memory.sqlite(Linux), or%APPDATA%\local-memory-mcp\memory.sqlite(Windows). Override withMEMORY_DB_PATH.
Privilege boundary: the server runs as the same user who started the MCP client. There is no privilege escalation surface.
Hybrid retrieval depends on a local embedding model. The first time embed() runs, @huggingface/transformers will fetch the model files (default Xenova/multilingual-e5-small, ~30 MB q8 quantized) over HTTPS from huggingface.co and cache them under the Transformers.js cache directory (typically ~/.cache/huggingface/). Subsequent calls are fully offline. The download is:
- Initiated by user activity — only when the user (via their MCP client) calls a tool that triggers
embed(). The server never preemptively reaches out. - Opt-out — set
MEMORY_EMBED_DISABLED=1to force FTS5-only mode. No download happens, search still works (keyword only). - Replaceable — point
MEMORY_EMBED_MODEL=…at a self-hosted or alternative feature-extraction model. Combine withMEMORY_EMBED_CACHE_DIR=…for an air-gapped pre-seeded cache.
No analytics or identifiers are sent. The HTTPS request is to the Hugging Face Hub for the model artifacts only — this is the same fetch any Transformers.js consumer makes and is the only outbound network event the server can produce.
sqlite-vec is a native SQLite extension written in pure C (asg017/sqlite-vec, MIT/Apache-2.0). It is loaded via db.loadExtension at boot. The prebuilt binary that ships in the matching npm package (e.g. sqlite-vec-linux-x64) is what gets loaded — it must be present, matching the host platform, and matching the better-sqlite3 ABI. If any of that fails, loadVecExtension catches the error, sets vectorEnabled = false, and the server runs in FTS5-only mode for the lifetime of the process. No fatal crash, no silent half-state.
There is no path by which a user-supplied SQL query selects a different shared object or invokes loadExtension at runtime — db.loadExtension is called exactly once per Database connection, with a path determined by the sqlite-vec npm package itself.
memory_export reads the local database and returns a JSON envelope to the calling MCP client; it writes nothing to disk and makes no network call. memory_import reads a JSON envelope passed in by the client and writes rows to the same local SQLite file via parameterized INSERT OR IGNORE statements (no SQL is built from envelope strings — all values are bound parameters). Re-embedding on import uses the same local pipeline as any write and obeys MEMORY_EMBED_DISABLED. memory_learn_bulk and memory_observation_supersede are likewise pure local SQLite operations. None of the v2.2 tools introduce a network, shell, or filesystem-write surface beyond the single SQLite file and the one opt-out model fetch documented above.
Some automated security scanners flag patterns in this codebase that look risky in isolation but are safe in context. This section documents them so reviewers can verify quickly.
Pattern scanners that regex-match .exec( without resolving the receiver's type sometimes flag the schema-bootstrap line as shell command execution. It is not.
The receiver db is a Database instance from better-sqlite3 (imported at the top of the file). Database.prototype.exec(sql) is the official SQLite API for executing one or more SQL statements from a string. It does not spawn a subprocess, does not invoke a shell, and has no relation to child_process.exec.
Independent verification you can run yourself:
git clone https://github.com/studiomeyer-io/local-memory-mcp
cd local-memory-mcp
grep -rn "child_process" src/ # zero results
grep -rn -E "spawn|execSync|execFile" src/ # zero results
grep -rn -E "shell\s*:\s*true" src/ # zero resultsThe only .exec( call in the entire codebase is db.exec(schema) at src/db/client.ts:57, applied to a better-sqlite3 Database, executing a static schema.sql file bundled with the package (no user input enters that string).
process.env.MEMORY_DB_PATH, process.env.APPDATA, process.env.XDG_DATA_HOME, and process.env.MEMORY_LOG_LEVEL are read to compute platform-standard filesystem paths and configuration. None of these values is ever interpolated into a shell command, passed to a subprocess, or used in any code path that could escape to the OS.
src/tools/entity.test.ts sets MEMORY_DB_PATH to a mkdtempSync temp directory before each test and deletes it after. This is standard Vitest test isolation, not runtime behavior.
Runtime dependencies (audit-relevant):
@modelcontextprotocol/sdk— official MCP SDKbetter-sqlite3— synchronous SQLite driver (native binding, no shell)sqlite-vec— native SQLite vector-search extension (MIT/Apache-2.0, prebuilt platform binary)@huggingface/transformers— Transformers.js, runs the local embedding model on CPUzod— schema validation
Dev dependencies are not shipped to users (tsx, vitest, typescript, @types/*).
npm audit is run on every release. The two new v2 runtime deps were audited at v2.0.0 cut:
sqlite-vec@0.1.9— pure C, no transitive runtime deps, scoped to the SQLite extension surface.@huggingface/transformers@4.2.0— pure JS + WASM backend, no native bindings. Brings inonnxruntime-webas a runtime transitive but it's used purely as a WASM execution provider — no network outside the explicit model fetch documented above.