May 24, 2026
Every filesystem tool gets a spec and a policy
A day spent tearing three filesystem abilities down to spec and giving Chalie a safe way to search, write, download, and chmod files without touching a shell.
The goal today was narrow and deliberate: stop letting Chalie reach for bash to touch the filesystem, and make the purpose-built alternatives boring, predictable, and spec-true. Three abilities — search_files, file_write, web_download — had drifted from their original specs as they picked up features nobody asked for. Today was the day I sat down and pulled them back in line, then closed the gap with a new one.
search_files was the biggest offender. It had grown a JSON wrapper, a truncated flag, a 5MB file cap, and a deadline timeout — none of it in the spec, all of it surface area an LLM has to reason about before it can just grep a directory. I stripped it back to plain text output: glob returns newline-separated paths, grep returns path-plus-context blocks. Defaults got corrected too — max_files 10 to 5, context_lines 3 to 5 — and overflow on either now raises a validation error instead of silently clamping, because a tool that quietly changes your request is worse than one that tells you no. The test suite went from 23 plumbing tests asserting mock call counts to 8 real feature tests running against an actual filesystem, which is the only kind of test that catches a real regression here.
file_write and web_download got the same treatment. file_write lost its tmp/system action split entirely — there’s now one contract: write to a caller-supplied absolute path, with a read-guard that only fires if the file already exists, and content is auto-created with its parent directories. web_download shed a pile of unrequested hardening (100MB cap, retry logic, SSL fallback, redirect limits) it had accumulated, and gained what the spec actually called for: a user-facing timeout param, OS-aware temp directories, and original filenames preserved inside UUID subdirectories so two downloads never collide. Policy across both tools was flattened to a single flat allow/deny per context instead of the two-tier tmp/system split I’d bolted on earlier — one clear rule beats two vague ones.
The new piece is file_permissions — a chmod ability that returns permissions-before and permissions-after on every call, so a permission change is auditable without ever running raw chmod through a shell. It ships deny-by-default for subconscious and external-agent contexts; a background process or an external caller doesn’t get to touch file permissions, only a chat session with the user watching can ask for that.
Underneath all four abilities, a bigger cleanup landed: FileMapperService, a single OOP singleton that now owns every path resolution in the codebase. It replaces a procedural paths.py module and the last scattered Path(__file__).resolve().parent calls — the kind of pattern that quietly breaks the moment you move a file or package the app differently. Fifty-three files got migrated to the new API for a net negative-line diff. Document storage paths, previously duplicated across three separate modules, now all resolve through one method on that service. This isn’t a feature anyone will notice directly, but it’s the kind of consolidation that keeps future filesystem abilities honest — there’s now exactly one place that decides where anything lives on disk.
There’s a theme forming across the abilities work this stretch: give the model a small number of tools that do one thing with a hard-edged contract, instead of a general-purpose escape hatch it can bend to fit anything. A model with search_files, file_write, web_download, and file_permissions never needs to reach for a shell to do routine filesystem work — and every one of those calls is auditable, policy-gated, and predictable in a way bash find | grep never was. That’s the shape the next stretch of ability work is going to keep pushing on.
-
search_filesstripped to plain-text glob/grep output — no JSON wrapper, no truncation flag, no file-size cap, matching the original spec exactly -
file_writeunified under one absolute-path contract with auto-created parent directories and a read-guard scoped to files that already exist -
web_downloadre-aligned to spec: user-facing timeout, OS-aware temp storage, original filenames preserved, flat allow/deny policy per context -
New
file_permissionschmod ability records permissions-before/after for every change, deny-by-default outside chat -
FileMapperServicesingleton replaces the old proceduralpaths.py, migrating 53 files off scatteredPath(__file__)resolution for a net negative-line diff