Shoddy · Fettler

Quick Start

Install it, register it, turn it on in a project, check it. Four steps, one command each. The pages after this one are the detail behind them.

You need two things before you start: the archive for your platform from the latest release, and a project folder to turn this on in. Everything else is below.

1. Install it

One file. No .NET runtime, no installer, nothing else from this repository. X.Y.Z below is whatever version you downloaded.

Windows · PowerShell
$dir = "$env:LOCALAPPDATA\Programs\fettle"
New-Item -ItemType Directory -Force $dir | Out-Null
Expand-Archive fettle-X.Y.Z-win-x64.zip -DestinationPath $dir -Force
Get-ChildItem $dir -Recurse | Unblock-File
$user = [Environment]::GetEnvironmentVariable('Path', 'User')
if ($user -notlike "*$dir*") {
    [Environment]::SetEnvironmentVariable('Path', "$user;$dir", 'User')
}
macOS and Linux
mkdir -p ~/.local/bin
tar -xzf fettle-X.Y.Z-osx-arm64.tar.gz -C ~/.local/bin
xattr -d com.apple.quarantine ~/.local/bin/fettle   # macOS only

Those commands also put the folder on PATH — the list of folders your terminal searches when you type a command. Now open a new terminal, because the one you just used still has the old PATH. Then check it:

fettle --version
fettle 1.0.0
If that says not recognised or command not found, the usual cause is the terminal — open a new one. The second cause is a typo in the directory. Nothing later on this page works until this line does. The long version, with the reasons, is putting it on PATH.

2. Register it once for this machine

Registering tells your assistant that Fettler exists, so it can launch it. Do this once per account, not once per project. --all means every assistant client this machine actually has; ones that are not installed are skipped rather than created. --dry-run prints exactly what the real run would do and writes nothing. Run it first, every time:

fettle setup --all --global --dry-run
fettle setup --all --global
changed:
  C:\Users\you\.claude.json  register fettler under mcpServers
note: fettle is on PATH at C:\Users\you\AppData\Local\Programs\fettle\fettle.exe, so the
      registration names it plainly
note: this is the machine level and declares no tree; each project still needs its own
      .fettler.json - run `fettle setup claude-code --local` in the tree, which creates one
      declaring that folder as 'work'
note: not written: Read, Write, Edit, NotebookEdit, Grep, Glob, Bash, PowerShell, Monitor,
      BashOutput, TaskOutput would be denied in C:\Users\you\.claude\settings.json. Changing
      how the assistant works in every project needs --deny said out loud
A global run registers, and stops there. It declares no tree, so it writes no .fettler.json. And it only offers the deny list — the settings entry that blocks the assistant's built-in file tools, so Fettler becomes the only route. Adding --deny writes that too, for every project this account ever opens. Leave it off for now; step 3 writes it where you can see it.

3. Turn it on in a project

Change to your project folder and run this. Same --dry-run habit:

fettle setup claude-code --local --dry-run
fettle setup claude-code --local

Four files, and that is the whole of it:

changed:
  C:\work\myproject\.fettler.json  created, declaring this folder as 'work'
  C:\work\myproject\.mcp.json  register fettler under mcpServers
  C:\work\myproject\.claude\settings.json  deny the built-in Read, Write, Edit, NotebookEdit, Grep, Glob, Bash, PowerShell, Monitor, BashOutput, TaskOutput
  C:\work\myproject\CLAUDE.md  add a Fettler instruction block
note: the shell is denied too, so every command through it now stops. Allow back only what
      this project needs - "allow": ["Bash(git status:*)"] and the like - or declare them as
      tasks in .fettler.json, which run inside the boundary. Never allow a command that writes files
FileWhat it does
.fettler.jsonThe boundary. Declares this folder as a tree called work. Fettler will touch nothing outside it.
.mcp.jsonTells the assistant to launch Fettler.
.claude/settings.jsonDenies the built-in file tools and the shell, so Fettler is the route rather than one option among several.
CLAUDE.mdTells the assistant, in words, to use it.
setup and doctor are the only two verbs that run before .fettler.json exists. That is why this step is one command and not two: --local writes the very file every other verb requires. Every other verb in an undeclared folder refuses, and says so.
Your build and test commands stop working at this point. That is the deny doing its job. A shell is a complete way round the boundary, so it is closed by default, and you open only what you need. Either put the command in .fettler.json as a task, or name it in allow"allow": ["Bash(git status:*)"]. Never allow one that writes files. The reasoning is the shell section.

To use another client instead of claude-code: claude-desktop, vscode-copilot, github-copilot, or --all for every one this machine has.

4. Check it

fettle doctor
fettle 1.0.0  C:\Users\you\AppData\Local\Programs\fettle\fettle.exe
on PATH: C:\Users\you\AppData\Local\Programs\fettle\fettle.exe
tool inventory drawn 2026-08-20; check 2.8 says nothing about a tool added to the client since

claude-code     global  healthy   registered and launchable as ...fettle.exe
                        C:\Users\you\.claude.json
claude-code     repo    healthy   registered and launchable as ...fettle.exe
                        C:\work\myproject\.mcp.json
claude-desktop  repo    n/a       this client has no project scope, so there is nothing to configure here

what could let the assistant go round the boundary:
  - 2.8  nothing denies the built-in Read, Write, Edit, NotebookEdit, Grep, Glob, Bash,
         PowerShell, Monitor, BashOutput, TaskOutput, so Fettler is an option rather than
         the route; `fettle setup claude-code --local` writes the denies
      C:\Users\you\.claude\settings.json

The block after the rows is the second half of the report, and it is the half worth reading. The finding shown here is the expected one. Step 2 registered the machine and deliberately did not touch its deny list, so the global settings file still has nothing in it. Your project is covered; every other project on this account is not.

WordMeaningExit
healthyregistered, and the command it names actually launches0
n/anothing to configure at that level for that client — not a fault0
absentthe client is here; Fettler is not registered with it1
brokenregistered and wrong — usually the command does not resolve2

Then restart your assistant. A client reads its server list at launch. A session already running does not have Fettler and will not notice it appear.

If doctor does not come back clean

The report is two halves, and they are fixed in different places. The wiring is one row per client per level, ending in a verdict. The findings come after it, under what could let the assistant go round the boundary. Each finding is numbered, and each is a route past Fettler rather than a broken registration.

What you seeWhat to do
on PATH: noStep 1 did not take. Putting it on PATH — and open a new terminal. Nothing else works until this does.
absentRun step 2 or step 3 for that client and level. The wiring table gives the exact command per row.
brokenThe registration is there, and its command does not resolve. Re-run setup with --command naming the binary, and --force to overwrite an entry that says something else. Details.
A numbered finding — 2.6, 2.8, B.17Everything doctor can report, and how to clear it has a row per check: what it found, and the edit that clears it.
Exit 1 with no finding you recogniseWarnings only, nothing broken. The four verdicts says which rows count as what.
The commonest three, on a first run. 2.15 — your assistant's scratch directory (the staging folder it writes drafts into) is outside every tree, which matters now that Write is denied. Declare it, per step 2 of setting up a project. 2.8 at the global level — expected, since step 2 deliberately left the machine-wide deny list alone. And 2.17 if you run any other MCP server, which most people do: reported as a fact rather than a fault, because the deny list closes names and another server's tools are not called Read.

Proving it, in three commands

Worth doing once, because the guardrail is the reason to install this at all. Run these in the project you just set up.

fettle roots
work        C:\work\myproject  (default)
            can: list read create update rename delete

declared by C:\work\myproject\.fettler.json

That is the whole boundary. Now step outside it:

fettle read C:\Windows\win.ini
refused: the path is outside every declared tree; ask for the roots to see the boundary

Exit 4. And the file that grants the permissions cannot be edited by the thing they are granted to:

fettle write .fettler.json --text "x"
refused: .fettler.json is one of the files that say what this tool and the
assistant driving it may do, and it does not edit those. Change it with an
editor.

Exit 11. Both refusals are the same whether a person or an assistant asks. The boundary is read at launch, from a file neither of them can write.

Where to go next

You wantPage
To widen the tree, add a second one, or hide a folder from the assistant entirelyStep 1, declaring your trees
The scratch directory an assistant stages work in, which needs declaring before Write is deniedStep 2, declaring the scratch
Every flag setup takesIn a project
Every exit code a script can branch onThe reference
To stop a tree of health or customer records leaving in a transcriptScreening
What the permissions mean and why the defaults are what they areFettler
To run it from a script instead of an assistant, or to register it by handThe MCP front end