Giving Your Local LLM Safe Filesystem Access With Ollama Tool Use A developer has created a secure filesystem access system for local LLMs using Ollama tool use, implementing path traversal protections that prevent models from reading files outside a designated sandbox directory. The system uses three tools (`list_dir`, `read_file`, `grep`) with a `resolveInSandbox` function that checks resolved absolute paths against an allowed root using `path.relative` rather than string matching, and also handles symlink traversal by resolving real paths and re-checking against the sandbox boundary. A local LLM that can read your files is genuinely useful. A local LLM that can read your files without guardrails is a path-traversal bug with a chat interface. I covered tool calling basics in an earlier post: define a tool schema, the model returns a structured request, your code decides whether to run it. That's the foundation. This post is about how to not get burned once those tools touch the filesystem. We're going to give the model three tools list dir , read file , grep , wire up the dispatch loop with Ollama, and then harden every single one so a confused or adversarial model can't read your .env , climb out of the project, or hand you back a 2GB file. The model is the planner. Your code is the executor. The executor is also the only thing standing between an unpredictable token generator and your home directory. Treat it that way. Before any code, be honest about what can go wrong. The LLM is not malicious, but it is unpredictable, and the input feeding it might be malicious a file it reads could contain instructions, a classic prompt-injection vector . So plan for all of it: /etc/passwd or ~/.ssh/id rsa . ../../../../etc/shadow as a "relative" path. .env and helpfully prints your API keys into the chat transcript.Every defense below maps to one of these. None of them trust the model. The single most important control: every path the model gives you gets resolved to an absolute path and checked against an allowed root. If it escapes the root, reject it. No exceptions, no "but it's probably fine." python import path from "node:path"; import fs from "node:fs/promises"; // The ONLY directory the model is allowed to touch. const SANDBOX ROOT = path.resolve process.env.SANDBOX ROOT ?? "./workspace" ; class PathError extends Error {} // Resolve a model-supplied path and prove it stays inside the sandbox. function resolveInSandbox userPath: string : string { // Resolve against the root, collapsing any .. segments. const resolved = path.resolve SANDBOX ROOT, userPath ; // The check that matters: is resolved actually under the root? const rel = path.relative SANDBOX ROOT, resolved ; if rel.startsWith ".." || path.isAbsolute rel { throw new PathError Path escapes sandbox: ${userPath} ; } return resolved; } Why path.relative instead of a startsWith SANDBOX ROOT string check? Because startsWith is a trap. /home/pavel/workspace-secrets starts with /home/pavel/workspace , but it's a different directory. path.relative does it structurally: if the relative path begins with .. , the target is above the root. Done. Test it before you trust it: php resolveInSandbox "notes.txt" ; // OK -