When Claude starts asking for *read* or *write* permissions on your hard drive, the first thing you should feel is a spike of anxiety. A rogue request can leak secrets, overwrite logs, or even trigger ransomware‑style deletes before you see a warning. In my last production rollout, an un‑sandboxed file‑read tool let the agent crawl the entire `/home` tree, blowing up our storage bill and exposing user‑data snapshots. The fix? Treat the agent as a **tool**, not a user, and wrap every filesystem call in a hardened permission layer that only executes after a real‑time human says “yes”.

⚡ TL;DR — Key takeaways
  • Never give Claude unrestricted filesystem access; enforce a sandbox.
  • Define explicit JSON‑schema‑driven tools (read, list, write) that stay inside a base directory.
  • Use a PyQt6 GUI to intercept each tool request and ask the user for consent.
  • Canonicalize paths, block symlinks, and reject directory‑traversal attempts.
  • Keep a per‑session ACL manager and audit log for revocation and forensic analysis.

Before you start: Python 3.11+, Anthropic Python SDK ≥ 1.0, PyQt6 ≥ 6.5, a valid Anthropic API key, and a dedicated sandbox folder (e.g. `~/claude_sandbox`). Install dependencies with pip install anthropic[tool_use] pyqt6 watchdog. You’ll also need asyncio‑compatible networking (Python’s built‑in event loop works fine).

Secure File System Access for Claude Agents

Secure file system access for Claude agents needs a sandboxed architecture. Use the Anthropic SDK’s Tool Use to create a Python permission layer, exposing only safe functions such as `read_file` that stay inside a predefined directory. A PyQt GUI then intercepts every request, prompting real‑time user approval before execution, enforcing least‑privilege security.

Why Sandboxed File Access is Non‑Negotiable for Claude Agents

The Risk of Unrestricted Agent Filesystem Access

Claude can generate perfectly valid shell‑style paths. If those paths resolve outside a controlled area, an attacker could harvest configuration files, API keys, or even delete critical binaries. In 2026, the “AI‑Pwn” report showed that 73 % of breaches involving AI agents originated from sloppy file permissions, not from model exploits.

Core Security Principles: Least Privilege & Explicit Consent

We apply the classic “least privilege” rule: the agent only gets the *exact* operations it needs, and only on the *exact* files the user approved. Consent is always a human‑in‑the‑loop (HITL) step, mirroring the workflow described in my [HITL Approval Workflows for ADK Agents (2026 Guide)](https://nileshblog.tech/hitl-approval-adk-agents/).

Architectural Goal: Agent‑as‑Tool, Not Agent‑as‑User

Think of Claude as a microservice that calls your own API to read or write files. It never touches the OS directly; the sandbox layer mediates every call, validates the path, and returns a structured JSON response or an error you can log.

Architecting the Secure Interface: Python Stack & Anthropic SDK

2026 Tool Stack Breakdown

LayerTechnologyReason
Agent runtime**Anthropic SDK (v1.0+)** – `client.messages.create` with `tools`Native tool‑use support, streaming, and JSON schema validation
Permission layer**Python 3.11+** – `pathlib`, `os`, `asyncio`Simple, async‑first, built‑in OS path handling
GUI**PyQt6** (Qt 6.5) – `QMainWindow`, `QFileDialog`, `QThreadPool`Native desktop dialogs, non‑blocking event loop
Watchdog**watchdog** – `Observer`Optional live‑view of sandbox changes for audit
Logging**logging** – JSON formatterStructured logs for SIEM integration

Defining the Agent’s “Toolbox”: JSON Schema for File Operations

Claude expects tools to be described in JSON schema. Here’s a minimal yet complete definition:

{
  "type": "function",
  "name": "read_file",
  "description": "Read a plain‑text file from the sandbox.",
  "parameters": {
    "type": "object",
    "properties": {
      "path": {
        "type": "string",
        "description": "Relative path inside the sandbox (e.g., \"docs/report.txt\")"
      }
    },
    "required": ["path"],
    "additionalProperties": false
  }
}

We’ll ship three tools: `read_file`, `list_dir`, `write_file`. Each tool returns a JSON payload so Claude can continue reasoning.

State Management for Session Isolation and Permission Revocation

A singleton `SessionManager` holds a per‑session ACL dictionary:

session_acls: Dict[str, Set[Path]]  # session_id → allowed absolute paths

When the user grants a request, the absolute path is stored; future identical requests are auto‑approved, avoiding repetitive dialogs. Revocation clears the set and forces a new prompt.

Step‑by‑Step: Building the File System Permission Layer

Implementing the Base Sandbox Directory & ACL Manager

# sandbox_manager.py – v1.0 (2026)
import os
import logging
from pathlib import Path
from threading import RLock
from typing import Set, Dict

BASE_SANDBOX = Path.home() / "claude_sandbox"
BASE_SANDBOX.mkdir(parents=True, exist_ok=True)

log = logging.getLogger(__name__)
log.setLevel(logging.INFO)

class ACLManager:
    """Thread‑safe manager for per‑session allowed paths."""
    def __init__(self):
        self._acls: Dict[str, Set[Path]] = {}
        self._lock = RLock()

    def grant(self, session_id: str, path: Path) -> None:
        with self._lock:
            self._acls.setdefault(session_id, set()).add(path.resolve())
            log.info("Granted %s for session %s", path, session_id)

    def revoke(self, session_id: str) -> None:
        with self._lock:
            self._acls.pop(session_id, None)
            log.info("Revoked all permissions for session %s", session_id)

    def is_allowed(self, session_id: str, path: Path) -> bool:
        """Check if *absolute* path is within allowed set and inside the sandbox."""
        try:
            resolved = path.resolve()
        except Exception:
            return False
        if not resolved.is_relative_to(BASE_SANDBOX):
            return False
        with self._lock:
            return resolved in self._acls.get(session_id, set())

Coding the Tool Functions: `read_file`, `list_dir`, `write_file` (with Validation)

# tools.py – v1.0 (2026)
import json
from pathlib import Path
from typing import Any, Dict
from sandbox_manager import BASE_SANDBOX, ACLManager

acl = ACLManager()

def _sanitize(rel_path: str) -> Path:
    """Convert user‑provided relative path into an absolute, safe Path."""
    # Reject absolute paths outright
    if Path(rel_path).is_absolute():
        raise ValueError("Absolute paths are not allowed")
    # Resolve symlinks and normalize '..' segments
    candidate = (BASE_SANDBOX / rel_path).resolve()
    if not candidate.is_relative_to(BASE_SANDBOX):
        raise ValueError("Path traversal outside sandbox detected")
    return candidate

def read_file(session_id: str, params: Dict[str, Any]) -> Dict[str, Any]:
    try:
        target = _sanitize(params["path"])
        if not acl.is_allowed(session_id, target):
            raise PermissionError("Access not approved by user")
        content = target.read_text(encoding="utf-8")
        return {"status": "ok", "content": content}
    except Exception as exc:
        return {"status": "error", "message": str(exc)}

def list_dir(session_id: str, params: Dict[str, Any]) -> Dict[str, Any]:
    try:
        target = _sanitize(params.get("path", "."))
        if not acl.is_allowed(session_id, target):
            raise PermissionError("Access not approved by user")
        entries = [p.name for p in target.iterdir()]
        return {"status": "ok", "entries": entries}
    except Exception as exc:
        return {"status": "error", "message": str(exc)}

def write_file(session_id: str, params: Dict[str, Any]) -> Dict[str, Any]:
    try:
        target = _sanitize(params["path"])
        data = params.get("content", "")
        if not acl.is_allowed(session_id, target):
            raise PermissionError("Access not approved by user")
        target.write_text(data, encoding="utf-8")
        return {"status": "ok"}
    except Exception as exc:
        return {"status": "error", "message": str(exc)}

Injecting the Sandboxed Tool Set into Claude’s Agent Runtime

# agent_runtime.py – v1.0 (2026)
import asyncio
import json
import logging
from anthropic import Anthropic, AsyncMessage
from tools import read_file, list_dir, write_file
from sandbox_manager import ACLManager, BASE_SANDBOX

client = Anthropic(api_key="YOUR_ANTHROPIC_API_KEY")
log = logging.getLogger(__name__)

TOOLS = [
    {
        "type": "function",
        "name": "read_file",
        "description": "Read a file from the sandbox.",
        "parameters": {
            "type": "object",
            "properties": {"path": {"type": "string"}},
            "required": ["path"],
            "additionalProperties": False,
        },
    },
    {
        "type": "function",
        "name": "list_dir",
        "description": "List entries in a sandbox directory.",
        "parameters": {
            "type": "object",
            "properties": {"path": {"type": "string"}},
            "required": [],
            "additionalProperties": False,
        },
    },
    {
        "type": "function",
        "name": "write_file",
        "description": "Write a string to a sandbox file.",
        "parameters": {
            "type": "object",
            "properties": {
                "path": {"type": "string"},
                "content": {"type": "string"},
            },
            "required": ["path", "content"],
            "additionalProperties": False,
        },
    },
]

async def handle_tool_call(session_id: str, tool_name: str, arguments: str) -> str:
    """Dispatch to the corresponding sandbox tool after user approval."""
    args = json.loads(arguments)
    # The GUI will push a request onto an asyncio.Queue (see gui.py)
    from gui import permission_queue
    await permission_queue.put((session_id, tool_name, args))
    # Block until GUI sends back a response
    response = await permission_queue.get()
    # response = {"approved": bool, "message": str}
    if not response["approved"]:
        return json.dumps({"status": "error", "message": "User denied permission"})
    # Grant the path for this session so future calls are auto‑approved
    ACLManager().grant(session_id, Path(BASE_SANDBOX / args["path"]).resolve())
    # Call the actual implementation
    result = globals()[tool_name](session_id, args)
    return json.dumps(result)

async def main_loop():
    session_id = "session-001"
    messages = [
        {"role": "user", "content": "Please read the file `notes/todo.txt`."}
    ]
    async with client.messages.stream(
        model="claude-3-5-sonnet-202406",
        max_tokens=1024,
        temperature=0,
        system="You are a helpful assistant that can only read/write files via the provided tools.",
        messages=messages,
        tools=TOOLS,
    ) as stream:
        async for event in stream:
            if event.type == "tool_use":
                tool_res = await handle_tool_call(session_id, event.name, event.input)
                # Push tool result back to the model
                await stream.send_tool_result(event.id, tool_res)
            elif event.type == "message_done":
                log.info("Assistant finished: %s", event.content[0].text)
                break

if __name__ == "__main__":
    asyncio.run(main_loop())

Building the Guardian GUI: Real‑Time Permission Override & Monitoring

GUI Framework Choice: PyQt for Robust Desktop Control (2026)

PyQt6 gives us native file‑chooser dialogs, non‑blocking `QThreadPool`, and an event‑driven model that meshes nicely with `asyncio` via `QEventLoop`. While Tkinter would also work, PyQt’s signals/slots let us cleanly separate UI from the agent thread.

Designing the Interactive Permission Dialog & File Explorer View

The dialog shows three pieces of information:

  1. **Tool name** (read, list, write)
  2. **Requested path** (relative, highlighted)
  3. **Optional preview** (first 200 bytes for reads)

Two buttons – *Approve* and *Deny* – send a structured response back through an `asyncio.Queue`.

Implementing the Event Loop: Linking GUI Actions to Agent Tool Execution

# gui.py – v1.0 (2026)
import asyncio
import json
import sys
from pathlib import Path

from PyQt6.QtWidgets import (
    QApplication,
    QDialog,
    QLabel,
    QPushButton,
    QVBoxLayout,
    QTextEdit,
    QFileDialog,
)
from Py
Written by

’m Nilesh, a Software Development Engineer with 2+ years of experience, specializing in Go, JavaScript, Python, Docker, Kubernetes, Git, Jenkins, microservices, and system design (LLD/HLD), backed by a strong foundation in data structures and algorithms. Alongside my engineering journey, I bring 4+ years of hands-on experience in SEO, where I’ve worked extensively on content strategy, keyword research, technical SEO, and organic growth, helping products and businesses scale efficiently by aligning solid technology with search-driven performance.