# Filesystem MCP Server Permissions & Controls, Explained

How the filesystem MCP server's allowed-directories boundary actually works — CLI args, the MCP roots protocol that silently replaces them, two patched path-check CVEs, and why read-only means a Docker mount.

# The filesystem MCP server control model, explained

The filesystem server is the reference server for this series' most personal surface: the agent's own disk. It's maintained by the Model Context Protocol project itself ([modelcontextprotocol/servers](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem)), survived the May 2025 reference-server cull, and at [~232k weekly npm downloads](https://socket.dev/npm/package/@modelcontextprotocol/server-filesystem) has the highest raw install volume of the ten servers we surveyed. Its control model is also the simplest: one boundary — a list of allowed directories — and nothing else. This page is what that one boundary checks, who can move it, and the two times it broke. Sources: the [official README](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem) and the linked issues and advisories.

*This page covers the server's own controls. For the cross-server picture, see [the MCP server controls comparison](/mcp-controls); for what a control layer adds on top, [the coverage matrix](/coverage).*

## What's at stake on this surface

Unlike [GitHub](/mcp-controls/github) or [Supabase](/mcp-controls/supabase), there's no service on the other side of this server — the blast radius is the local machine. The tool surface includes `write_file`, `edit_file`, `move_file`, and `create_directory` alongside the reads (`read_text_file`, `read_multiple_files`, `search_files`, `directory_tree`). File operations here are effectively destructive: an overwrite has no previous version, a move has no trash, and none of it has an undo. The reference incident for this surface is [the Amazon Q wipe](/blog/recreated-amazon-q-filesystem-wipe) — an agent with file and shell access executing deletion it was injected into running. That's the sentence the one control below exists to narrow.

## The allowed-directories boundary

Directories are passed as CLI arguments in the client config:

```json
{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": [
        "-y", "@modelcontextprotocol/server-filesystem",
        "/Users/you/projects/one",
        "/Users/you/projects/two"
      ]
    }
  }
}
```

Every tool call validates its path arguments against that list; `list_allowed_directories` reports the current state; the server errors at startup if the list would be empty. That's the entire native model — one list, checked in-process on every call. It's a real restriction and the right first move: an agent that only needs one project directory should be handed exactly one. But note what kind of control it is: a path check in the same process the agent drives, configured in a file the agent can typically edit, with no distinction between reading a path and destroying it.

## The roots quirk: the client can rewrite the fence

The [README](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem) offers a second way to set allowed directories — the MCP roots protocol — and documents the interaction plainly: roots provided by the client **completely replace** the server-side allowed directories. At initialization, if the client advertises roots support, the server fetches `roots/list` and swaps the list in; a `roots/list_changed` notification repeats the swap mid-session. [Issue #3602](https://github.com/modelcontextprotocol/servers/issues/3602) documents the practical consequence against v2026.1.14: start the server with four CLI directories, connect a roots-capable client, and `list_allowed_directories` returns only the client's roots — the CLI directories are dropped, silently.

The project's framing is ergonomic — roots let the boundary follow the client's workspace without a restart — and as a workspace convenience that's fair. As a control it means the boundary is **client-writable at runtime**. The client is the process the agent lives in; whatever the agent's session can convince the client stack to send as roots is the new fence. A directory list that the side being restricted can replace mid-session is configuration, not enforcement — the same shape as [GitHub's dynamic toolsets](/mcp-controls/github), one layer down. If your safety story is "the server can only see `/projects/one`," verify it holds against your actual client, not just your config file.

## The check has failed twice

Two CVEs, both disclosed mid-2025, both in the boundary check itself:

- **[CVE-2025-53110](https://nvd.nist.gov/vuln/detail/CVE-2025-53110)** — the allowed-directory check used path-*prefix* matching, so `/tmp/allowed_evil` passed a check meant for `/tmp/allowed`. CVSS 7.3.
- **[CVE-2025-53109](https://nvd.nist.gov/vuln/detail/CVE-2025-53109)** — a symlink placed inside an allowed directory could be followed out of it. CVSS 7.3.

Both were patched in July 2025 (versions 0.6.4 / 2025.7.01), and neither has known exploitation. The point isn't that the maintainers were careless — prefix bugs and symlink races are the two classic ways path restriction fails, and plenty of mature software has shipped both. The point is that when the *only* control is an in-process path check, every bug in that check is a full boundary failure with the disk as blast radius. There is no second layer to hold.

## What doesn't exist

- **No read-only mode.** There is no flag, and no way to serve only the read tools. The tools carry MCP annotations (`readOnlyHint`, `destructiveHint`) so clients can tell reads from writes — but annotations are hints to the client, not enforcement by the server.
- **No per-tool filtering.** You cannot launch the server without `write_file` or `move_file` on the surface.
- **No approvals.** No call can be held for a human by the server; any confirmation dialog is the client's courtesy.
- **No audit.** The server writes no record of which files an agent read, wrote, moved, or deleted — [the general MCP audit gap](/blog/mcp-audit-gap), on the surface where "what did it delete" is the first question anyone asks.

The honest read-only workaround is the documented Docker setup: bind-mount directories into the container with the `ro` option, and the OS refuses writes regardless of what the tool layer does. That's enforcement below the server — the strongest property available on this surface, the same layer-below logic as [Supabase's read-only Postgres role](/mcp-controls/supabase), and the reason [containment and authorization are different jobs](/blog/sandboxes-and-control-planes). What it costs: a Docker dependency, per-directory mount configuration, and it's all-or-nothing per mount — there is still no "read anywhere, write this one place with approval."

## Where the native model ends

Grade this server on its own terms — it's a reference implementation, it documents what it does, and what it does is one directory list. The shape of what's missing:

- **The one boundary is client-writable at runtime.** Roots replace the CLI list by design; the fence answers to the side being fenced.
- **The boundary is binary.** A directory is fully in — readable, writable, movable, overwritable — or fully out. "Read broadly, write narrowly" is not expressible.
- **The check is in-process,** and its two known failures were both full escapes.
- **Nothing is held and nothing is recorded.** No approvals, no trail, no way to reconstruct what an agent did to the disk.

The practical posture: pass the narrowest directory list that does the job, pin the package version past July 2025, test what `list_allowed_directories` actually returns under your client before trusting the config, and prefer the Docker `ro` mount over trusting the in-process check for anything the agent shouldn't write. Then put the read/write distinction, the approval on destructive calls, and the record of what actually happened in [a layer the agent can't edit](/what-is-an-agentic-control-plane) — because on this surface, the native model doesn't have one.
