Keep the File Tools Inside the Workspace
A coding agent's read_file tool takes whatever path the model asks for. Yesterday the model asked for ../../.aws/credentials, and got them.
The model did not go looking for trouble. A README said "config lives two directories up", the model believed it, and a tool with no notion of a boundary did the rest. Every file tool needs one rule, enforced in code rather than in the prompt: a path resolves inside the workspace, or the tool refuses.
Implement safe_join(root, path). root is the workspace directory as a POSIX path (/repo). Return the resolved path as a string, or raise PathEscape (provided) when the request would leave the workspace.
Resolve the way a filesystem would, without touching one:
- Split on
/. Treat a backslash as a separator too; the model will produce Windows paths on a Linux box. - Drop empty segments and
., sosrc//./app.pyissrc/app.py. ..steps up one segment. Stepping up from the workspace root is an escape.- An absolute path (
/etc/passwd) or a drive letter (C:) is an escape, even when it happens to start withroot. The model should not be handing you absolute paths at all. - A leading
~is an escape: it means the home directory, which is not the workspace. - A null byte anywhere is an escape. It truncates the path in C and is never legitimate.
The result is root followed by the surviving segments. An empty request, or ., resolves to root itself.
read_file(files, path) in the fixtures calls your function before it looks anything up. That is the whole pattern: the check sits between the model and the filesystem, and it does not care how persuasive the README was.