Shoddy · Fettler

Fettler

File, search and edit tools for a model and a script. One program, two front ends, and no shell between it and the file.

An assistant given Fettler cannot go wandering. Everything outside the boundary is denied. Every path it touches must sit inside a tree — a folder you named ahead of time. Outside those trees, everything is refused: writing, reading, listing, even learning that a file exists. There is no current directory to change. No path can climb out of a tree. There is no shell to sneak past it with. So the assistant cannot wander into a sibling project, an old copy, or your home directory, read the wrong thing, and then answer from it with complete confidence.

A terminal showing two declared trees with their permissions, and a path from outside the boundary refusedfettlebounded file tools · no shell in between

Inside the boundary, it does the work an assistant actually does on source files. It can find a file, search its contents, read it, write it, edit it, move it, copy it, and delete it. It can also run a named build task. It is written in C#, ships from this repository, and installs as one file. And nothing it does changes how you work. Your shell, your file explorer, your editor, and your git stay exactly as they were.

The project is Fettler; the command is fettle. The command uses the verb form because fettle move a.txt b.txt reads as an instruction, and fettler move does not. Both spellings are correct. Neither is a typo.

Note This page asks you to type nothing. To have it working in about five minutes instead, go straight to the quick start.

What it does

Bounded, not advisoryEvery path resolves inside a tree you declared. Outside is refused identically whether or not the file exists, so a refusal cannot be used to probe a disk. No current directory, no path that climbs out, no shell to reach past it with.
Every answer is the next call's argumentsearch hands back tree:path:line:column. read numbers its lines and states a hash. edit asks for exactly those. Refused edits stop happening.
It reads more than sourceNotebooks as cells, spreadsheets as rows with their formulas, Word as paragraphs under their headings, PDF as text per page, images as facts — and inside archives without unpacking them.
Batches are all or nothingEvery edit resolves before the first byte moves. A permission not granted, a file changed underneath, an anchor that no longer matches: any of these stops the whole batch with the tree exactly as it was.
Two front ends, one coreA CLI for a script, an MCP server for a model, over the same operations. Machine-readable mode puts the complete result on stdout — failures included.
Nothing changes how you workYour shell, file explorer, editor and git stay exactly as they were.

One refactor, with the arithmetic

Rename a C# type in this repository and you touch 200 occurrences across 23 files. That number is measured, not estimated — fettle search counted it. This is the change nobody does by hand, and every assistant is asked to do anyway.

Built-in file toolsFettler
Callsone search, then a read and an edit for each file — 47search, replace --dry-run, replace3
Context spent23 whole files pulled through itthe matching lines
If it fails at file 1514 files renamed, 9 not: a tree that no longer compiles, half-changed — and nothing says which halfnothing is written unless all 23 resolved first; if a write then fails, it names the file it stopped at and how many were already done
Per-file propertieswhatever the writer happened to emiteach file keeps its own encoding, line endings and execute bit
The measurement that decided the design. A model's tool call costs a round trip and a block of its context. Scanning this repository's 15 MB of tracked text costs a few hundred milliseconds. The exchange costs about a thousand times more than the work — so the work is not the thing to optimise.

It can refuse to disclose regulated data

Optional, off by default, and it costs one line to try. A tree can declare that certain things must not leave it. The first tier runs inside fettle with nothing installed: labelled record numbers, dates of birth, SSNs, Luhn-valid cards, emails, phone numbers.

"trees": {
  "records": { "path": "./records", "can": ["list", "read"], "screen": ["identifiers"] }
}

Anything it finds refuses the whole response rather than handing back a redacted copy that looks safe and is not — and an image, which it cannot read into, is refused rather than served unchecked.

And here is what it does not catch. Names, addresses, conditions and free text need a model, which is a separate download. identifiers is the only one of the four categories that works with nothing installed; clinical, legal and scientific screen nothing at all until you install one, and fettle roots says so in as many words rather than letting the category name imply otherwise. How screening works →

Getting started

It is written in C#, ships as one self-contained file per platform, and needs nothing installed alongside it. burler, the model host the screen's second tier uses, is a separate download wanted only by people who switch that tier on.