{"slug": "deep-agents-filesystem-permissions-a-beginner-friendly-guide-with-3-python", "title": "Deep Agents Filesystem Permissions: A Beginner-Friendly Guide with 3 Python Examples", "summary": "Deep Agents introduces FilesystemPermission rules that let developers control which paths an AI agent's built-in filesystem tools can access and how. A guide with three Python examples demonstrates how to set up allow, deny, and interrupt modes to restrict agent file operations, such as blocking access to .env files while allowing edits to project code.", "body_md": "An AI agent that can read and write files is only as safe as the boundaries you put around it. A coding agent might need full access to a project folder, but it should never be able to open `.env`\n\n, read a shared credentials file, or overwrite a policy document it was only supposed to consult.\n\nDeep Agents solves this with `FilesystemPermission`\n\nrules: small, declarative statements that say which paths an agent's built-in filesystem tools can touch, and how.\n\nThis guide builds three examples, each adding one new idea:\n\nEach example is a complete, runnable script. If you want to follow along, you'll need `deepagents`\n\ninstalled and a chat model configured — the full scripts (linked at the end) include that setup so you can just run them.\n\nPermission rules need `deepagents>=0.5.2`\n\n. If you want to use `mode=\"interrupt\"`\n\n(Example 2), you need `deepagents>=0.6.8`\n\n. If your examples don't behave as described below, check your installed version first.\n\nA rule has three parts:\n\n```\nFilesystemPermission(\n    operations=[\"read\", \"write\"],\n    paths=[\"/workspace/**\"],\n    mode=\"allow\",\n)\n```\n\n** operations** — which kind of filesystem call the rule watches:\n\n`\"read\"`\n\ncovers `ls`\n\n, `read_file`\n\n, `glob`\n\n, `grep`\n\n`\"write\"`\n\ncovers `write_file`\n\n, `edit_file`\n\n, `delete`\n\n** paths** — glob patterns matched against the agent's\n\n`**`\n\nmatches any depth (`/workspace/**`\n\nmatches `/workspace/app.py`\n\nand `/workspace/sub/dir/file.txt`\n\n). You can also use `{a,b}`\n\nto match alternatives, e.g. `/workspace/{src,tests}/**`\n\n.** mode** — what happens when a call matches:\n\n`\"allow\"`\n\n— let it through`\"deny\"`\n\n— reject it`\"interrupt\"`\n\n— pause the whole agent graph and wait for a human to approve, edit, or reject the callThree rules govern how a *list* of `FilesystemPermission`\n\nobjects behaves, and all three matter more than any single rule on its own:\n\n`deny`\n\nrule on `/**`\n\n.`ls`\n\n, `read_file`\n\n, `glob`\n\n, `grep`\n\n, `write_file`\n\n, `edit_file`\n\n, `delete`\n\n). They do `execute`\n\ntool — that's a separate concern, covered at the end of this guide.With that model in place, the examples below should read less like magic incantations and more like straightforward consequences of these three rules.\n\n**Goal:** the agent can freely read and edit files in `/workspace`\n\n, except a `.env`\n\nfile sitting inside it.\n\nThe script maps a real folder to the agent's virtual root using `FilesystemBackend(root_dir=..., virtual_mode=True)`\n\n. Concretely, if `root_dir`\n\nis `secure_workspace_data/`\n\n, then the real file `secure_workspace_data/workspace/app.py`\n\nappears to the agent simply as `/workspace/app.py`\n\n. The agent never sees your actual disk paths — only the virtual ones you've exposed.\n\n``` python\nimport os\nfrom pathlib import Path\n\nfrom dotenv import load_dotenv\nfrom langchain.chat_models import init_chat_model\nfrom deepagents import FilesystemPermission, create_deep_agent\nfrom deepagents.backends import FilesystemBackend\n\nload_dotenv()\n\nROOT = Path(__file__).parent / \"secure_workspace_data\"\nWORKSPACE = ROOT / \"workspace\"\nWORKSPACE.mkdir(parents=True, exist_ok=True)\n(WORKSPACE / \"app.py\").write_text(\"print('demo application')\\n\", encoding=\"utf-8\")\n(WORKSPACE / \".env\").write_text(\"DEMO_SECRET=do-not-read\\n\", encoding=\"utf-8\")\n\nmodel = init_chat_model(\"nvidia:meta/llama-3.1-70b-instruct\")\nbackend = FilesystemBackend(root_dir=ROOT, virtual_mode=True)\n\npermissions = [\n    # Rule 1: the exception. Checked first, so it wins before the broad allow below.\n    FilesystemPermission(\n        operations=[\"read\", \"write\"],\n        paths=[\"/workspace/.env\", \"/workspace/secrets/**\"],\n        mode=\"deny\",\n    ),\n    # Rule 2: the general case. Everything else in the workspace is fair game.\n    FilesystemPermission(\n        operations=[\"read\", \"write\"],\n        paths=[\"/workspace/**\"],\n        mode=\"allow\",\n    ),\n    # Rule 3: the catch-all. Anything outside /workspace is blocked by default.\n    FilesystemPermission(\n        operations=[\"read\", \"write\"],\n        paths=[\"/**\"],\n        mode=\"deny\",\n    ),\n]\n\nagent = create_deep_agent(model=model, backend=backend, permissions=permissions)\nresult = agent.invoke(\n    {\n        \"messages\": [\n            {\n                \"role\": \"user\",\n                \"content\": \"Read /workspace/.env.\",\n            }\n        ]\n    }\n)\nprint(result[\"messages\"][-1].content)\n\nresult = agent.invoke(\n    {\n        \"messages\": [\n            {\n                \"role\": \"user\",\n                \"content\": \"Read /workspace/app.py.\",\n            }\n        ]\n    }\n)\nprint(result[\"messages\"][-1].content)\n```\n\nthis will generate output something like:\n\n```\nI cannot read or list the contents of /workspace or /workspace/.env due to a permission error.\nThe file `/workspace/app.py` contains only one line of code: `print('demo application')`.\n```\n\n**Now lets see why the order matters here:** `/workspace/.env`\n\nmatches *both* Rule 1 and Rule 2. Because Rule 1 is checked first, `.env`\n\nis denied before Rule 2's broad allow ever gets a chance to apply. Swap the two, and the allow rule would win instead — the deny rule would still exist in your code, but it would never fire, because a match was already found above it.\n\n| Agent action | Result | Which rule decided it |\n|---|---|---|\nRead/edit `/workspace/app.py`\n|\nAllowed | Rule 2 |\nRead/edit `/workspace/.env`\n|\nDenied | Rule 1 |\nRead `/anything-else.txt`\n|\nDenied | Rule 3 (nothing else matched) |\n\n**Goal:** the agent can read a shared policy document but never edit it, and any write under `/secrets`\n\npauses for a human to approve.\n\nThis introduces `mode=\"interrupt\"`\n\n, which behaves differently from `allow`\n\n/`deny`\n\n: instead of resolving the tool call immediately, it **freezes the agent's execution graph** at that point and returns control to your application. Your application decides what happens next — approve the call as-is, edit its arguments, or reject it — and then resumes the same run.\n\nBecause the graph has to be paused and later resumed, it needs somewhere to store its state in between. That's what the `checkpointer`\n\nand `thread_id`\n\nare for: without a checkpointer, there's no state to resume *from*, so `mode=\"interrupt\"`\n\nrequires one.\n\n```\n\"\"\"Example 2: read-only memory and human approval for sensitive writes.\"\"\"\n\nimport os\nfrom pathlib import Path\n\nfrom dotenv import load_dotenv\nfrom langchain.chat_models import init_chat_model\nfrom langgraph.checkpoint.memory import InMemorySaver\nfrom langgraph.types import Command\nfrom deepagents import FilesystemPermission, create_deep_agent\nfrom deepagents.backends import FilesystemBackend\n\nload_dotenv()\n\nif not os.environ.get(\"NVIDIA_API_KEY\", \"\").startswith(\"nvapi-\"):\n    raise RuntimeError(\n        \"Set NVIDIA_API_KEY to a valid NVIDIA API key (starting with 'nvapi-') \"\n        \"in your environment or a .env file before running this example.\"\n    )\n\nROOT = Path(__file__).parent / \"approval_memory_data\"\n# Prepare separate demo locations so each permission rule has a real path to\n# test: normal work, shared memory, and sensitive secrets.\nfor directory in (\"workspace\", \"memories\", \"secrets\"):\n    (ROOT / directory).mkdir(parents=True, exist_ok=True)\n(ROOT / \"workspace\" / \"report.md\").write_text(\"# Draft report\\n\", encoding=\"utf-8\")\n(ROOT / \"memories\" / \"company-policy.md\").write_text(\n    \"Never commit credentials to source control.\\n\", encoding=\"utf-8\"\n)\n\n# This creates the following demo structure on disk. The secrets directory is\n# intentionally empty; the agent will request approval before writing there.\n# approval_memory_data/\n# |-- workspace/\n# |   `-- report.md\n# |-- memories/\n# |   `-- company-policy.md\n# `-- secrets/\n\n# This model supports one tool call per response, so disable parallel tool\n# calls when the agent has several filesystem operations to perform.\nmodel = init_chat_model(\n    \"nvidia:meta/llama-3.1-70b-instruct\",\n    model_kwargs={\"parallel_tool_calls\": False},\n)\n# Agent path /memories/company-policy.md maps to ROOT/memories/company-policy.md.\nbackend = FilesystemBackend(root_dir=ROOT, virtual_mode=True)\n# An interrupt pauses the graph; the checkpointer stores its state while a\n# human decides whether the pending filesystem operation should continue.\ncheckpointer = InMemorySaver()\n\npermissions = [\n    # Memory is readable but cannot be changed by the agent.\n    FilesystemPermission(\n        operations=[\"write\"],\n        paths=[\"/memories/**\", \"/policies/**\"],\n        mode=\"deny\",\n    ),\n    # Reading policy is allowed separately from writing it.\n    FilesystemPermission(\n        operations=[\"read\"],\n        paths=[\"/memories/**\", \"/policies/**\"],\n        mode=\"allow\",\n    ),\n    # The graph pauses here and must be resumed with a human decision.\n    FilesystemPermission(\n        operations=[\"write\"],\n        paths=[\"/secrets/**\"],\n        mode=\"interrupt\",\n    ),\n    # Normal workspace changes do not require approval.\n    FilesystemPermission(\n        operations=[\"read\", \"write\"],\n        paths=[\"/workspace/**\"],\n        mode=\"allow\",\n    ),\n    FilesystemPermission(\n        operations=[\"read\", \"write\"],\n        paths=[\"/**\"],\n        mode=\"deny\",\n    ),\n]\n\nagent = create_deep_agent(\n    model=model,\n    backend=backend,\n    permissions=permissions,\n    checkpointer=checkpointer,\n)\n\nconfig = {\"configurable\": {\"thread_id\": \"approval-memory-demo\"}}\nresult = agent.invoke(\n    {\n        \"messages\": [\n            {\n                \"role\": \"user\",\n                \"content\": (\n                    \"Read /memories/company-policy.md, then write a short note to \"\n                    \"/secrets/review.txt saying that the policy was reviewed. \"\n                    \"The secret write requires human approval.\"\n                ),\n            }\n        ]\n    },\n    config=config,\n)\n\nwhile True:\n    interrupts = result.get(\"__interrupt__\", ())\n    if not interrupts:\n        print(result[\"messages\"][-1].content)\n        break\n\n    # Filesystem permission interrupts use the same action_requests structure\n    # as other LangGraph human-in-the-loop tool interrupts.\n    interrupt_value = interrupts[0].value\n    action_requests = interrupt_value[\"action_requests\"]\n    decisions = []\n\n    for action in action_requests:\n        print(\"\\nApproval required\")\n        print(f\"Tool: {action['name']}\")\n        print(f\"Arguments: {action['args']}\")\n\n        while True:\n            decision = input(\"Approve this operation? [a]pprove/[r]eject: \").strip().lower()\n            if decision in {\"a\", \"approve\"}:\n                decisions.append({\"type\": \"approve\"})\n                break\n            if decision in {\"r\", \"reject\"}:\n                decisions.append(\n                    {\n                        \"type\": \"reject\",\n                        \"message\": \"The user rejected this filesystem operation. Do not retry it.\",\n                    }\n                )\n                break\n            print(\"Please enter 'a' to approve or 'r' to reject.\")\n\n    # Resume with the same config so InMemorySaver can restore this thread.\n    result = agent.invoke(Command(resume={\"decisions\": decisions}), config=config)\n```\n\nA couple of details that are easy to miss:\n\n`deny`\n\nand `interrupt`\n\nmean different things.`deny`\n\nis for actions that should `interrupt`\n\nis for actions that are legitimate but need a person in the loop before they happen. Mixing them up either blocks something you actually wanted to allow-with-review, or lets something through that should have been reviewed.`delete`\n\non a folder, permissions are checked against `/secrets/**`\n\n, not a wildcard-first pattern like `/**/secrets`\n\n. Bulk operations (`ls`\n\n, `glob`\n\n, `grep`\n\n, or `delete`\n\non a directory) will trigger the interrupt any time their search **Goal:** a parent agent can edit code inside a project. It delegates review work to an `auditor`\n\nsubagent that must never write anything, only read.\n\nThis example uses a `CompositeBackend`\n\ninstead of a plain `FilesystemBackend`\n\n. A composite backend lets you route different virtual path prefixes to different underlying storage — here, everything under `/workspace/`\n\nis mapped to a real project folder on disk, while anything else falls back to `StateBackend()`\n\n(in-memory, scratch state that isn't persisted to disk).\n\n``` python\nimport os\nfrom pathlib import Path\n\nfrom dotenv import load_dotenv\nfrom langchain.chat_models import init_chat_model\nfrom deepagents import FilesystemPermission, create_deep_agent\nfrom deepagents.backends import CompositeBackend, FilesystemBackend, StateBackend\n\nload_dotenv()\n\nROOT = Path(__file__).parent / \"multi_agent_data\"\nPROJECT = ROOT / \"project\"\n(PROJECT / \"src\").mkdir(parents=True, exist_ok=True)\n(PROJECT / \"src\" / \"app.py\").write_text(\n    \"def greet(name):\\n    return f'Hello, {name}'\\n\", encoding=\"utf-8\"\n)\n\nmodel = init_chat_model(\"nvidia:meta/llama-3.1-70b-instruct\")\nbackend = CompositeBackend(\n    default=StateBackend(),\n    routes={\n        \"/workspace/\": FilesystemBackend(\n            root_dir=PROJECT,\n            virtual_mode=True,\n        ),\n    },\n)\n\nparent_permissions = [\n    FilesystemPermission(\n        operations=[\"read\", \"write\"],\n        paths=[\"/workspace/**\"],\n        mode=\"allow\",\n    ),\n    FilesystemPermission(\n        operations=[\"read\", \"write\"],\n        paths=[\"/**\"],\n        mode=\"deny\",\n    ),\n]\n\nauditor_permissions = [\n    # Nothing this subagent does can write anywhere.\n    FilesystemPermission(\n        operations=[\"write\"],\n        paths=[\"/**\"],\n        mode=\"deny\",\n    ),\n    # It may only read inside the project workspace.\n    FilesystemPermission(\n        operations=[\"read\"],\n        paths=[\"/workspace/**\"],\n        mode=\"allow\",\n    ),\n    FilesystemPermission(\n        operations=[\"read\"],\n        paths=[\"/**\"],\n        mode=\"deny\",\n    ),\n]\n\nagent = create_deep_agent(\n    model=model,\n    backend=backend,\n    permissions=parent_permissions,\n    subagents=[\n        {\n            \"name\": \"auditor\",\n            \"description\": \"Reviews workspace code but must never edit it.\",\n            \"system_prompt\": \"Review code and report issues. Never modify files.\",\n            \"permissions\": auditor_permissions,\n        }\n    ],\n)\n\nresult = agent.invoke(\n    {\n        \"messages\": [\n            {\n                \"role\": \"user\",\n                \"content\": \"Ask the auditor to inspect /workspace/src/app.py.\",\n            }\n        ]\n    },\n    config={\"configurable\": {\"thread_id\": \"multi-agent-auditor-demo\"}},\n)\nprint(result[\"messages\"][-1].content)\n```\n\nThe single most important thing to understand here: ** permissions on a subagent spec replaces the parent's rules, it does not add to them.** A subagent doesn't inherit a stricter version of its parent's policy by default — if you give it\n\n`permissions`\n\nat all, that list is now the entire policy for that subagent, evaluated in isolation.That's exactly why `auditor_permissions`\n\nis a complete three-rule list on its own — deny all writes, allow reads inside `/workspace`\n\n, deny all other reads — rather than just the single write-deny rule. Remember rule #2 from earlier: unmatched calls are allowed by default. If the auditor only had the write-deny rule, it would correctly be blocked from writing, but it would also be free to *read* absolutely anything, since nothing would deny it.\n\n**One more thing worth knowing if you reach for CompositeBackend with a sandbox:** if the backend's\n\n`default`\n\nroute is a sandbox (one that can execute arbitrary shell commands), every permission path you declare must be scoped under one of the composite's named route prefixes — you can't write a permission like `/**`\n\nor `/workspace/**`\n\nthat reaches into the sandbox's default route, and doing so raises `NotImplementedError`\n\n. This example avoids that because its default is `StateBackend()`\n\n, not a sandbox — but it's the first thing to check if you swap in a sandbox default and your permissions suddenly stop working.It's worth restating plainly: `FilesystemPermission`\n\nrules only govern Deep Agents' own built-in filesystem tools. They say nothing about:\n\n`execute`\n\nIf your agent can run shell commands, filesystem permission globs cannot contain that — a shell command can read or write anything the process itself has access to, permission rules or not. That's a separate problem, solved with an actual sandboxed execution environment, not with path patterns.\n\n`/workspace/**`\n\n, rather than trying to enumerate everything to deny.`deny`\n\non `/**`\n\n— remember, no match means allowed.`interrupt`\n\n(with a checkpointer) when a human should sign off on an action, and `deny`\n\nwhen an action should never happen at all.`CompositeBackend`\n\nwith a sandbox as the default route, scope every permission path to a named route prefix.The full runnable versions are available in this project as [secure_workspace_agent.py](https://secure_workspace_agent.py), [approval_memory_agent.py](https://approval_memory_agent.py), and [multi_agent_auditor.py](https://multi_agent_auditor.py).\n\nFor the current API reference and additional examples, see the [Deep Agents permissions documentation](https://docs.langchain.com/oss/python/deepagents/permissions).", "url": "https://wpnews.pro/news/deep-agents-filesystem-permissions-a-beginner-friendly-guide-with-3-python", "canonical_source": "https://dev.to/syeedmdtalha/deep-agents-filesystem-permissions-a-beginner-friendly-guide-with-3-python-examples-5hd8", "published_at": "2026-07-28 10:20:55+00:00", "updated_at": "2026-07-28 10:36:06.932186+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-safety"], "entities": ["Deep Agents"], "alternates": {"html": "https://wpnews.pro/news/deep-agents-filesystem-permissions-a-beginner-friendly-guide-with-3-python", "markdown": "https://wpnews.pro/news/deep-agents-filesystem-permissions-a-beginner-friendly-guide-with-3-python.md", "text": "https://wpnews.pro/news/deep-agents-filesystem-permissions-a-beginner-friendly-guide-with-3-python.txt", "jsonld": "https://wpnews.pro/news/deep-agents-filesystem-permissions-a-beginner-friendly-guide-with-3-python.jsonld"}}