Security
The token that leaves your network cannot do anything.
The boundaries this project holds, why each one exists, and exactly what you give up if you relax it.
Two servers, two tokens, two bind addresses.
Most MCP servers expose reads and writes behind one credential, so a leaked token is total. Brainlyy runs two.
The write server doesn't prefer loopback. It reads its bind address at startup and exits if that address is not a loopback address. A config typo can't silently publish write tools.
| Read server | Write server | |
|---|---|---|
| Binds | your mesh address | 127.0.0.1 only |
| Token | read-token.txt | write-token.txt |
| Tools | search_notes, ask_*, code_search | post_message, run_agent, agent_status, rate_answer |
A property test proves this by starting the process with a wildcard bind and a valid token, then reading the exit code. The missing-token case is the negative control. Without it, a refusal for an unrelated reason would look identical to the guard working.
Why the split is not paranoia.
Every argument an MCP tool receives began as model output. Model output can be steered by anything the model has read: a web page, a note, a repository someone else wrote.
A read tool talked into misbehaving returns a wrong answer. A write tool talked into misbehaving posts, runs, or deletes. Splitting the credentials means the one that travels your network cannot act.
Reachability, and logging in.
The dashboard is served by the read server, and a browser cannot set an Authorization header on a navigation. So it uses a session cookie: the one-time token is verified with a constant-time compare, exchanged for an HttpOnly, SameSite=Strict cookie, and the token leaves the URL bar on the redirect.
A session cookie never authenticates the MCP route. The browser attaches cookies automatically, so anything they open is open to any page the browser is on, and the tools must never be in that set.
For a phone on the mesh, set a passphrase. It stores an scrypt hash, never the passphrase. Five failures from one address lock it out for fifteen minutes, counted on a monotonic clock, and the lockout refuses the correct passphrase too. A lockout that keeps verifying is just a slower error message.
For a team, do not stretch this into multi-user auth. Put an OIDC-aware reverse proxy in front and let it own identity, MFA and revocation, which are entire products.
$brainlyy passphrase$brainlyy ui --print-urlWrite actions in the dashboard.
The dashboard's write endpoints do not act. Each is proxied to the write server over loopback with the write token attached inside the proxy, so everything past that hop is enforced exactly as it would be for a local caller. The dashboard decides only whether to forward.
That decision is judged on the address the connection arrived on, the door, never on the client address, which says who is asking rather than which door they came through.
| Status | Meaning |
|---|---|
| 401 | no session cookie or bearer; the /api gate, inherited |
| 403 | the connection did not arrive on loopback and ui.remote_writes is false; the body names that key |
| 400 | the body is not the JSON the endpoint documents |
| 503 | the write server is not running; an absent capability, not a crash, and the body says which process to start |
| 502 | the write server answered but refused the call |
| 405 | any method but POST; a write a GET can reach is a write a prefetcher can make |
| 200 | the write server ran the tool; the body carries its output |
- a session cookie captured from your phone can run an agent or post a message
- the practical value of the read and write split drops to the strength of one cookie, because the mesh-reachable surface can now act
- the write server's own guard is still intact, but the thing deciding whether to proxy is now network-facing
It's off by default for those reasons. If you turn it on, prefer a tailnet with device approval, and rotate the read token if a device is lost.
Secrets, and the public edge.
Secrets never appear in config. Setup generates them into separate files, so a config can be committed and shared while credentials cannot. Both live outside the install, so upgrading never touches them.
If you expose the read server publicly, source-restrict the port to your assistant provider's published egress range. Reachability beats authentication: a request that can't arrive can't be replayed, brute-forced, or scanned.
Use HTTP-01 for ACME, never TLS-ALPN-01. TLS-ALPN-01 validates over 443, which your allowlist blocks. Renewal then fails silently, about sixty days after issuance.
Reporting a vulnerability.
Open a private security advisory on the repository. Please don't open a public issue.