Shipping guardrails filesystem write access agents is non-negotiable the moment an LLM can create or modify files in your environment. A single unbounded write can corrupt application state, leak secrets through temp files, or become a privilege-escalation pivot. This guide gives an ordered, implementable path from container boundaries to runtime interception that keeps autonomous agents useful without handing them the keys to the host.
1. Define the trust boundary before writing code
Start by declaring exactly which directories an agent may touch. Everything else is off-limits, including parent traversal, symlinks, and hardlinks that point outside the allowlist.
Resolve and verify absolute paths
Never trust the path string the model emits. Canonicalize it and check containment on every call.
import os
ALLOWED_ROOTS = ["/srv/agent/work", "/tmp/agent"]
def safe_path(user_path: str) -> str:
real = os.path.realpath(os.path.abspath(user_path))
for root in ALLOWED_ROOTS:
if real == root or real.startswith(root.rstrip("/") + "/"):
return real
raise PermissionError(f"Path {real} outside allowed roots")
Pitfall: os.path.abspath does not resolve symlinks. Always use os.path.realpath after anchoring. If you skip this, an agent can write a symlink /srv/agent/work/evil -> /etc and then open /srv/agent/work/evil/passwd. Treat the allowlist as a hardcoded constant, not a configuration file the agent can read and edit.
2. Run the agent in a minimal container
A namespace boundary is your first physical line of defense. Use a container with a read-only root filesystem, a small tmpfs for mandated writes, and all capabilities dropped.
docker run --rm \
--read-only \
--tmpfs /tmp/agent:size=64m,mode=1777 \
--cap-drop ALL \
--security-opt no-new-privileges \
--mount type=bind,source=$(pwd)/work,target=/srv/agent/work,readonly=false \
agent-image:latest
Tradeoff: bind mounts are convenient but a wrong :ro flag exposes host paths. Mount only the exact directory and verify with mount inside the container. For stronger isolation, add a seccomp profile that blocks mount, ptrace, and chmod on setuid bits. AppArmor profiles can additionally confine which executables the agent may spawn—critical if the model tries to shell out to dd or cp.
If your agent relies on an inference gateway (n4n.ai offers an OpenAI-compatible endpoint spanning 240+ models with automatic fallback when a provider is degraded), make file writes idempotent so a retried completion doesn’t append duplicate content after a fallback trigger.
3. Intercept filesystem calls with a proxy layer
Container boundaries stop escapes but not wasteful or malicious writes inside the allowed root. Hook the language runtime to enforce policy centrally.
Monkey-patch open in Python agents
If the agent is Python, wrap builtins.open at startup:
import builtins, os
from pathlib import Path
def guarded_open(file, mode='r', *args, **kwargs):
if any(c in mode for c in ('w', 'a', 'x', '+')):
p = safe_path(str(file))
if Path(p).suffix not in {'.txt', '.json', '.md', '.csv'}:
raise PermissionError("Extension not permitted")
return builtins.__open_original(file, mode, *args, **kwargs)
builtins.__open_original = builtins.open
builtins.open = guarded_open
This catches naive open() calls but not subprocesses or C extensions. For those, use an LD_PRELOAD shim that overrides open, openat, and write. LD_PRELOAD is portable across languages but breaks under static binaries; eBPF requires CAP_BPF and kernel 5.4+, but gives syscall-level visibility without modifying the agent code.
LD_PRELOAD sketch
A minimal C shim compiles to a shared object and filters paths:
#define _GNU_SOURCE
#include <dlfcn.h>
#include <string.h>
#include <errno.h>
typedef int (*orig_open_t)(const char*, int, ...);
int open(const char* path, int flags, ...) {
if (strstr(path, "/srv/agent/work") == NULL && strstr(path, "/tmp/agent") == NULL) {
errno = EACCES;
return -1;
}
orig_open_t orig = (orig_open_t)dlsym(RTLD_NEXT, "open");
return orig(path, flags);
}
Build with gcc -shared -fPIC -o guard.so guard.c and launch the agent with LD_PRELOAD=./guard.so. Pitfall: the shim must handle open64 and openat separately on glibc.
4. Enforce write quotas and content limits
Autonomous loops can spam thousands of files. Apply per-run caps on file count and total bytes.
class QuotaEnforcer:
def __init__(self, max_files=100, max_bytes=10_000_000):
self.files = 0
self.bytes = 0
self.max_files = max_files
self.max_bytes = max_bytes
def check(self, size_hint: int):
if self.files >= self.max_files:
raise PermissionError("File count quota exceeded")
if self.bytes + size_hint > self.max_bytes:
raise PermissionError("Total write quota exceeded")
self.files += 1
self.bytes += size_hint
Call check() inside the guarded open before granting write. Pitfall: size_hint from stat is unreliable for streams; wrap the returned file object to count actual bytes written. A streaming wrapper also lets you scan content for secrets before flush.
class CountingWriter:
def __init__(self, fh, enforcer):
self.fh = fh
self.enforcer = enforcer
def write(self, data):
self.enforcer.bytes += len(data)
return self.fh.write(data)
5. Make writes auditable and immutable
Logs are useless if the agent can rewrite them. Ship each write event to an append-only sink with a hash chain.
{
"ts": "2024-05-12T08:21:03Z",
"agent_id": "research-7",
"op": "write",
"path": "/srv/agent/work/out.json",
"bytes": 412,
"prev_hash": "a1f3...",
"hash": "9c2e..."
}
Store the sink on a separate mount or remote service with chattr +a (append-only) at the filesystem level. If you must keep logs in the same container, use a dedicated tmpfs and forward over TLS immediately; assume the agent can read but not modify the remote. Retention policy should match your incident-response window, not the agent’s session length.
6. Simulate adversarial behavior in CI
Guardrails filesystem write access agents need constant verification. Add tests that try to break out:
def test_path_escape():
with pytest.raises(PermissionError):
safe_path("/srv/agent/work/../../etc/passwd")
def test_symlink_attack(tmp_path):
link = tmp_path / "evil"
link.symlink_to("/etc")
with pytest.raises(PermissionError):
safe_path(str(link / "shadow"))
def test_quota_enforced():
q = QuotaEnforcer(max_files=1)
q.check(10)
with pytest.raises(PermissionError):
q.check(20)
Run these in the same container profile as production. A green unit test on a dev box with different mount semantics gives false confidence. Extend the suite with fuzz inputs from the model prompt history to catch unexpected path encodings like ..%2f.
7. Enforce idempotency on model-driven writes
LLM calls can retry. If your agent writes tool outputs directly, a duplicate completion creates duplicate files. Prefix filenames with a content hash or task UUID and overwrite rather than append.
import hashlib
def target_for(content: str) -> str:
h = hashlib.sha256(content.encode()).hexdigest()[:16]
return safe_path(f"/srv/agent/work/{h}.json")
This dovetails with gateway-level retries: an OpenAI-compatible route that honors client routing directives will not surprise you with silent provider switches if you pin the model per task. Idempotent paths also simplify cleanup—delete by hash instead of scanning directories.
8. Monitor runtime behavior and alert
Quotas and path checks reduce risk, but anomalies still slip through. Track write rate, unique paths, and extension diversity per agent instance. Alert when an agent writes more than 2× its rolling median in a minute.
from collections import deque
class RateMonitor:
def __init__(self, window=60):
self.writes = deque()
def tick(self, now):
self.writes.append(now)
while self.writes and now - self.writes[0] > 60:
self.writes.popleft()
if len(self.writes) > 200:
raise Alert("Write burst detected")
Common pitfall: teams implement path checks but forget /proc/self/cwd symlinks or O_TMPFILE anonymous writes. Cover those in seccomp or by blocking openat with O_TMPFILE flag.
9. Treat the agent like a junior engineer with sudo off
No guardrail replaces review. Rotate agent credentials hourly, scope them to the single work directory, and alert on any write outside business hours. The goal is not perfect prevention—it is making the blast radius small and the forensic trail complete.
Ship the container, the runtime hook, the quota enforcer, and the CI adversarial suite together. That combination turns guardrails filesystem write access agents from a theoretical concern into a deployed control you can defend in a postmortem.