Security

Who can reach the server

By default the OAuth flow is deliberately public: it exists so that Claude Code’s discovery handshake completes, and there are no real credentials behind it. Anyone who can reach the server’s address can use every tool it exposes, so access control has to come from the network layer. Do not expose an unauthenticated server to the public internet. Run it on a private network, or an overlay network such as Tailscale, so that only devices you trust can reach it.

When the server has to be reachable from outside such a network, turn on authentication instead.

Treat the server as having the same reach as its most permissive vault: whatever the process user can read and write in a configured vault, a connected client can too.

Authentication

Setting auth in the config file makes the server check a signed token from an OAuth service you choose, for users you list, on every call to /mcp. It is the only thing that makes it reasonable to put the server on a public address. What it does, and does not, give you:

Limiting what a client can touch

The config file’s denyPaths blocks specific folders, globally or per vault, and is enforced in the server before a tool touches any file. It is a coarse guard, not a substitute for network access control, and it has one exception: query-graph answers from a graphify knowledge graph without consulting the deny list, so a graph built over denied folders can reveal their content. Build the graph from a vault without that content, or don’t use the tool.

denyBinaryPaths is a narrower, read-only guard for binary files. It stops read-file returning a protected file’s bytes, and stops move-file (and move-note, for a non-markdown file) moving a protected file out of protection, so the agent can’t relocate a file to read it. .trash is protected along with it, and read-note refuses a protected non-markdown file. It doesn’t affect markdown notes, and it doesn’t hide names: list-files still lists a protected file, with its modification time and size. To find protected files, start with list-files. A path scope that is a symbolic link, or passes through one, is not followed: those tools list nothing for it.

Symbolic links are handled differently by different tools. read-file resolves a link and checks the real location: it refuses a link that points outside the vault, and applies denyPaths and denyBinaryPaths to the target. The other tools, including read-note, judge a path as the client gives it and read through a link. If the process user can read the target, a connected client can read it with read-note by way of a symlink in the vault, whatever the deny lists say. Don’t put symlinks to sensitive files inside a served vault.

Other protections that apply to every request:

fetch-file

This tool makes the server perform an outbound request to a URL chosen by the caller. On any deployment, the host it runs on may be able to reach private network services that a remote client should never be able to reach. Treating that as the main risk, the tool:

read-file

This tool is read-only and makes no network request or subprocess call. It checks the path against denyPaths and denyBinaryPaths before any filesystem access, and checks the file’s size against readMaxBytes using stat before reading it, so an oversized or protected file is never loaded into memory. Symbolic links are resolved first, and only regular files are returned. The denyBinaryPaths matcher runs in time bounded by the pattern and path lengths, so a client can’t slow the server with a crafted path. The size limit matters because the whole file is base64-encoded into one response, which is a third larger than the file itself.

upload-file

This tool reserves a one-time URL, and a file sent to it ends up in the vault, so the URL has to be hard to misuse. It is built like this:

What it doesn’t do: it is not authentication. The server has no real credentials (see Who can reach the server), so anyone who can call its tools can reserve a URL. The token limits one upload to one destination; it doesn’t decide who may upload. The URL also appears in the tool’s result, so it is visible to the model and in the conversation transcript, which is why it is short-lived and works only once.

An upload does not have to come from the address that reserved its URL: the check is off by default, because the call and the curl often leave from unrelated networks (a vendor’s cloud and your own machine). What protects the URL is that it works once, briefly, for one destination, and for a file that matches the SHA-256 and size declared when it was reserved, so anyone who gets it can only upload those exact bytes or use it up. The audit log records the address of the call (reserved_from) and of the upload (addr) for every placed file.

uploadNetworkPrefix turns the address check on: 32 for the same address, 16 to 31 for the same IPv4 network. Callers behind the same NAT share an address, and behind a reverse proxy the check does nothing unless trustedProxies is set. Set it only to proxies you run, because the X-Forwarded-For header from anywhere else is ignored; if the server can be reached directly as well as through the proxy, a forged header would otherwise defeat the check.

replace-file

This tool reserves the same kind of one-time URL, and everything above applies: the token, the single use, the fixed destination, size and hash, the Origin refusal, the staging folder and the symbolic-link handling. What differs is that it overwrites, so it has to be hard to use on the wrong file:

The audit log

The server can keep a record of who did what. Every tool call, every login and refused login, and every upload is logged with the user, the caller’s address and the host the request named; see Logging and the audit log. Set logFile to keep it in a file of its own. It never holds a token, an upload URL, the text of a note or a search query. It does hold user names, addresses and the paths a call touched, so the file is created readable by its owner only and should be kept that way. The log is a record for you; it doesn’t stop anything.

Reporting a problem

If you think you have found a security problem, please raise it through GitHub issues. Describe the problem without including a working exploit.