Filesystem security for AI agents
Most agent configurations contain exactly one line that determines whether a prompt injection is an inconvenience or an incident. It is usually a path.
One argument
- "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me"]
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/code/project"]
The first grants an agent your SSH private keys, your AWS credentials, your kubeconfig, your GPG keyring, your npm publish token, every other client's code on the machine, and every document you have ever downloaded.
The second grants it the project you are working on.
Same tool, same server, same credential, same everything else. The difference in blast radius is roughly total, and it is one argument.
It is the cheapest large win available in agent security.
What is in your home directory
Developers under-estimate this consistently, because a home directory does not feel like a credential store. Here is a partial inventory of a normal one:
| Path | What it is |
|---|---|
~/.ssh/id_ed25519 | The private key that authenticates you to every server and git host |
~/.aws/credentials | Long-lived cloud access keys |
~/.config/gcloud/ | Refresh tokens and application default credentials |
~/.kube/config | Cluster credentials, frequently production |
~/.gnupg/ | Signing and encryption keys |
~/.docker/config.json | Registry credentials, often with push access |
~/.npmrc | A publish token: a leak here is a supply-chain incident |
~/.gitconfig, ~/.git-credentials | Sometimes a plaintext token |
~/.zsh_history | Every command you have ever typed, including the ones with secrets in |
~/Downloads/ | Whatever anyone has ever sent you |
~/code/ | Every other project, including other clients' |
An agent scoped to ~ has all of it. Not "could theoretically obtain". Has, on
the first read_file call, with no further permission needed.
GATE008 reports the specific credential directories as critical.
Scopes
Gate classifies every filesystem grant into a scope, and the scope is what drives the finding:
- Name
repository- Type
- expected
- Description
Inside the repository being scanned. Not reported: this is the correct configuration.
- Name
external- Type
- high
- Description
Outside the repository but not a known credential location. Another project, a shared directory, a data folder.
- Name
home- Type
- high, escalates
- Description
The whole home directory. Escalates blast radius to at least
HIGH, and toCRITICALif the agent can also communicate externally.
- Name
root- Type
- critical
- Description
/or a drive root. Every file on the machine, including other users and system configuration.
Home-relative forms are expanded before classification, so ~, $HOME,
${HOME} and %USERPROFILE% are all recognised.
The directories that matter
Gate keeps a short list. A long list of "sensitive" directories produces a long list of ignored findings; these are the ones where an agent reaching them is almost always a mistake:
~/.ssh · ~/.aws · ~/.config/gcloud · ~/.kube · ~/.gnupg ·
~/.docker · ~/.npmrc · ~/.config · the home directory itself ·
the filesystem root · /etc, /var, C:/Windows · .env files
~/.config is on the list as a whole directory because it is a catch-all: it
holds credentials for dozens of CLIs, and granting the directory grants all of
them.
Environment files
.env deserves its own mention because it is inside the repository, so scope
analysis alone will not catch it.
An environment file is where an application keeps its secrets. Handing one to an MCP server hands that server every credential the application has, including the ones it has no use for. And it usually happens through a convenience flag:
{ "command": "bash", "args": ["-c", "source ./.env && node ./server.js"] }
Two findings in one line: an environment file and a shell wrapper.
Enumerate the variables the server needs, explicitly, so the list is reviewable:
{
"command": "node",
"args": ["./server.js"],
"env": {
"DATABASE_URL": "${env:DATABASE_URL}"
}
}
Write is not the only risk
Teams often reason that read-only filesystem access is safe. It is not, for two reasons.
Read plus egress is exfiltration. An agent that can read your keys and send a message can send your keys. It does not need write access to do damage; it needs a channel. This is the lethal trifecta.
Search is worse than read. A read tool gets what you point it at. A search
tool finds what nobody knew was there: the credential in the old branch, the
customer export in ~/Downloads, the incident doc with the password in it. It
turns "the agent can read the project" into "the agent can find anything anyone
ever left lying around".
Write access adds its own problems. In particular, write access to anything
later interpreted is execution. A tool that can write to .github/workflows/
has arranged for code to run in CI with CI's credentials.
Doing it right
- Scope to the project directory, not its parent, not your code folder, and never home or root.
- Prefer relative paths (
./docs) over absolute ones. They are portable, reviewable, and obviously inside the repository. - Mount the minimum. If the agent needs one shared directory outside the project, grant that one directory, not the tree containing it.
- Separate read from write where the server supports it. A documentation agent needs neither write nor delete.
- Never grant
.env. Enumerate the variables instead. - Check what the path expands to.
~/codelooks narrow and contains every project you have. - Scan it.
npx @usegate/cli scan
CRITICAL
GATE008 filesystem
filesystem is granted access to ~/.ssh (SSH private keys (~/.ssh)).
Contains private keys that authenticate to servers and Git hosts. An
agent with read access to this directory can be made to leak the keys
to your entire fleet.