PromptABCD
FeaturesLearnHow it worksUse casesFAQGuideBlogContext Blocks
Sign inGet started free
Sign inSign up
PromptABCD

A calm home for your best AI prompts. Save them once, find them in seconds, reuse them forever.

Product

  • Features
  • Chrome Extension
  • Free Courses
  • How it works
  • Use cases
  • Blog
  • Context Blocks
  • Export Anywhere
  • FAQ

Resources

  • User guide
  • Learn prompting
  • Sign in
  • Get started free

© 2026 PromptABCD. All rights reserved.

Privacy PolicyTerms and Conditions
Home/Blog/AI Harness/File System Access in an Agent Harness: A Case Study
AI Harness

File System Access in an Agent Harness: A Case Study

Safe agent file system access means never trusting a path you didn't resolve. A docs team's case study on the os.path.join trap and the jail that fixed it.

August 28, 2026·8 min read
ShareShare
⚡Featured Prompt— copy and use right now
import os

ROOT = "/home/app/docs"

def read_file(path):
    full = os.path.join(ROOT, path)
    return open(full).read()

def write_file(path, content):
    full = os.path.join(ROOT, path)
    with open(full, "w") as f:
        f.write(content)
    return "written"

How do you let an agent read and write files without letting it wander into

/etc/passwd
or overwrite something three directories up? That's the question a documentation-tooling team asked me after their agent, told to "update the README," somehow modified a file outside the project entirely. Nobody wrote malicious code. The bug hid in one line of path handling that looked completely innocent. Agent file system access is where a lot of harnesses quietly leak, and this case study walks through exactly how — and how the team closed it.

The Problem a Docs Team Faced

A four-person developer-experience team built an agent to maintain their documentation. Give it a task like "add a section on rate limits to the API guide," and it would read the relevant file, edit it, and write it back. The tools were simple:

read_file(path)
and
write_file(path, content)
.

It worked until a task phrased a path with a leading slash. The agent, reasoning about "the config at /config/limits.md," passed an absolute-looking path — and the write landed somewhere nobody intended. The file that got modified wasn't even in the repository. The team's first reaction was to blame the model for "using the wrong path." But the model's path wasn't the bug. The harness was, because it trusted the path without ever checking where it actually pointed.

The Wrong Approach

Here's the original file access code. Read it closely, because the flaw is famous and almost invisible:

hljs python
[object Object], os

ROOT = ,[object Object],

,[object Object], ,[object Object],(,[object Object],):
    full = os.path.join(ROOT, path)
    ,[object Object], ,[object Object],(full).read()

,[object Object], ,[object Object],(,[object Object],):
    full = os.path.join(ROOT, path)
    ,[object Object], ,[object Object],(full, ,[object Object],) ,[object Object], f:
        f.write(content)
    ,[object Object], ,[object Object],

What this does: it joins the requested path onto a root directory and reads or writes there. It looks jailed — everything goes "under"

ROOT
, right? Not quite.
os.path.join("/home/app/docs", "/etc/passwd")
returns
/etc/passwd
. When the second argument is absolute,
os.path.join
throws the root away entirely. The jail has a hole you can drive an absolute path straight through.

And even relative paths escape:

os.path.join(ROOT, "../../../etc/passwd")
resolves happily upward, out of the docs directory and into the system. The naive join enforces nothing.

⚠️ Common mistake: Assuming

os.path.join(root, user_path)
confines the result to
root
. It does not. An absolute
user_path
silently discards the root, and
..
segments climb out of it. This single misconception is behind a large share of agent file-access escapes, and it's invisible in every test that only ever passes clean relative paths.

The Correct Approach: Jailing Agent File System Access

The fix is to resolve the final path and verify it still lives under the root before touching disk:

hljs python
[object Object], os

ROOT = os.path.realpath(,[object Object],)

,[object Object], ,[object Object],(,[object Object],):
    ,[object Object],
    candidate = os.path.realpath(os.path.join(ROOT, path.lstrip(,[object Object],)))
    ,[object Object], ,[object Object], (candidate == ROOT ,[object Object], candidate.startswith(ROOT + os.sep)):
        ,[object Object], PermissionError(,[object Object],)
    ,[object Object], candidate

,[object Object], ,[object Object],(,[object Object],):
    ,[object Object], ,[object Object],(resolve_in_jail(path)).read()[:,[object Object],]

,[object Object], ,[object Object],(,[object Object],):
    full = resolve_in_jail(path)
    os.makedirs(os.path.dirname(full), exist_ok=,[object Object],)
    ,[object Object], ,[object Object],(full, ,[object Object],) ,[object Object], f:
        f.write(content)
    ,[object Object], ,[object Object],

What this does: it strips leading slashes so absolute paths can't blow away the root, resolves the real path (following any

..
and symlinks), and refuses anything that doesn't land inside the jail. The
realpath
call is the important part — it collapses
..
segments and resolves symlinks, so both escape routes close at once. A blocked path raises a clear error the model can read and correct.

⚡ Pro tip: The

startswith(ROOT + os.sep)
check needs that trailing separator. Without it, a sibling directory like
/home/app/docs-secret
would pass the check because its name starts with
/home/app/docs
. It's a one-character bug that reopens the jail — add the separator and confirm it with a test that a sibling path gets rejected.

Results and What Changed

After the change, the escape simply stopped being possible. The team wrote a handful of adversarial tests — absolute paths,

..
chains, a symlink pointing outside the jail — and watched each one raise cleanly instead of touching a forbidden file. The model still occasionally produced an odd path, but now an odd path became a readable error and a retry, not a write to the wrong place.

The subtler win was confidence. Before, the team hesitated to give the agent write access at all, because they couldn't prove it was contained. After, they could point to the jail check and the adversarial tests and say "here's the boundary, here's the proof it holds." That proof is what let them actually turn the agent on for real repositories instead of keeping it in a sandbox nobody trusted.

The debugging story changed too. With the old code, an escaped write left no obvious trace — a file changed somewhere outside the project, discovered days later, with no clue which run did it. With the jail in place, every escape attempt logs a

PermissionError
naming the exact path the model tried to reach. Those logs turned into a useful signal: the team noticed the model kept trying to read a shared config one level above the jail, which told them the task framing was pushing it outside the intended scope. They fixed the prompt, not just the jail. A good boundary doesn't only stop escapes — it tells you when your instructions are quietly asking the agent to go somewhere it shouldn't.

⚡ Pro tip: Handle symlink races explicitly if your jail holds anything sensitive. Between the

realpath
check and the
open
, a symlink could in theory be swapped — a time-of-check-to-time-of-use gap. For most docs tooling it's a non-issue, but for anything security-critical, open with
O_NOFOLLOW
on the final component so a last-moment symlink swap fails instead of following.

Separating Read Access From Write Access

Jailing paths solves where the agent can go. The next question is what it can do once it's there, and the docs team learned to split those apart. Most agent tasks read widely and write narrowly — an agent updating one file might need to read ten for context. Granting write access to everything it can read is a needless expansion of blast radius.

hljs python
READ_ROOT = os.path.realpath(,[object Object],)
WRITE_ROOT = os.path.realpath(,[object Object],)   ,[object Object],

,[object Object], ,[object Object],(,[object Object],):
    ,[object Object], ,[object Object],(resolve_in(path, READ_ROOT)).read()[:,[object Object],]

,[object Object], ,[object Object],(,[object Object],):
    full = resolve_in(path, WRITE_ROOT)   ,[object Object],
    ,[object Object], ,[object Object],(full, ,[object Object],) ,[object Object], f:
        f.write(content)
    ,[object Object], ,[object Object],

What this does: it gives the agent read access across the whole docs tree but confines writes to a

drafts/
subdirectory, using the same resolve-and-verify jail with two different roots. The agent can read anything for context and change only what's meant to be changed. A misfired write lands in drafts, where a human reviews it before it's promoted — turning "the agent edited the wrong file" into "the agent proposed an edit in the review folder."

This read-wide, write-narrow split maps onto how careful teams already work with junior contributors: full visibility, limited commit rights. The agent gets the context it needs without the authority to break things it shouldn't touch.

⚡ Pro tip: Return a manifest of what an agent wrote at the end of each run — the list of paths it created or modified. A human scanning five paths in

drafts/
can approve a change in seconds, and the manifest doubles as a rollback list if something looks wrong. Cheap to produce, and it makes the whole write path reviewable.

A regulated-industry engineer uses exactly this split so an agent can read across a large knowledge base for grounding but can only ever write to an isolated output folder that's version-controlled and reviewed — read access for quality, write confinement for safety.

How to Apply This to Your Situation

The pattern generalizes anywhere an agent touches paths:

  • A legal-tech engineer whose agent reads contract files jails access to a single matter's folder, so an agent working case A can never read case B — a confidentiality boundary enforced in code, not policy.
  • A game developer letting an agent edit asset configs confines it to the
    assets/
    tree, so a misfired path can't corrupt engine source files elsewhere in the repo.
  • A healthcare data engineer jails an agent to a de-identified working directory and adds an audit log to every resolved path, satisfying a requirement that access to patient-adjacent files be traceable.

In each case the move is identical: pick a root, resolve every requested path to its real location, and refuse anything that lands outside. The jail is a dozen lines and it converts "we hope the paths are fine" into "escapes are structurally impossible."

That shift — from hoping to proving — is the whole point, and it's what makes an agent you can actually put to work on files that matter.

Next Steps

Audit your own file tools for the

os.path.join
trap today — pass them an absolute path and a
../../
chain and see what happens. If either escapes, you have the same hole this team did. Add the resolve-and-verify jail, write the three adversarial tests, and you've closed it. It's an hour of work that removes an entire category of incident, and the tests keep it closed as the code changes around it.

Secure agent file system access comes down to never trusting a path you didn't resolve, and the jail function above is small enough to copy into any harness. Pair it with the read-wide, write-narrow split and a per-run manifest, and you have a file layer that's both useful and provably contained. The tool descriptions that go with it — telling the model it works within a fixed root and paths are relative to that root — are worth saving too, because they cut down how often the model produces an escaping path in the first place. Keeping those proven descriptions in a prompt library like PromptABCD, paired with the jail code they assume, means every file-touching agent you build starts contained by default instead of one innocent-looking

join
away from a leak.

agent file system accesspath traversalsecurityai agentscase studypython

Continue Reading

The SWE-bench Harness Explained for Agent Builders
AI Harness

The SWE-bench Harness Explained for Agent Builders

The swe-bench harness fails logically correct patches when the environment is wrong. Learn how it grades, what FAIL_TO_PASS means, and how to run it yourself.

August 28, 2026·8 min read
Building an Evaluation Harness for Your Agent
AI Harness

Building an Evaluation Harness for Your Agent

The best way to build agent eval harness infrastructure isn't LLM-as-judge. Learn to design verifiable tasks and programmatic graders you can actually trust.

August 28, 2026·8 min read
What Is an Eval Harness, and Why Do Agents Need One?
AI Harness

What Is an Eval Harness, and Why Do Agents Need One?

An AI eval harness tells you your agent works across a hundred tasks, not just the one you tried. Learn what it measures and why agents need it more than models.

August 28, 2026·8 min read

Save the prompts from this post

PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.

Start free →
← PreviousBuilding a Safe Shell Tool for Your Agent HarnessNext →Handling Model Output That Won't Parse in Your Harness
Share this post:
ShareShare