Shoddy · Fettler

The Fettled Workstation

Two pictures — the disk a developer actually has, and the three boundaries that make it look that way.

This page states no rule of its own. It draws the whole arrangement twice. The first picture is the disk — the directories a developer really has, which of them the assistant may enter, and every way off the machine. The second is the mechanism that produces that result. One answers what can it reach; the other answers how is that held, and it is worth seeing the answer before the machinery.

No command produced this arrangement. Every line of it was written by hand into two short files at the top of the project — a checked-in .fettler.json and a gitignored .fettler.local.json beside it. Both are printed in full directly under the map, so every block can be read back to the line that put it there. fettle setup claude-code --local writes the client’s deny list and the registration and nothing else: it never declares a tree, a scope or a task, and it could not have drawn any of this.

The pages after this one build it piece by piece: the quick start turns it on, installing it is the machine-level reference, in a project is the reference behind the two files below, and screening is the one feature with a page of its own.

A map of a developer's disk and the network beyond it. The machine is one hatched area meaning out of bounds by default. On the left, the code folder holds a sibling project that is out of bounds, a planning repository that grants nothing at its root but opens two folders by name while keeping two others invisible, and the shoddy project which is fully in bounds apart from a sealed pocket holding its own configuration files. On the right, the user's folder holds one permitted scratch directory and several refused ones: keys and credentials, Documents which contains a Dropbox folder, browser profiles, and the operating system. Below the machine is the network: two sanctioned exits lead from the shoddy project to github.com and nuget.org, every other address has no route, and the Dropbox folder has its own continuous sync door to dropbox.com that no task opens and nothing here can close.
The disk, and every way off it. The hatched ground is the whole machine, and it is the default; the lit blocks are the entire list, not a summary of it. A repository can be opened at one folder and shut at the next, so requirements\completed is invisible while its sibling is writable. The files that define the boundary are sealed inside the very tree that grants everything else. And the two exits open from one room only, because that is the single place permitted to run anything at all. Then there is Dropbox — a way off the machine that no task opens and nothing here can close, which is why sealing that folder is not about privacy but about shutting a route that would bypass every other line on the map.

And here is what drew it. Two files, sitting at the top of the project. Read them beside the map: every lit block above is a line below, and every dark one is dark because no line mentions it.

// .fettler.json - checked in, travels with the project
{
  "trees": {
    "work": {
      "path": ".",
      "can": ["list", "read", "create", "update", "rename", "delete", "execute"]
    }
  },
  "tasks": {
    "build":   { "run": "pwsh -File build.ps1 build", "cwd": "." },
    "test":    { "run": "pwsh -File build.ps1 test",  "cwd": "." },
    "feature": { "run": "pwsh -File scripts/shoddy-branch.ps1 feature hebdenbridge", "cwd": "." },
    "ship":    { "run": "pwsh -File scripts/shoddy-branch.ps1 ship -Yes", "cwd": "." }
  }
}
// .fettler.local.json - gitignored, this machine's own trees
{
  "trees": {
    "scratch": {
      "path": "C:/Users/foster/AppData/Local/Temp/claude/c--github-shoddy",
      "can": ["list", "read", "create", "update", "rename", "delete"]
    },
    "requirements": {
      "path": "../shoddy-planning/requirements",
      "can": [],
      "scopes": {
        "backlog":    { "can": ["list", "read", "create", "update", "rename"] },
        "inprogress": { "can": ["list", "read", "create", "update", "rename"] },
        "cancelled":  { "can": [] },
        "completed":  { "can": [] }
      }
    }
  }
}

Correlating the two — including the three things on the map that no line in either file could have put there:

On the mapWhat says so
c:\github\shoddy, and the two exits below itthe work tree — the only can list ending in execute, so the only room on the machine a task may run in
github.com and nuget.org, and no other addressthe four declared tasks: feature and ship run git, build and test restore packages. Nothing names a third host, so there is no third route
Scratch is writable, but nothing may be executed therethe scratch tree’s can, which stops one word short of execute
shoddy-planning grants nothing at its root"can": [] on the requirements tree
backlog and inprogress writable, yet nothing there can be deletedthose two scopes’ can, which omits delete
completed and cancelled sealed, and not even listed"can": [] on each — a scope replaces what its tree grants rather than adding to it, so an empty one grants nothing at all
shoddy-devilsdust, Documents, .ssh, AppData\Roaming, the whole of C:\no line at all. Out of bounds is the default; these two files are the list of exceptions to it, and whatever is absent stays dark
The sealed pocket inside shoddyno line, and no line could. The six governed names are refused by every write path at every permission level — the tool boundary lists them
Dropbox syncing out to dropbox.comneither file’s doing. That door belongs to another program, which is precisely why the folder it watches is one of the ones left dark

And here is how it is held. The same arrangement again, drawn as the three boundaries that produce it: what the assistant is, the one process between it and the disk, and the filesystem as the declared trees present it.

Component diagram of three boundaries. First, the model, Claude in VS Code, whose eleven built-in tools are every one of them struck through as denied: the six file tools Read, Write, Edit, NotebookEdit, Grep and Glob; then the three shells Bash, PowerShell and Monitor, Monitor being a shell because it runs the command it is handed in the same environment Bash does; and last the two output readers BashOutput and TaskOutput, which start nothing and write nothing but hand back the output of work already done, bytes that reached the model without passing the tree boundary, the secret scan or the disclosure screen. Second, the Fettler MCP server, whose boundary comes from .fettler.json and which refuses to write the files that govern it or the model. Third, the filesystem, visible only as the declared trees, with everything else refused as nonexistent.
A workstation in fine fettle. The client denies all eleven of its own built-ins — the six file tools, the three shells, and the two output readers — so the Fettler server is the only route to disk. Monitor is a shell, running what it is handed in the same environment Bash does, so denying Bash and leaving it open would deny the word and not the thing. BashOutput and TaskOutput are readers: they start nothing and write nothing, but they hand back the output of work already done, and those bytes reached the model without passing the tree boundary, the secret scan or the disclosure screen. The server takes its boundary from .fettler.json at launch, and refuses to write any file that governs it or the model. And the filesystem exists only as the declared trees — everything else answers exit 4, whether or not it is there. The diagram fits the page; Full screen shows it at reading size.

1. How to read it

2. The model boundary

Two levels of configuration reach the model, and the diagram shows both. The deny list — the settings entry that blocks the assistant's built-in file, shell and output tools — appears at each level.

The deny list fettle setup claude-code --local writes, verbatim:

{
  "permissions": {
    "deny": ["Read", "Write", "Edit", "NotebookEdit", "Grep", "Glob",
             "Bash", "PowerShell", "Monitor",
             "BashOutput", "TaskOutput"]
  }
}

The registration it writes, and nothing more:

{
  "mcpServers": {
    "fettler": { "command": "fettle", "args": ["serve"] }
  }
}

And the opt-in session hook (--hooks), which runs doctor at the start of each session — the moment a diagnostic will actually be read:

{
  "hooks": {
    "SessionStart": [
      { "hooks": [
          { "type": "command", "command": "\"C:\\tools\\fettle\\fettle.exe\" doctor --hook", "timeout": 10 }
      ] }
    ]
  }
}

3. The tool boundary

The server learns its trees once, at launch, from files it can never write.

The two files that say all of this are printed under the map at the top of the page: the checked-in .fettler.json naming the project tree and its tasks, and the gitignored overlay adding this machine’s scratch tree and its scoped planning tree. Neither can be written by the thing it governs — which is what a refusal looks like when the boundary is asked to cross itself:

$ fettle write .fettler.json --text "{}" --overwrite
refused: .fettler.json is one of the files that tell this tool what it
may do, and it does not edit those. Change it with an editor.
$ echo $LASTEXITCODE
11

$ fettle read ../secrets.txt
refused: the path is outside every declared tree; ask for the roots to
see the boundary
$ echo $LASTEXITCODE
4

4. The filesystem boundary

Seven permission words, combined per tree and per folder, say everything the diagram colours. The seven permissions defines each in full.

VerbGrants
listappears in find and searchabsent means hidden, not merely unreadable
readcontent may be read
createa file or directory that is not there may be made
updatean existing file’s content may be changed
renamerenamed or moved within this scope, or into a descendant
deleteremoved — and moved out of this scope, which from here is the same thing
executea declared task may run here — never granted by default, anywhere

Prove it, don’t trust it. The wiring being drawn correctly and the boundary holding are two different claims, and only the second one matters. Ask the assistant to try each of these, and watch what comes back. Verifying the guardrails is the same checklist where the setup steps live:

Ask it to…A correct setup…
read C:\Windows\win.inirefuses — outside every tree, exit 4, identically whether or not the file exists
write .fettler.jsonrefuses — governed, exit 11, whatever the tree grants
write .claude/settings.jsonrefuses — governed, exit 11: a model does not edit the file naming its tools
write a file holding an AWS keyrefuses — credential, exit 12, naming the line but never the secret
run a task with no execute grantrefuses — exit 8, naming the missing grant
edit with the built-in editoris denied by the client — and reaches for Fettler instead
stage a file in its scratch directorywrites — because the overlay declared that tree; if this fails, that was the skipped step