{"slug": "how-to-block-malicious-file-writes-in-autonomous-python-agents", "title": "How to Block Malicious File Writes in Autonomous Python Agents", "summary": "A developer released AgentWritePathResolver, a standalone Python standard-library script that validates and canonicalizes file paths proposed by autonomous LLM agents before any disk write occurs. The tool runs an eleven-step check covering directory traversal, symlink abuse, null bytes, control characters, hidden files, extension allowlists and workspace boundaries, returning an ALLOW or REFUSE verdict with a specific refusal reason. It requires Python 3.8 or higher and no external packages.", "body_md": "Autonomous AI agents, automated coding assistants, and Large Language Model (LLM) tool executors frequently need to write files to disk. However, accepting a file path generated by an AI model introduces substantial security risks. If an agent hallucinates, encounters prompt injection, or attempts to modify files outside its target directory, it can overwrite system configurations, expose private keys, or destroy version control history.\n\nCommon attacks and edge cases include directory traversal (such as `../../`), symlink abuse where a link points to an arbitrary location outside the project, embedded null bytes, non-printable control characters, and writes targeting sensitive files like `.env` or `.ssh/authorized_keys`.\n\nThe `AgentWritePathResolver` script provides a security policy engine designed specifically for these workflows. It inspects, canonicalizes, and verifies candidate file write paths before any modification is permitted on disk.\n\nThe tool is implemented as a standalone script using only the Python standard library. It requires Python 3.8 or higher. No external packages need to be installed via `pip`.\n\nTo set it up:\n\n`agent_write_path_resolver_refuse_writes_whose.py`.\n\n```\npython agent_write_path_resolver_refuse_writes_whose.py --help\n```\n\nYou can integrate the resolver directly into your agent execution loop or tool-handling layer.\n\nBelow is an example of importing the resolver inside a Python application to guard an agent tool function:\n\n``` python\nfrom pathlib import Path\nfrom agent_write_path_resolver_refuse_writes_whose import (\n    AgentWritePathResolver,\n    PathVerdict,\n    RefusalReason,\n)\n\n# Define the boundaries of the agent workspace\nworkspace = Path(\"/tmp/agent_workspace\")\nworkspace.mkdir(parents=True, exist_ok=True)\n\n# Initialize the resolver with custom rules\nresolver = AgentWritePathResolver(\n    workspace_root=workspace,\n    deny_hidden=True,\n    allowed_extensions=[\".py\", \".txt\", \".json\", \".md\"],\n    disallow_symlinks=True,\n)\n\n# Test candidate paths proposed by an agent\ncandidates = [\n    \"safe_file.py\",\n    \"../../etc/passwd\",\n    \"notes/todo.md\",\n    \".env\",\n    \"script\\x00.py\",\n]\n\nfor candidate in candidates:\n    result = resolver.resolve(candidate)\n    if result.verdict == PathVerdict.ALLOW:\n        print(f\"[ALLOWED] {candidate} -> {result.resolved_path}\")\n    else:\n        print(f\"[REFUSED] {candidate} -> Reason: {result.reason} ({result.message})\")\n```\n\nWhen evaluated, paths escaping the root or matching protected rules are flagged with `PathVerdict.REFUSE`, halting execution before any disk write operation occurs.\n\nThe resolver executes an eleven-step validation sequence against the requested path. It validates the input string, checks system constraints, traces ancestor paths for symbolic links, verifies boundaries, and inspects pattern denylists.\n\nBefore resolving the path on the file system, the resolver checks for control characters and null byte injection attempts:\n\n```\n# 1. Null byte detection\nif \"\\x00\" in candidate:\n    return ResolutionResult(\n        candidate_path=candidate,\n        verdict=PathVerdict.REFUSE,\n        reason=RefusalReason.NULL_BYTE_DETECTED,\n        message=\"Candidate path contains prohibited null byte (injection vector).\",\n        workspace_root=str(self.workspace_root),\n    )\n\n# 2. Control characters detection\nif re.search(r\"[\\x01-\\x1f\\x7f]\", candidate):\n    return ResolutionResult(\n        candidate_path=candidate,\n        verdict=PathVerdict.REFUSE,\n        reason=RefusalReason.CONTROL_CHAR_DETECTED,\n        message=\"Candidate path contains non-printable control characters.\",\n        workspace_root=str(self.workspace_root),\n    )\n```\n\nIf the candidate path passes sanitization, the resolver checks whether the provided workspace directory actually exists. If the workspace does not exist or is not a directory, it returns `RefusalReason.WORKSPACE_NOT_FOUND`.\n\nAgents often target files that do not yet exist. Calling standard `resolve()` directly on non-existent paths on some platforms can fail to expand intermediate symlinks correctly. \n\nThe `_safe_canonicalize` method resolves intermediate directories step-by-step from the root downward. This ensures that if any parent directory is a symbolic link leading outside the workspace, the link is detected:\n\n``` php\ndef _safe_canonicalize(self, path: Path) -> Tuple[Path, bool]:\n    symlink_encountered = False\n    parts = path.parts\n    if not parts:\n        return path.resolve(), False\n\n    # Build incrementally from anchor\n    current = Path(parts[0])\n    for part in parts[1:]:\n        current = current / part\n        if current.is_symlink():\n            symlink_encountered = True\n            try:\n                current = current.resolve()\n            except RuntimeError:\n                # Possible circular symlink\n                symlink_encountered = True\n                break\n\n    resolved = current.resolve()\n    return resolved, symlink_encountered\n```\n\nIf `disallow_symlinks=True` is configured and an intermediate path component is a symlink, the operation is refused with `RefusalReason.SYMLINK_ESCAPE`.\n\nOnce canonicalized, the target path must reside strictly within `self.workspace_root`. The helper method `_is_subpath` uses `Path.relative_to` to confirm containment:\n\n``` php\n@staticmethod\ndef _is_subpath(target: Path, base: Path) -> bool:\n    try:\n        target.relative_to(base)\n        return True\n    except ValueError:\n        return False\n```\n\nIf the resolved target passes the boundary check, the relative path is checked against `DEFAULT_DENY_PATTERNS`. This built-in denylist rejects modifications to sensitive project files and directories:\n\n`*.git*`, `*/.git/*`\n`*.env*`, `*.pem`, `*.key`, `*.id_rsa*`\n`*.ssh/*`, `*/authorized_keys*`\n`*.bashrc`, `*.zshrc`, `*.profile`\n`*.vscode/*`, `*/node_modules/*`\nAdditionally, if `deny_hidden=True` is enabled, any path component containing a leading period (such as `.config/test.json`) triggers `RefusalReason.HIDDEN_TARGET_REFUSED`. Finally, the resolver checks for extension constraints, directory collisions, and write permissions on existing files.\n\nWhile `AgentWritePathResolver` prevents common path manipulation exploits, you should be aware of the following architectural limitations:\n\n`.env` and `.git`, but it cannot anticipate every custom secret or proprietary configuration file name your system may use. Custom patterns should be explicitly supplied via `deny_patterns`.\n**Why does the resolver track symlinks if it already checks the final resolved path?**\n\nEven if a symlink resolves to a location inside the workspace, allowing agents to navigate or create symlinks can lead to loops, bypass naming rules, or alter files across logically separated subdirectories. Setting `disallow_symlinks=True` blocks symlink traversal entirely.\n\n**Can an agent target an absolute path that happens to be inside the workspace?**\n\nYes. If an agent passes `/path/to/workspace/file.txt`, the resolver evaluates the absolute path directly. As long as it resides within the configured `workspace_root` and satisfies all pattern and extension rules, the verdict will be `PathVerdict.ALLOW`. If it falls outside the root, it returns `RefusalReason.ABSOLUTE_PATH_ESCAPE`.\n\n**How does the tool handle writes to existing read-only files?**\n\nBefore returning an approval verdict, the resolver checks whether the target file exists. If it exists and `os.access(target, os.W_OK)` returns `False`, the operation is refused with `RefusalReason.EXISTING_FILE_READONLY` rather than failing mid-write.\n\nFull version and the rest of the toolkit: [Agent write path resolver refuse writes whose on PyForge](https://py-forge.com/products/agent-write-path-resolver-refuse-writes-whose?utm_source=devto&utm_medium=article&utm_campaign=agent-write-path-resolver-refuse-writes-whose). Product page: [https://py-forge.com/products/agent-write-path-resolver-refuse-writes-whose](https://py-forge.com/products/agent-write-path-resolver-refuse-writes-whose)", "url": "https://wpnews.pro/news/how-to-block-malicious-file-writes-in-autonomous-python-agents", "canonical_source": "https://dev.to/alessio99xx/how-to-block-malicious-file-writes-in-autonomous-python-agents-41jj", "published_at": "2026-11-15 02:09:47+00:00", "updated_at": "2026-11-15 02:21:25.225540+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-tools", "developer-tools", "artificial-intelligence"], "entities": ["AgentWritePathResolver", "Python", "PathVerdict", "RefusalReason"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/how-to-block-malicious-file-writes-in-autonomous-python-agents", "markdown": "https://wpnews.pro/news/how-to-block-malicious-file-writes-in-autonomous-python-agents.md", "text": "https://wpnews.pro/news/how-to-block-malicious-file-writes-in-autonomous-python-agents.txt", "jsonld": "https://wpnews.pro/news/how-to-block-malicious-file-writes-in-autonomous-python-agents.jsonld"}}