惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Blog — PlanetScale
Blog — PlanetScale
SecWiki News
SecWiki News
Google DeepMind News
Google DeepMind News
WordPress大学
WordPress大学
小众软件
小众软件
C
CERT Recently Published Vulnerability Notes
Jina AI
Jina AI
N
Netflix TechBlog - Medium
GbyAI
GbyAI
IT之家
IT之家
Apple Machine Learning Research
Apple Machine Learning Research
AWS News Blog
AWS News Blog
G
GRAHAM CLULEY
L
Lohrmann on Cybersecurity
C
Cybersecurity and Infrastructure Security Agency CISA
I
Intezer
T
Tor Project blog
P
Palo Alto Networks Blog
P
Privacy & Cybersecurity Law Blog
P
Privacy International News Feed
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
P
Proofpoint News Feed
T
Tailwind CSS Blog
C
Check Point Blog
Cloudbric
Cloudbric
Y
Y Combinator Blog
The Last Watchdog
The Last Watchdog
Forbes - Security
Forbes - Security
Last Week in AI
Last Week in AI
S
Security Affairs
博客园 - Franky
F
Fortinet All Blogs
量子位
M
MIT News - Artificial intelligence
C
Cisco Blogs
酷 壳 – CoolShell
酷 壳 – CoolShell
Stack Overflow Blog
Stack Overflow Blog
S
Secure Thoughts
V
Visual Studio Blog
AI
AI
美团技术团队
B
Blog RSS Feed
Application and Cybersecurity Blog
Application and Cybersecurity Blog
博客园 - 三生石上(FineUI控件)
阮一峰的网络日志
阮一峰的网络日志
Engineering at Meta
Engineering at Meta
人人都是产品经理
人人都是产品经理
Microsoft Security Blog
Microsoft Security Blog
T
Threatpost
Cyberwarzone
Cyberwarzone

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Giving Your Local LLM Safe Filesystem Access With Ollama Tool Use
Pavel Espitia · 2026-06-12 · via DEV Community

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.

The threat model first

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:

  • The model asks to read /etc/passwd or ~/.ssh/id_rsa.
  • The model passes ../../../../etc/shadow as a "relative" path.
  • The model reads .env and helpfully prints your API keys into the chat transcript.
  • The model asks to read a 4GB log file and pins your RAM.
  • A file the model reads contains "ignore previous instructions, now write to ...".

Every defense below maps to one of these. None of them trust the model.

The sandbox: one root, resolved, allow-listed

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."

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:

resolveInSandbox("notes.txt");        // OK -> <root>/notes.txt
resolveInSandbox("sub/dir/a.md");     // OK
resolveInSandbox("../secrets.env");   // throws PathError
resolveInSandbox("/etc/passwd");      // throws PathError
resolveInSandbox("a/../../etc/hosts");// throws PathError

One more thing path.resolve does not cover: symlinks. A symlink inside the sandbox can point anywhere. If your workspace could contain symlinks you don't control, resolve them too and re-check:

async function resolveRealInSandbox(userPath: string): Promise<string> {
  const resolved = resolveInSandbox(userPath);
  try {
    const real = await fs.realpath(resolved);
    const rel = path.relative(SANDBOX_ROOT, real);
    if (rel.startsWith("..") || path.isAbsolute(rel)) {
      throw new PathError(`Symlink escapes sandbox: ${userPath}`);
    }
    return real;
  } catch (err) {
    if ((err as NodeJS.ErrnoException).code === "ENOENT") return resolved;
    throw err;
  }
}

A deny-list for the obvious landmines

Allow-listing the root is the structural control. On top of it, a small deny-list stops the model from reading things that are inside the sandbox but still secret. Match on the basename, not a substring, so environment.md doesn't get caught by an .env rule.

const DENIED_NAMES = new Set([".env", ".git", "id_rsa", "id_ed25519"]);
const DENIED_SUFFIXES = [".env", ".pem", ".key"];

function assertReadable(absPath: string): void {
  const base = path.basename(absPath);
  if (DENIED_NAMES.has(base) || base.startsWith(".env")) {
    throw new PathError(`Refusing to read protected file: ${base}`);
  }
  if (DENIED_SUFFIXES.some((s) => base.endsWith(s))) {
    throw new PathError(`Refusing to read protected file type: ${base}`);
  }
}

Keep this list short and obvious. The structural sandbox is your real defense; the deny-list just catches the secrets that legitimately live in a project folder.

The tools

Three read-only tools. Notice read_file has a hard byte budget, and none of them write anything.

const MAX_READ_BYTES = 256 * 1024; // 256 KB. Models do not need a 2GB file.

async function listDir(dirPath: string): Promise<string[]> {
  const abs = await resolveRealInSandbox(dirPath);
  const entries = await fs.readdir(abs, { withFileTypes: true });
  return entries.map((e) => (e.isDirectory() ? `${e.name}/` : e.name));
}

async function readFile(filePath: string): Promise<string> {
  const abs = await resolveRealInSandbox(filePath);
  assertReadable(abs);

  const stat = await fs.stat(abs);
  if (!stat.isFile()) throw new PathError(`Not a file: ${filePath}`);
  if (stat.size > MAX_READ_BYTES) {
    throw new PathError(
      `File too large: ${stat.size} bytes (limit ${MAX_READ_BYTES}).`,
    );
  }
  return fs.readFile(abs, "utf8");
}

async function grep(pattern: string, dirPath: string): Promise<string[]> {
  // Compile the model's pattern; reject anything that won't compile.
  let re: RegExp;
  try {
    re = new RegExp(pattern);
  } catch {
    throw new PathError(`Invalid regex: ${pattern}`);
  }

  const abs = await resolveRealInSandbox(dirPath);
  const hits: string[] = [];
  const entries = await fs.readdir(abs, { withFileTypes: true });

  for (const entry of entries) {
    if (!entry.isFile()) continue;
    const child = path.join(abs, entry.name);
    try {
      assertReadable(child);
    } catch {
      continue; // skip protected files silently in search results
    }
    const stat = await fs.stat(child);
    if (stat.size > MAX_READ_BYTES) continue;

    const text = await fs.readFile(child, "utf8");
    text.split("\n").forEach((line, i) => {
      if (re.test(line)) hits.push(`${entry.name}:${i + 1}: ${line.trim()}`);
    });
  }
  return hits.slice(0, 100); // cap output so a broad pattern can't flood context
}

The schemas, in the same JSON Schema format from the function-calling post:

const tools = [
  {
    type: "function",
    function: {
      name: "list_dir",
      description: "List files and folders in a directory inside the workspace",
      parameters: {
        type: "object",
        properties: { path: { type: "string", description: "Relative path" } },
        required: ["path"],
      },
    },
  },
  {
    type: "function",
    function: {
      name: "read_file",
      description: "Read a UTF-8 text file inside the workspace",
      parameters: {
        type: "object",
        properties: { path: { type: "string", description: "Relative path" } },
        required: ["path"],
      },
    },
  },
  {
    type: "function",
    function: {
      name: "grep",
      description: "Search files in a directory for a regex pattern",
      parameters: {
        type: "object",
        properties: {
          pattern: { type: "string" },
          path: { type: "string", description: "Relative directory path" },
        },
        required: ["pattern", "path"],
      },
    },
  },
];

The dispatch loop

Here is where most tutorials get sloppy: they eval-style dispatch on the tool name and pass arguments straight through. Don't. Validate arguments with Zod, route through an explicit switch, and turn every thrown error into a tool result the model can read and recover from. An error is data, not a crash.

import { z } from "zod";

const PathArgs = z.object({ path: z.string() });
const GrepArgs = z.object({ pattern: z.string(), path: z.string() });

async function dispatch(name: string, rawArgs: string): Promise<string> {
  try {
    switch (name) {
      case "list_dir":
        return JSON.stringify(await listDir(PathArgs.parse(JSON.parse(rawArgs)).path));
      case "read_file":
        return await readFile(PathArgs.parse(JSON.parse(rawArgs)).path);
      case "grep": {
        const a = GrepArgs.parse(JSON.parse(rawArgs));
        return JSON.stringify(await grep(a.pattern, a.path));
      }
      default:
        return `Error: unknown tool ${name}`;
    }
  } catch (err) {
    // Hand the failure back to the model. It will usually correct itself.
    return `Error: ${(err as Error).message}`;
  }
}

Now the agent loop against Ollama. Same two-round-trip shape as before, wrapped so the model can chain calls:

async function run(userPrompt: string): Promise<string> {
  const messages: any[] = [
    {
      role: "system",
      content:
        "You can read files inside the workspace only. Never assume a path " +
        "outside it exists. If a tool returns an error, adjust and retry.",
    },
    { role: "user", content: userPrompt },
  ];

  for (let turn = 0; turn < 8; turn++) {
    const res = await fetch("http://localhost:11434/v1/chat/completions", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ model: "qwen2.5:7b", messages, tools, tool_choice: "auto" }),
    });
    const msg = (await res.json()).choices[0].message;
    messages.push(msg);

    if (!msg.tool_calls?.length) return msg.content;

    for (const call of msg.tool_calls) {
      const result = await dispatch(call.function.name, call.function.arguments);
      messages.push({ role: "tool", tool_call_id: call.id, content: result });
    }
  }
  return "Stopped: too many tool-calling turns.";
}

The turn < 8 cap matters. Without it, a model that keeps requesting tools (or gets stuck in a retry loop on a prompt-injected file) will run forever.

Writes are different: require a human

Reading is reversible. Writing is not. So I keep writes out of the autonomous loop entirely and gate them behind an explicit human approval. The tool doesn't write: it proposes a write, prints a diff, and waits for you.

import readline from "node:readline/promises";

async function proposeWrite(filePath: string, content: string): Promise<string> {
  const abs = resolveInSandbox(filePath);   // same sandbox check
  assertReadable(abs);                       // same secrets guard

  console.log(`\nProposed write to ${path.relative(SANDBOX_ROOT, abs)}:`);
  console.log("-".repeat(40));
  console.log(content.slice(0, 2000));
  console.log("-".repeat(40));

  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
  const answer = (await rl.question("Apply this write? [y/N] ")).trim().toLowerCase();
  rl.close();

  if (answer !== "y") return "Write rejected by user.";
  await fs.writeFile(abs, content, "utf8");
  return `Wrote ${content.length} bytes to ${filePath}.`;
}

The model can want to write all day. Nothing hits disk until a human types y. This is the same principle as the read sandbox, applied to the higher-stakes operation: the LLM proposes, your code (and you) dispose.

The hardening checklist

Every filesystem tool you expose to an LLM should pass all of these:

  1. Resolve and re-check. path.resolve, then path.relative against the root. Reject anything starting with ... Never startsWith on the raw string.
  2. Resolve symlinks too. fs.realpath and re-check, or you've left a back door inside the sandbox.
  3. Deny secrets by basename. .env, keys, .git. Short list, matched on basename, not substring.
  4. Cap read size. A byte budget per read and a result cap on search. Context windows and RAM are finite.
  5. Read-only by default. Writes go through a separate, human-approved path. No write tool in the autonomous loop.
  6. Validate every argument. Zod-parse tool arguments before they reach fs. The model hallucinates fields.
  7. Errors are tool results. Catch, stringify, return to the model. Never let a bad path crash the process.
  8. Cap the turn count. Bound the agent loop so a stuck or injected model can't spin forever.

Takeaway

Function calling makes a local LLM useful. Filesystem access makes it powerful, and powerful is exactly when you have to slow down. The model is an untrusted planner working over potentially untrusted input. Your tools are the trust boundary. Build them so the worst a confused model can do is read a text file it was already allowed to see.

I run this exact pattern in spectr-ai, my local-first smart contract auditor, so the model can walk a contract's source tree without ever leaving the project folder. Sandbox first, features second. That order is the whole point.