Please report security problems privately through GitHub's private vulnerability reporting: open the repository's Security tab and choose Report a vulnerability. Do not open a public issue for a security problem.
RootMind is a personal project without a support team. Reports are read and
handled on a best-effort basis, and fixes land on the main branch. There are
no maintained release branches.
RootMind is designed for one person on their own computer: the backend,
the desktop app and the indexed folders all belong to the same user. The REST
API and the MCP HTTP server listen on 127.0.0.1 by default.
The desktop app uses the RootMind service (a Windows service, or on Linux two systemd user services of the account), which requires a token on every request, so other accounts on the same computer and web pages cannot use it. RootMind does not protect against malware that runs under your own account, against local administrators, or against exposing its ports to a network. These are out of scope (see the end of this document). The one deliberate exception is remote access (off by default): it makes MCP over HTTP reachable from the internet through ngrok, protected by the MCP token (see Remote access).
Server use is a second, opt-in mode: a server indexes shared folders and serves MCP over HTTP to other machines, each client with a named token scoped to some folders. Its different trust model is described in Server use; the guide is docs/SERVER.md.
| Action | What is sent | Where |
|---|---|---|
| Indexing, search, listing, reading | Nothing | Local only; no LLM is involved |
Asking a question (POST /query) |
The question; for follow-ups, the last few chat turns (for the condense step); every tool result the model requests, which includes search snippets and document text of up to 50,000 characters per read | The LLM provider you configured (Anthropic, OpenAI or xAI), or the Ollama server at your base URL (nothing leaves the computer when it runs on the same one; a LAN address receives it over plain http://) |
Chat titles (POST /chat/title) |
Up to 500 characters each of the first question and its answer | The configured provider |
Model list (POST /llm/models) |
The API key, to list models (none for Ollama) | The configured provider, or the Ollama server |
| MCP tool calls | Tool results go to the MCP client, and from there to whatever model that client uses | Decided by the MCP client |
Langfuse tracing (tracing.langfuse, or run_evals.py --langfuse; off by default) |
Per question: the question, every model call (model, token counts, stop reason, the model's text and tool requests), every tool call (arguments, the documents found, the result text up to 20,000 characters, which includes document text) and the answer with its sources | The Langfuse server in LANGFUSE_BASE_URL: a self-hosted Langfuse stays on your network; the default, Langfuse Cloud, is a third party |
| Remote access, while it is on | The ngrok authtoken (to sign in), and every request and answer through the public address, including the MCP token in the Authorization header: ngrok terminates TLS at its edge |
ngrok; a cloud assistant's requests and the document text it reads also go to that assistant's provider |
RootMind has no telemetry and no update check. Tracing is off by default;
with it off, the agent loop makes no tracing calls and loads no tracing code
(a test asserts that nothing is imported, written or sent). The local trace
file (tracing.local, run_evals.py --trace) never leaves the computer.
Langfuse export loads the optional langfuse package only when it is turned
on; its keys come only from the LANGFUSE_* environment variables, never from
config.yaml. No trace contains an API key: provider errors are recorded only
by type and RootMind's own message, and key- and token-like strings
(sk-…, xai-…, pk-…, Bearer …) are replaced with [redacted] in every
field. The desktop app's CSP only
allows network requests to localhost and 127.0.0.1. With remote access
off (the default), the RootMind service opens no connection to ngrok and does
not even load the SDK.
The backend keeps its files in its home folder (RLM_HOME):
- the RootMind service, which the desktop app uses:
%ProgramData%\RootMind; on Linux$XDG_DATA_HOME/rootmind(default~/.local/share/rootmind), with the same layout, owner-only (see Background service on Linux); - command line:
RLM_HOME, or the working directory when it is unset.
| Data | Location | Notes |
|---|---|---|
| API keys saved through the desktop app | config.yaml and its backup config.yaml.bak in the home folder |
Plain text. Written atomically and owner-only (0600) on Linux and macOS; on Windows the folder's permissions apply. In the service's folder the installing account, SYSTEM, administrators and, while MCP over HTTP is on, the MCP service account can read them (see Background service). From the command line, prefer environment variables (ANTHROPIC_API_KEY, OPENAI_API_KEY, XAI_API_KEY); the service sees only machine-wide variables, which every process on the computer can read |
| Index | Service: data\db\index.db; command line: data\index.db; plus -wal/-shm |
Contains the full extracted text of every indexed file. In the service's folder the installing account can read it |
| REST token | data\rest_token.txt (service, and --require-token), or RLM_API_TOKEN |
See REST API |
| MCP HTTP token | data\api_token.txt, unless MCP_API_TOKEN is set |
In the service's folder readable by the installing account, SYSTEM, administrators and the MCP service account (see MCP over HTTP). Created with mode 0600 on Linux and macOS; elsewhere on Windows it inherits the folder's permissions |
| ngrok authtoken (remote access) | data\ngrok_authtoken.txt |
Only in the service's folder: readable by SYSTEM, Administrators and the MCP service account, not by the installing account's standard token. Never in config.yaml, never returned by the API, never logged (see Remote access). Kept by an uninstall without Delete the application data |
| Remote access status | data\remote_access.json |
State, public address, last error and time; no secrets. Written by the MCP service account in place |
| Logs | Service: logs\backend\, logs\mcp-http\ and logs\install.log. Command line: the console |
Contain file paths, questions and tool arguments. RootMind never logs API keys or tokens, only a short fingerprint of a token; provider error messages are logged as the provider sends them |
Traces (only with tracing.local: true) |
logs\traces\<time>-<process>.jsonl and .md in the home folder; eval runs: the folder given to --trace |
Questions, the model's text and tool arguments, search hits and the document text the model read (up to 20,000 characters per tool result), answers and sources, in plain text. No API keys. Same permissions as the other logs; RootMind does not rotate or delete them, so delete them when you no longer need them |
| Chat history | %APPDATA%\io.github.cgaeking.rootmind\conversations; Linux: ~/.local/share/io.github.cgaeking.rootmind/conversations |
Questions, answers and source paths in plain JSON |
| Custom backend address and token | The desktop app's local storage | Only when you connect to a backend under Settings → Advanced |
| Data of the RootMind 1.x built-in backend | %LOCALAPPDATA%\io.github.cgaeking.rootmind\backend |
Left in place by the upgrade to 2.0 and no longer used: its config.yaml (with API keys saved in 1.x), index and logs. Delete the folder, or tick Delete the application data when uninstalling, which removes it with the chats |
RootMind never indexes its own data: the data folder, the database's folder
and, when RLM_HOME is set, all of RLM_HOME are skipped even inside an
indexed folder, and a folder inside them is refused.
The REST API serves the desktop app on the same machine. Whether it requires a token depends on how it runs:
| Mode | Token | Where the token lives |
|---|---|---|
| RootMind service (the desktop app's backend) | Always | %ProgramData%\RootMind\data\rest_token.txt, readable by SYSTEM, Administrators and the installing account; not by the MCP service account or other accounts. Never in the service's registry entries. The app reads it only after the listener check (see Background service) and never stores it |
| Custom backend (Settings → Advanced) | Optional, entered by you | The app's local storage |
rootmind api from the command line |
Off, unless RLM_API_TOKEN is set or --require-token is given |
The variable, or <RLM_HOME>/data/rest_token.txt (created if missing) |
With a token:
- Every route except
GET/HEADon/and/healthrequiresAuthorization: Bearer <token>. Missing or wrong tokens get 401. The token is read only from that header, never from the query string or a cookie, and compared in constant time (secrets.compare_digest). /docs,/redocand/openapi.jsonare turned off. WebSocket connections are closed unless they carry the token.- An empty token (variable or file) refuses to start.
RLM_API_TOKENis removed from the process environment after it has been read. Logs show only where the token came from and an 8-character SHA-256 fingerprint.
In every mode:
- It binds to
127.0.0.1by default (api.host,--host). Binding to another address logs a warning, and requests from non-loopback addresses are refused with 403 regardless of the bind address. TrustedHostMiddlewarerejects requests whoseHostheader is not127.0.0.1,localhostortauri.localhost. This blocks DNS rebinding attacks from web pages and requests that arrive through a tunnel.- CORS allows only the desktop app's origins (and the Vite dev server when
RLM_DEVis set), without credentials. Web pages cannot read responses. - Endpoints that change state or call the LLM take JSON bodies. A web page
cannot send JSON cross-origin without a CORS preflight, which fails, so
"simple" form or
no-corsrequests to these endpoints get 422. Tests cover this forPOST /config. GET /confignever returns API keys, only an ellipsis plus at most the last four characters. A masked value sent back by a client keeps the stored key.
Requests pass the checks in this order: loopback peer (403), CORS, Host
header (400), token (401).
Remaining risks:
- Without a token (
rootmind apifrom the command line), any local process can use the API: read the text of every indexed document, ask questions on your API key, and change the configuration, including which folders are indexed. Use--require-tokenwhen other people or programs you do not trust use the same computer. - A token does not stop programs that run under your own account. They can read the service's token file or the memory of the app.
- A web page can still send blind requests to a backend without a token (it
cannot read the answers). Two endpoints accept an empty body:
POST /index/refreshstarts an incremental index sync, andPOST /llm/modelsasks the configured provider for its model list. - The Ollama base URL is an address the backend connects to. Only
config.yamland the authenticated settings screen can change it (it must start withhttp://orhttps://).POST /llm/modelsaccepts a base URL in its body to test a server before saving, so with the API open to local processes (no token), such a process can make the backend send a request to an address of its choice; the response is only the list of model names.
Never bind the REST API to a network interface. If you need remote access, use the MCP HTTP server.
The RootMind service (RootMind-Backend) is the desktop app's backend. It
runs the REST API and the indexer as LocalSystem, so it can index while
no one is logged in and read the user's folders without changing their
permissions. The installer sets it up with rootmind-backend.exe service install; on a machine without the app, the same command from the release zip
in an elevated prompt does it.
Who can install it. Setting the service up needs one administrator
approval (UAC) by the account that will use it. The installer compares the
account it runs as (from its process token) with the user of its Windows
session and sets the service up only for a user account that is the signed-in
one, or that runs it in a session without a user (SSH, a CI runner). When it
runs as SYSTEM or another service account (Intune, Configuration Manager,
winget's validation machines), or as another account than the signed-in one
(an administrator approving the prompt for a standard account, "over the
shoulder"), it installs only the app, never a service for an account it
cannot identify, records why in its uninstall entry and exits with 0. The app
then offers Set up, which runs service install elevated for the
signed-in account. Only an update run as SYSTEM may touch the service:
service install as LocalSystem updates our existing service for the
account it is registered for and refuses everything else (exit code 8),
including setting up a service for an account no one approved. A 1.x
installation in the user's profile is removed only when the signed-in
account runs the installer, never as SYSTEM. service install checks on its
own that the approving account is the account it sets the service up for,
and it never takes over a service that belongs to another account (exit
code 8): to move RootMind to another account, uninstall it and install it
from that account. A standard account cannot get the service, because its
REST API and its document parsers run as SYSTEM: a parser bug would then
lift a standard account to SYSTEM. The installer still refuses another
install folder than %ProgramFiles%\RootMind (exit code 10), whoever runs
it.
Reads are confined to the account's unelevated rights. The service reads
only what the installing account can read without administrator rights
(backend/src/rootmind/winaccess.py):
- It builds a Windows AuthZ access context from the account's SID. When User Account Control is on, the groups that UAC filters out of an administrator's standard token (Administrators, Backup Operators and the others Windows filters) become deny-only, as in the account's normal token. INTERACTIVE and LOCAL are treated as present; NETWORK, BATCH and SERVICE are deny-only. The service checks at startup that the filter takes effect.
- Every folder and every file is checked on its open handle: its final path must lie inside the indexed folder (so a junction swapped in after listing gains nothing), and the account must be allowed to list the folder or read the file. A hard link shares its file's permissions, so it gains nothing either. Results are cached per security descriptor.
- Refused locations: network drives and shares, device paths, paths without a drive letter (such as volume shadow copies), whole drives, the Windows, Program Files and ProgramData folders and everything below them, the folder that holds all user profiles, other users' profiles, folders the account cannot list without elevation, and folders reached through a symbolic link. A network path is refused before anything is opened, so adding one never makes the service authenticate to another computer with the computer's account.
- Inside an indexed folder the service never follows a link. Symbolic links, junctions and mount points are skipped while listing, without looking at their targets. A stored document path is resolved without opening it, and every folder on the way to a file is examined (without following it) before the file is used; a path through a link counts as missing. Files are opened without following a link at the file itself, and the handle is checked before anything is read. What remains is a race: a folder replaced by a link in the moment between its check and the open. Creating symbolic links needs administrator rights or Developer Mode.
POST /configrefuses a new folder that the policy does not allow, with 400 and the reason. A folder that is already configured, or that becomes unreadable later, stays in the settings and is listed byGET /connectorswith"available": false; the app marks it, and other settings can still be saved.- Every check fails closed. If the access context cannot be built (for
example for a domain account whose domain cannot be reached, or when the
installing account is not a user account),
service installexits with 9 and the service refuses to start; it never falls back to reading as SYSTEM.
Some folders the account can open in Explorer are still refused: folders shared only with groups that a context built from the SID does not contain (CONSOLE LOGON, This Organization, the logon session, Microsoft or cloud account groups), and folders with "Read" but without "Read & execute" (traverse) permission.
MCP over HTTP runs as its own account. The service RootMind-MCP-HTTP,
on by default, runs as the virtual account NT SERVICE\RootMind-MCP-HTTP
with only SeChangeNotifyPrivilege. It serves from the index and never reads
the indexed folders. It can read config.yaml and data\api_token.txt, write
the database folder and its own logs, and nothing else in the data folder; in
particular it cannot read the REST token. For remote access it can also read
data\ngrok_authtoken.txt and read and write (not delete, not re-permission)
data\remote_access.json. config.yaml also holds the LLM API
keys saved through the app, so the MCP service account can read them, although
it never uses them. Turning MCP over HTTP off (Settings → RootMind service)
removes the service and the account's entries from the data folder, and turns
remote access off in config.yaml. What the
token gives access to is described under
MCP over HTTP.
Program files. service install copies the program into a new folder,
%ProgramFiles%\RootMind Service\<version>-<id>\, for every install and
update: file by file, refusing links and checking the hash of each written
file. The folder is writable by SYSTEM and Administrators only, and older
version folders are deleted afterwards (files in use at the next restart). The
services always run from there, never from the app's install folder
(ARCHITECTURE.md has the reasons). The
source of that copy is checked too: the installer installs only into
%ProgramFiles%\RootMind and refuses any other folder (/D=, or a changed
install location) with exit code 10 before anything changes, and
service install refuses with exit code 10 to copy from a folder that
accounts other than administrators can change, such as a download folder: no
such account may write there, or move, replace or re-permission anything in
it or in the folders above it. Their registered command lines are quoted. Elevated code takes these
paths from the Windows known-folder API, never from environment variables,
which an elevated process inherits from the user. The service host ignores
RLM_API_TOKEN, MCP_API_TOKEN, CONFIG_PATH, RLM_DEV, RLM_HOST and
RLM_PORT, refuses a database outside its data folder, and gives the
backend service a temporary folder that only SYSTEM and Administrators can
access.
Data folder. %ProgramData%\RootMind (SY SYSTEM, BA Administrators,
U the installing account, M the MCP service account, only while MCP over
HTTP is on):
RootMind\ owner BA, protected: SY full, BA full, U read, M read (this folder and its files)
config.yaml inherited (M may read it: folder list and database path)
data\ inherited, M: nothing
rest_token.txt inherited (M: nothing)
api_token.txt inherited + M read
ngrok_authtoken.txt protected: SY full, BA full, M read (U: nothing)
remote_access.json inherited + M read and write (no delete, no permission change)
db\ inherited + M modify (index.db, -wal, -shm)
logs\ inherited
backend\ service log
mcp-http\ inherited + M modify
install.log results of install, update and uninstall
tmp\ protected: SY full, BA full
A missing folder is created with these permissions in one step. An existing
one, for example one kept by an uninstall without Delete the application
data, is reused only when every item in it is owned by SYSTEM or
Administrators, grants write access to no other account (except M on its two
folders and on remote_access.json), and contains no reparse points or
hard-linked files. An update stops
both services before it checks the folder, and the permissions are written
through a handle of each item, never through a link that replaced it. Files
that service install creates are owned by Administrators, also for an
administrator with User Account Control turned off. Otherwise service install exits with 6: "%ProgramData%\RootMind exists but is not protected.
Remove or rename it from an elevated prompt, then try again." A folder
created by hand inherits "Users: create" from ProgramData and is refused.
Elevated commands from the app. The webview can call only a fixed set of
commands; the Rust side builds every program path and argument. Set up,
Repair and "Update service" elevate service install --user-sid <the signed-in account> with the backend installed with the app, %ProgramFiles%\RootMind\backend\; only when that is missing do they use the
service's registered copy under %ProgramFiles%\RootMind Service\. Switching MCP
over HTTP on or off elevates the registered copy, after checking that the
registered path lies under %ProgramFiles%\RootMind Service\. Removing the
service is the uninstaller's job; the app sets it up only when the installer
did not (Set up).
Token handoff. Before the app hands the service's token to its window,
the server on the service's port must prove that it knows the token, without
the token being sent: GET /proof?nonce=<random> answers
HMAC-SHA256(token, "rootmind-token-proof\0" + nonce), which only a holder of
the token can compute, and a fresh nonce keeps an old answer from being
replayed. A program of another account (or forwarded from WSL) that takes
over the port cannot answer and never receives the token. The proof does not
depend on the TCP listener table, which on some Windows systems does not list
the service's socket at all. A service from before /proof (older than
2.6.1) is checked as before: the process ID from the Service Control Manager
must equal the owner of the listening socket. The check is repeated on every
connection check (every 5 seconds, paused while a question is being
answered). If the service crashes and another program binds its port before
the next check, requests made in that window can still carry the token.
service install and the rootmind command line make the same check before
they send the REST or MCP token: if the program on the port cannot prove the
token, service install stops with exit code 7 and sends no token.
/proof answers only GET with a valid nonce; on the MCP server only on
loopback, not on its network listener.
Approving the prompts. Every program the app or the installer runs as
administrator lies in a folder that only administrators can change: the
app's backend in %ProgramFiles%\RootMind\backend\ (the installer accepts no
other folder) or the service's copy under %ProgramFiles%\RootMind Service\.
Malware running as your account therefore cannot replace it before you
approve, and service install refuses to copy from a folder that other
accounts can change (exit 10). The prompts show an unsigned
program, so approve one only when you started the action yourself.
Document parsers run as SYSTEM in the service, on files the account can read. A malicious file that exploits a parser bug would run code as SYSTEM. Index only folders whose content you would open yourself.
Uninstall and purge. Uninstalling RootMind (Windows Settings → Apps) runs
service uninstall with the service's own copy: it stops and deletes both
services, deletes the program folders (files in use at the next restart) and
data\rest_token.txt. It deletes anything in the data folder only when the
folder passes the same check as for a reinstall; otherwise it leaves the
folder untouched and says so. Without Delete the application data, and
always for a silent uninstall (/S), the data folder stays: config.yaml
(with saved API keys), the index, the logs and the MCP token, so MCP clients
keep working after a reinstall. With the box ticked, service uninstall --purge deletes %ProgramData%\RootMind, and the uninstaller also deletes
the app data (chats) of the account that runs it. To delete the data later,
remove %ProgramData%\RootMind from an elevated prompt. An uninstall that is
part of an update (/UPDATE) keeps the service. If the services cannot be
removed, the uninstaller names the command to run from an elevated prompt,
and service status warns while RootMind is no longer installed. To rotate a
token, delete its file from an elevated prompt and run service install
again.
On Linux the RootMind service is two systemd user services of the
account that uses RootMind, rootmind-backend.service and
rootmind-mcp-http.service in ~/.config/systemd/user/. There is no system
service, no root daemon and no second account: the units run as that account,
with its rights. The app sets them up on its first start with service install, run as the account; nothing in the process needs root, and
service install refuses to run as root (exit 8).
What the service can read. Everything the account can read, mounted network shares included, and nothing more. There is no confinement layer as on Windows because there is no privileged process to confine: a folder the account cannot open is not indexed.
Who can reach it. The REST API and MCP over HTTP bind to 127.0.0.1 and
require their bearer tokens, exactly as on Windows. Other accounts on the
computer can connect to the ports but have no token. Each account that starts
RootMind gets its own pair of services (the second on the fallback ports),
with its own data folder and tokens.
Files and modes. The data folder $XDG_DATA_HOME/rootmind (default
~/.local/share/rootmind) and every folder in it are mode 0700; the REST and
MCP tokens, the ngrok authtoken, config.yaml (API keys) and the
remote-access status are 0600, and the units run with UMask=0077, so the
index, logs and temporary files are private too. New folders and files are
created with their final mode (and fchmod against a lax umask), so no file
is readable by others at any moment, and secrets are replaced atomically
through a new owner-only file. An existing data folder is used only when it
and everything in it belong to the account, it contains no symbolic link and
no file with a second hard link, the folder, data/ and the secret files are
writable by nobody else (a user private group of the account's own name and
no other member counts as the account), and the folders above it do not let
another account replace it (writable by others only with the sticky bit).
Otherwise service install stops with exit code 6 and changes nothing. Each
service checks the folder, data/ and the secret files again when it starts
and refuses to start (exit code 2, not restarted) if they are not protected.
The program it runs. An AppImage runs from a temporary mount, so
service install copies the bundled backend into
~/.local/share/rootmind/runtime/<version>-<id>/ (mode 0700), verifying each
file by SHA-256, and the units run that copy; older copies are deleted after
a successful switch. It refuses to copy from a folder that accounts other than
root and the account can change (exit 10): the packages' /usr/lib/RootMind
belongs to root, and an AppImage's mount to the account. The copy lives in
the account's own data folder, so only the account (and root) can change
what the service runs, which is no more than the account can already do.
service.json next to it names the copy for the Claude Desktop bundle and
holds no secret.
Tokens and the proof. As on Windows, service install, the app and the
command line send a token to the service only after the server on the port
proved that it knows it (GET /proof, above), so a program of another
account that took the port first never receives a token. A service from
before /proof is checked in the listener table instead: every socket that
receives 127.0.0.1:<port> must belong to the unit's main process (from
/proc/net/tcp, /proc/net/tcp6 and the process's /proc/<pid>/fd).
The unit files carry no secret: the program path, the role and the port.
service install changes only units that carry its marker and run a copy
from the account's data folder; a unit with RootMind's name that it did not
write, in the account's folder or loaded by systemd from elsewhere, is left
alone (exit 3).
Uptime. The units start when the account logs in and stop when its last
session ends. loginctl enable-linger keeps them running without a session;
RootMind never turns that on by itself.
Uninstalling. service uninstall stops, disables and deletes the units
and the program copies and deletes data/rest_token.txt; the data folder
stays unless --purge. Removing the .deb or .rpm does not reach the user
units of each account (a package script cannot stop another account's user
services reliably), so the README says to run service uninstall first; the
units left behind would keep running from their own copy until then.
The MCP client starts rootmind-mcp (or rootmind-backend.exe mcp) as a
child process with the client's user rights. There is no authentication
because there is no listening socket. All five tools are read-only. The
stdio server serves the database its own config.yaml names (a source or uv
setup); it cannot open the service's index, whose folder the account can only
read.
The RootMind service serves MCP over HTTP by default: the service
RootMind-MCP-HTTP listens on 127.0.0.1, port 3000 or one from
18400–18499. Its token is in %ProgramData%\RootMind\data\api_token.txt,
which the installing account, SYSTEM, Administrators and the MCP service
account can read. So every program that runs as the installing account, and
every administrator, can read the token and through it every indexed
document. That is the same group that can already read the REST token and the
index file itself. Other accounts can connect to the port, but without the
token they get 401. The token stays the same across updates and across an
uninstall that keeps the data, and copies of it live in the MCP clients you
set up (for example n8n credentials or the Claude Code configuration). If you
do not use MCP, turn it off under Settings → RootMind service.
The Claude Desktop bundle (rootmind.mcpb) keeps no copy: it runs as the
signed-in account, reads the token file named by service status --json at
start and after a connection error, and sends the token only in the
Authorization header to 127.0.0.1. It never writes the token to its output
or its log (stderr); tests assert both. Unlike the app, it does not check
which process owns the port (Node.js has no API for that); it sends the token
only to the port that service status reports for the running MCP service.
-
Every MCP endpoint requires
Authorization: Bearer <token>. Only/healthis open; there are no/docs,/redocor/openapi.jsonpages. -
It only serves by default. With
--indexit also indexes, using the folders from its own config. It re-reads the folder list whenconfig.yamlchanges. -
The token comes from
MCP_API_TOKENordata/api_token.txt. If neither exists, the server generates 32 random bytes (secrets.token_urlsafe) and writes them to that file. If the variable or the file is present but empty, the server refuses to start (fail closed). The service never generates it:service installwrites it, or takes it from--mcp-token-from FILE. -
Tokens are compared in constant time (
secrets.compare_digest). The server never logs or prints the token, only its source and an 8-character SHA-256 fingerprint. Tests assert that the token never appears in log output. -
Requests with a browser
Originthat is not a loopback origin (or listed inMCP_ALLOWED_ORIGINS) are rejected with 403 before the token is checked. -
It binds to
127.0.0.1by default.rootmind mcp-http --hostwith another address needs--tls-certand--tls-key, or--insecure-httpfor plain HTTP, and warns. The service's loopback listener speaks plain HTTP: to reach it from another machine use remote access (a TLS tunnel), or the network listener with TLS or behind a TLS proxy (see Server use). Never expose a plain port. -
Requests with a browser
Originare also accepted from the origins inmcp_http.allowed_originsinconfig.yaml. -
Named tokens (
rootmind mcp tokens create, orPOST /mcp/tokenswith the REST token) are generated by the backend (32 random bytes), returned once, and stored only as their SHA-256 inconfig.yaml(mcp_http.tokens). A token'sfoldersnarrow the folder filter of the tool layer (ToolContext.allowed_connectors) for its requests: searches, listings, reads by id and the statistics see only those folders, and theinitializeinstructions name only them. A legacy SSE session belongs to the token that opened it. The server re-readsconfig.yamlwhen it changes (checked on every request), so a new token works and a revoked one is refused with its next request, without a restart. Tool calls are logged with the token's name. -
The token in
data/api_token.txtis the token nameddefault. It sees every folder, cannot be revoked through the API (the app, the Claude Desktop bundle andservice installuse it) and is not accepted on the network listener unlessmcp_http.network.allow_default_tokenis set.
Limits: tokens do not expire and there is no rate limiting. The default token
can read every indexed document. To rotate it, delete data/api_token.txt
(or change MCP_API_TOKEN) and restart the server; for the RootMind service,
see Background service. Named tokens are rotated by
creating a new one and revoking the old one.
With mcp_http.network.bind set, the RootMind service's MCP process also
listens on an address other machines reach (see
docs/SERVER.md). The loopback listener and everything above
stay as they are. What changes:
- One index serves every token. RootMind indexes what the service's
account can read, once, and does not check the permissions of a share per
user: the folder scope of each token is the access control. Someone
whose token covers
projectssees every indexed file inprojects, even files the share would not show them. Index only folders whose whole content every holder of a token for them may read, give each person or client their own token with--folders, and keep unscoped tokens (every folder, the default when--foldersis omitted) for administrators. - Transport. The network listener speaks TLS with
tls_cert/tls_key. Plain HTTP on a non-loopback address needsinsecure_http: true; tokens and document text then cross the network unencrypted. On a loopback address it is meant for a reverse proxy on the same machine that terminates TLS; only there areX-Forwarded-*headers trusted, and only from 127.0.0.1. The service refuses to start the MCP process when the settings are incomplete or the certificate cannot be loaded, and logs a warning whenever the network listener runs. - Host and Origin. Every request on the network listener must carry a
Hostfrommcp_http.allowed_hosts(required), else 400 before any token check. BrowserOrigins other than loopback are refused unless listed inmcp_http.allowed_origins(both listeners). - Tokens on the network are named tokens only, unless
allow_default_tokenis set. A stolen token gives what its folders hold until it is revoked; revocation takes effect with its next request. - Who manages it. Tokens, folders and the LLM key are changed through the
REST API, which listens on 127.0.0.1 only, needs the REST token and refuses
other peers; on Linux the token file is readable only by the service's
account. Everybody who can act as that account (and root) controls the
server.
askand the REST API are not scoped: they are the administrator's. - Not covered: rate limiting, token expiry, an audit log beyond the tool call log lines, per-user LLM budgets, and denial of service by expensive searches. Put a reverse proxy with rate limits in front when the network is not trusted.
rootmind status, folders, index, search, ask, config llm,
mcp tokens and remote-access use the REST API of the account's service:
- They read the port and the token file from
service status, and send the REST token only after checking that the service's own process (the PID from the Service Control Manager or systemd) listens on the port, as the desktop app does. Otherwise they stop with exit code 3 and send nothing. - The REST token is read from its protected file; an account that cannot read it gets exit code 4 and an explanation, never a fallback.
- Requests go to 127.0.0.1 without a proxy and never follow a redirect, so
the token cannot be forwarded to another address.
--urlaccepts plainhttp://only for loopback addresses. - Secrets come from files or stdin, never from command-line arguments (which
other accounts can see in the process list), and are never printed, not even
masked. A new MCP token is written to a new file (
--out, mode 0600 on Linux, an access list for the account alone on Windows), or to stdout only with--out -.
Remote access is off by default. When the user turns it on (Settings →
RootMind service, after a confirmation that names both risks below), the
RootMind service's MCP server opens an outbound connection to ngrok with the
user's authtoken and gets a public HTTPS address that forwards to
127.0.0.1:<MCP port>. No inbound port is opened on the computer, and no
ngrok program is installed: the tunnel runs in-process through ngrok's
open-source SDK (PyPI ngrok, a native extension locked with hashes in
uv.lock).
What it exposes:
- Only the MCP server. The tunnel forwards to the MCP port and nothing
else:
/healthanswers without a token, every MCP endpoint needs the MCP bearer token, and requests with a browserOriginthat is not loopback are refused before the token is checked. The REST API is never reachable through it (another port, and it refuses non-loopback peers and foreignHostheaders anyway). - Every indexed document, to anyone with the MCP token. The token is 32 random bytes and compared in constant time; there is no rate limiting, request log or lockout on the public path, and no expiry. Treat it like a password, keep the copies in assistants and n8n credentials, and rotate it (see Background service) if it may have leaked.
- The traffic to ngrok. ngrok terminates TLS at its edge, so ngrok can see requests and answers, including the MCP token and the document text a client reads. What a cloud assistant reads goes to that assistant's provider as well. Both are named in the confirmation and in the README.
- Turning remote access off (or MCP over HTTP off, or stopping the service)
closes the tunnel within a few seconds; the public address then answers
with ngrok's error page.
service install --no-mcp-httpalso turns it off inconfig.yaml, so turning MCP over HTTP on later never reopens the public address by itself.
The authtoken:
- The app sends it once to the REST API (
POST /remote-access, bearer token, JSON only); the field is write-only, and no endpoint returns it, only whether one is saved. An invalid value is refused with 400 without being echoed. - The backend (LocalSystem) writes it to
data\ngrok_authtoken.txt: a new file created with its final, protected access list (SYSTEM and Administrators full, the MCP service account read while the MCP service exists) and then moved over the old one, so no other account can open it at any moment. The installing account's standard token cannot read it (an elevated administrator can, as for every file there). It is never part ofconfig.yaml, which more accounts can read.service installand the data-folder check enforce the same entries. - It is never logged: RootMind's messages leave it out, errors from ngrok are redacted before they reach the log or the status file, and while the tunnel runs, a filter on the service's log handlers replaces it in every record (the SDK logs into Python logging). Tests assert all three.
- Someone who gets it can use the user's ngrok account (open endpoints, traffic quota), not RootMind's data. Revoke it in the ngrok dashboard.
The MCP service account, which runs the tunnel, can read the authtoken and
write exactly one file in the data folder, remote_access.json (state, URL,
last error, time; no secrets), in place: it can neither delete it nor change
its permissions. The backend reads that file with a size limit, checks every
field and passes on only https:// addresses, so even a compromised MCP
process gains nothing through it beyond what it can already read (the index).
Indexed documents are untrusted input. A document can contain text such as "ignore all previous instructions", and the model will read it. RootMind limits what such text can achieve:
- Read-only tools. The agent can search, list and read. It cannot write files, run commands, send messages or make network requests. A successful injection can produce a wrong or misleading answer; it cannot make RootMind act.
- Data framing. Tool results wrap document text in
<document>and<snippet>tags. Tag-like text inside, opening or closing and in any letter case, is escaped, so it cannot end the wrapper or start a new one. File names in result headers are reduced to one line without tags. The system prompt tells the model that this content is data and that instructions inside documents must not be followed. This reduces the risk; it does not remove it. - Budget. At most 8 tool calls run per model turn and at most 200,000 characters of tool output per question, so injected text cannot make one question read the whole index or run up an unbounded bill.
- Scope. Tools only return documents from the folders in the current configuration.
- Rendering in the desktop app. Answers are rendered as Markdown without
raw HTML. The CSP blocks remote images and remote network requests, which
closes the usual way of leaking data through a rendered answer. Links open
only on click, only for
http,httpsandmailto, and in the system browser. A crafted link that you click can still carry data in its URL. - Evals. Eval case c15 has an injection line in its source document that asks the model to say all prices are now free. The live eval fails the case if the word "free" appears in the answer.
MCP clients with other tools. When you connect RootMind to an MCP client that also has powerful tools (a shell, file writes, web access), document text enters that client's context. An injected instruction could try to make the client use those other tools. RootMind cannot prevent that; rely on the client's permission prompts and do not auto-approve tool calls after reading untrusted documents.
- Each folder connector resolves every path and rejects anything outside the folder root, including sibling folders with the same prefix and symlinks that point outside. While listing a folder it does not descend into symbolic links or NTFS junctions. In the background service, paths are resolved without following any link, links are skipped without being followed, and the folder access policy above checks every folder and file as well.
- The tools and the REST API read text from the index, not from disk.
GET /documents/{id}/path, which the desktop app uses to open a file, resolves the stored path through the same connector check. - The desktop app can only open files with the extensions the backend indexes (a test checks this against the parser list). It cannot launch executables, scripts or shortcuts through the opener. The allowed types include HTML and Office files, which open in their default applications with those applications' own protections.
Every file in an indexed folder that matches the include patterns and has a
parser is parsed: PDFs with pypdf, Office files with python-docx,
openpyxl and python-pptx, and text formats (TXT, CSV, Markdown, JSON,
XML, HTML, reStructuredText) as text. Other extensions are not indexed.
A malicious file that exploits a parser bug runs code with the backend's
rights: your own on the command line, LocalSystem in the RootMind service. Mitigations are limited to max_file_size_mb
(default 50 MB) and keeping dependencies up to date. Index only folders whose
content you would open yourself.
- CSP: scripts, styles and images only from the app itself (styles may also
be inline); no plugins,
<base>or form posts; network requests only to the Tauri IPC and tolocalhostand127.0.0.1. Tauri applies the CSP to production builds;pnpm tauri devloads the page from the Vite server without it. - Capabilities: no
core:defaultset. The window may set its theme, open a folder picker, open web and mail links in the default apps (the Markdown renderer only passeshttp,httpsandmailto), reveal a file in Explorer, and open files of the indexed types. The app has no shell plugin, and the webview has no permission to start processes. The developer tools toggle exists only in debug builds. - Rust commands: chat history (save, load, list, delete; every command that
takes a conversation id validates it, so it cannot escape the history
folder) and the RootMind service (state, connect, repair or update, MCP
over HTTP on or off, MCP token, details). No command takes a program path,
arguments or environment variables from the webview. The app starts the
backend program from its install folder only for
service status --json(not elevated) and for the elevatedservice installof Repair and "Update service"; switching MCP over HTTP runs the service's registered copy. On Linux the same commands runservice installas the account itself (no elevation), with arguments built in Rust, and the app runs it on its own once on the first start and once after an update of the app. - The app starts no backend of its own. A debug build never connects to the installed service on its own; it asks for a backend address.
- Releases are built by
.github/workflows/release.ymlfrom a version tag, without caches. The jobs that build and test have read-only tokens; one final job with write access packs and publishes what they built. Pull requests and manual runs are dry runs with read-only tokens and no secrets. - Every release asset is listed in
SHA256SUMS.txtand has a build provenance attestation (gh attestation verify <file> --repo cgaeking/rootmind). The installer contains exactly the backend build that passed the smoke and service tests; its tree hash is checked before and after packaging. With GitHub's immutable releases turned on (see CONTRIBUTING.md), published assets cannot be replaced. - The binaries are not code-signed yet. Windows SmartScreen will warn, and
Smart App Control may block them. The Linux packages are not signed with a
GPG key either (
dnfandzypperinstall them as local files). Verify the checksum and the attestation, or build from source. - The Linux packages contain exactly the Linux backend build that passed the smoke and service tests (tree hash checked before packaging), except that inside the AppImage linuxdeploy rewrites the run paths of its shared libraries; the release job runs that backend as well.
- Dependencies are locked (
uv.lock,pnpm-lock.yaml,Cargo.lock) and CI installs from the lockfiles; the release build fails ifCargo.lockdoes not match. The Rust compiler version is pinned in the workflows as well. Dependabot proposes monthly updates; a CI job scans all tracked files for secrets (detect-secrets, against the audited baseline.secrets.baseline). - GitHub Actions are pinned by commit SHA and check out without persisted credentials. Tests never contact an LLM provider: real SDK clients cannot be constructed in tests, no keys are set, and the provider base URLs point at a closed local port. Tests never contact ngrok either: remote access runs against a fake SDK, and the smoke and service tests never turn it on.
- Several users sharing one backend with their own permissions. The RootMind service belongs to the account that installed it (on Linux, every account has its own). In server use, several MCP clients share it, separated only by the folder scopes of their tokens.
- Exposing the REST API to a network, or the MCP HTTP server without TLS. Remote access and the network listener with TLS (or a TLS proxy) are the supported ways to expose MCP over HTTP; what ngrok and a cloud assistant's provider do with the traffic is under their terms.
- Malware running under your own account, attackers with administrator rights, and physical access to the machine.
- Content that LLM providers receive, store or log under their own terms.
- Answers that are wrong because the model misread or was misled by a document. Check important answers against the cited sources.