Shoddy · Fettler

Tools

The first level, on in every folder with nothing written down. Every verb, every check, and the folders your assistant already uses.

This page describes the tools level, which is always on and needs no file. Fettler does three kinds of work, and each kind is called a level. The tools level is the first one, and it gives you every verb, the checks that run on every call, and the folders your client keeps for this project. The client is the program your assistant runs in, such as Claude Code or VS Code. The other two levels, guardrails and screenings, stay off until a configuration file turns them on.

You do not have to write anything to use the tools. Install fettle, register it with your client, and start the client in a project folder. The configuration file, .fettler.json, is only for changing what this page describes, or for turning on the next two levels. Installing it covers the registration.
The tools level. With no configuration file, the launch folder is the tree work. fettle also opens this session's scratch folder, the client's tool-results and memory folders, and a companion server's root. Older sessions and the session transcripts stay outside.
The tools level. The launch folder is the tree work. fettle also opens this session's scratch folder, the client's tool-results and memory folders, and the folder a companion server works in. Older sessions and the session transcripts stay outside, with everything else on the disk.

A folder with no file

With no .fettler.json, the folder fettle starts in becomes the one tree you work in. That folder is called the launch folder. A tree is a folder Fettler may work in, together with the list of things it may do there. The program looks for the file in the launch folder and then in each folder above it, up to the top of the drive. If it finds none, you are at the tools level.

At the tools levelWhat happens
The launch folderIt is one tree, called work. It is the default tree, so a path written with no tree name lands there.
What work allowslist read create update rename delete. Every verb that reads or changes a file works inside it.
Running a taskA task is refused, because the tree does not grant execute and tasks belong to the guardrails level.
A path that climbs out of the launch folderRefused as outside-root, exit 4. The words are the same whether or not the file exists.
The client's foldersOpened as three more trees, which the client's folders describes below.

The server looks for a file again on every call until it finds one. The server is fettle running as fettle serve, the form your assistant talks to. If you write a guardrails section into a new .fettler.json while it runs, the next call obeys it with no restart. Once the server has found a file, it re-reads that same file on each call.

What went wrong, and what it looks like now

The home page lists what went wrong with the old tools. Here is the same list with the answers fettle gives instead. Each example is the terminal form. The MCP answer carries the same facts as JSON, and the JSON form of read carries a hash for every file. A path in an answer names its tree, as work:src/Glob.cs, because more than one tree is almost always open.

Two patterns go in one call. Each hit comes back as the tree, the path, the line and the column, with the matched line under it. A hit inside a PDF names its page instead. A pattern of files that matched nothing says so, rather than looking like a search that found nothing.

$ fettle search "class Glob" "IGlob" --glob "src/**/*.cs"
work:src/Fettler/Glob.cs:12:21
   12 | public sealed class Glob
work:src/Fettler/Glob.cs:31:9
   31 |     IGlob matcher = Build(pattern);
2 hits in 1 file

$ fettle search "TODO" --glob "src/**/*.ts"
0 files matched the glob; nothing was searched

One read, a cap it states

The first line gives the file's kind, encoding and line ending, and which lines of how many came back. A read that stops says how many lines are left and how to get them. A minified file, which is one enormous line, stops at the 40,000-character cap instead, and the answer says that too.

$ fettle read src/Fettler/Glob.cs --to 3
work:src/Fettler/Glob.cs  text  utf-8  lf  final-newline: yes  lines 1-3 of 212
    1 | using System;
    2 | 
    3 | namespace Fettler.Core;
... 209 more line(s); pass --from 4 to continue, or --to N for a range

An edit that refuses, because the file changed

A read returns a hash of the file. An edit takes it back as expect. If anything changed the file in between, the edit is refused before a byte is written, with exit code 6, and the fix is to read again. An anchor that matches in more than one place is refused the same way, naming the edit, so an edit never lands in the wrong place.

$ fettle edit src/Fettler/Glob.cs --expect 6f1c... --replace "IGlob" --with "IMatcher"
refused: the file changed since it was read; read it again and rebuild the edit
$ echo $LASTEXITCODE
6

A write that is refused

Two writes are refused whatever the tree grants. A write that would add a secret names the line and the kind of secret, never the value. A write to a file that says what fettle or the assistant may do names the file.

$ fettle write config/app.json --text-file app.json
refused: this write would add a credential to the file: line 14 (aws-access-key-id).
$ echo $LASTEXITCODE
12

$ fettle write .fettler.json --text-file new.json
refused: .fettler.json is a file that says what this tool and the assistant may do.
$ echo $LASTEXITCODE
11

What every call checks

These checks belong to the tools level, so they hold with no file at all. No configuration file can turn any of them off.

CheckWhat it does
Paths stay in the treesEvery path must land inside a tree. There is no current folder to change, and no path can climb out of a tree.
The hashread returns a hash of the file's content. Pass it as expect on an edit, and the edit is refused if the file changed since you read it.
The capsA read stops at 2,000 lines or 40,000 characters, and the answer says how much it left out. A search stops at its limit and says so.
The typed readsNotebooks come back as cells, spreadsheets as rows with their formulas, Word files as paragraphs, and PDFs as text per page.
Generated foldersfind and search skip bin, obj, .git, artifacts and node_modules unless you ask for them.
The credential checkA write that would add a secret, such as a cloud access key or a private key, is refused as credential, exit 12. The refusal names the line but never the secret.
The governed filesFettler refuses to write any file that says what it or the assistant may do. The refusal is governed, exit 11, whatever a tree grants.

The governed files are .fettler.json, .fettler.local.json, .mcp.json, .picker.json, .picker.local.json, .claude/settings.json, .claude/settings.local.json, .vscode/mcp.json and the machine file fettler/fettler.json. A person changes them in an editor. Fettler never writes its own rules explains why this one refusal makes every other rule hold.

The verbs at a glance

Every verb in this table works at the tools level. Each one is a command on the command line and a tool your assistant can call. The verbs on the reference page gives each one's arguments and answers in full.

VerbWhat it does
findLists the files that match a pattern, with the size and time of each one.
searchFinds the lines that match one or more patterns, and gives each hit as its tree, path, line and column.
readReads one or more files and returns the text with its encoding, line ending and hash.
writeCreates a file, or replaces one when you say it may overwrite.
editApplies a set of edits to one or more files. Either all of them apply, or none of them do.
replaceReplaces one string with another in every file that matches a pattern. Either all files change, or none do.
newCreates an empty file, and refuses if a file of that name is already in that folder.
mkdirCreates a folder, along with any folders missing above it.
moveRenames or moves a file or a folder, and says whether the move happened in one step.
copyCopies a file, or a whole folder when you ask for every file inside it.
deleteDeletes one path. A folder that still holds files needs you to ask for everything inside it.
extractUnpacks a zip or tar archive into a tree, and refuses the whole archive if any part would land outside.
execSets or clears the executable bit, the mark that lets a file run as a program on macOS and Linux.
batchRuns several operations in order, and stops at the first one that fails.
rootsLists the levels that are on, then every tree with what it allows.
doctorReports how fettle is wired into each client, with one section for each level. It changes nothing.

Two more verbs, run and tasks, belong to the guardrails level. At the tools level they answer that no task is declared. Declaring tasks is on the next page.

The client's folders

fettle finds three of your client's folders and opens each one as a tree, with nothing declared. Your client keeps these folders for the assistant, outside your project. The assistant writes drafts in one, reads large answers back from another, and keeps notes in the third. An assistant that may use only Fettler would have nowhere to put a draft without them.

TreeWhat it isIt may
scratchThis session's scratch folder, where the assistant stages drafts. On Windows it is <temp>\claude\<project>\<session>\scratchpad. On macOS and Linux it sits under /tmp/claude-<uid> instead.list read create update rename delete, and never execute
tool-resultsWhere the client saves a tool answer too large to show, then asks the assistant to read it back. It is ~/.claude/projects/<project>/<session>/tool-results, or the same name directly under the project folder.list read
memoryThe client's memory folder for this project, where the assistant keeps notes between sessions. It is ~/.claude/projects/<project>/memory.list read create update rename delete

<project> is the launch path, spelled the way the client spells it. Each folder separator and the drive colon become a hyphen, so c:\github\app becomes c--github-app. A declared tree may not use the names scratch, tool-results or memory, because fettle gives those names to these folders.

Which session is this one

Only this session's folder is opened, and fettle chooses it in a fixed order. A session is one conversation with the assistant, and the client gives each one its own folder.

  1. First, a value in fettle's environment that is exactly a session folder's name. The client starts fettle with a set of environment variables. If one of them holds the name of a session folder on disk, that folder is chosen. This is an exact match, never a guess.
  2. Otherwise, the newest session folder. That is right for a new conversation. It can be wrong for a conversation you resumed, which is why roots says which of the two routes it took.

roots says how each of these folders was found. Each one carries a found: line. For scratch it reads either this session's folder, named by the environment variable NAME or the newest session folder, because no environment variable names one. If you see the second line in a resumed conversation, check the folder before you trust it.

Companion servers

A companion is another MCP server that works in one folder you name. MCP is the protocol an assistant uses to call a tool server, and fettle is one such server. A companion's registration passes --root PATH, and that server reads and writes the files under that folder. sparky, the Shoddy calculator, is the usual case.

fettle opens a companion's folder as a tree named for the server, with read and write. The assistant can already reach that folder through the companion, so the tree grants nothing new. It lets fettle write a file there that the companion loads in the same turn.

At the tools level, doctor reports each companion's folder as one line of fact. It is never a finding here, because no tree states a rule that the companion could ignore. With guardrails on, a companion folder inside a declared tree becomes a serious finding, which the guardrails page explains.

What roots says at the tools level

Ask for the roots at the start of any session. At the tools level the trees section shows only work, the launch folder. The folders fettle found for itself come after it, each with its found: line. A level that is off prints off.

$ fettle roots
levels
  tools         on, always
  guardrails    off
  screenings    off

trees
work          C:\github\app  (default)
              can: list read create update rename delete
              branch: main (local)

found by fettle
scratch       C:\Users\you\AppData\Local\Temp\claude\c--github-app\1f0c9a2e-5b7d-4c11-9e3a-7d2b8f6a4c10\scratchpad
              can: list read create update rename delete
              found: this session's folder, named by the environment variable NAME
tool-results  C:\Users\you\.claude\projects\c--github-app\1f0c9a2e-5b7d-4c11-9e3a-7d2b8f6a4c10\tool-results
              can: list read
              found: in the client's folder for this project, in this session's folder; this session's folder, named by the environment variable NAME
memory        C:\Users\you\.claude\projects\c--github-app\memory
              can: list read create update rename delete
              found: in the client's folder for this project
sparky        C:\Users\you\Dropbox\shoddy\halifax
              can: list read create update rename delete
              found: the --root of the sparky server registered in C:\Users\you\.claude.json

declared by no .fettler.json, so the launch folder is the tree

The optional tools section

A tools section changes four things about the tools level, and nothing else. You need it only when fettle's own choices are wrong for this machine or this project. Writing it does not turn on guardrails. The caps, the hashes and the readers are fixed, and no file can change them.

KeyWhat it does
generatedNames the folders that find and search skip. It replaces the built-in list, so repeat any of the five you still want skipped.
scratchNames a folder to open as scratch, in place of the one fettle would choose. Use it when your client keeps its scratch somewhere fettle does not look.
scratch-rootNames the folder that holds every project's scratch folders. fettle looks for this project's folder inside it.
client-rootNames the folder that holds the client's folder for each project. When you leave it out, fettle uses ~/.claude/projects.

A path in the file is read from the folder the file sits in. A path that is a fact about one machine, such as scratch, belongs in .fettler.local.json. That is the uncommitted file beside the project file, and the personal overlay describes how it merges.

A worked tools file

This project builds into a dist folder, so its file adds that folder to the list. On the left is the whole file. On the right is what roots says with it in place. Notice that guardrails are still off, and that the tools line now names the file.

// C:\github\app\.fettler.json
{
  "tools": {
    "generated": ["bin", "obj", ".git",
                  "artifacts", "node_modules",
                  "dist"]
  }
}
$ fettle roots
levels
  tools         on, always; settings from C:\github\app\.fettler.json
  guardrails    off
  screenings    off

trees
work          C:\github\app  (default)
              can: list read create update rename delete
              branch: main (local)

found by fettle
scratch       C:\Users\you\AppData\Local\Temp\claude\...\scratchpad
              ...
memory        C:\Users\you\.claude\projects\c--github-app\memory
              ...

declared by C:\github\app\.fettler.json

The trees are the same as with no file, because a tools section declares no tree. Only a guardrails section does that, and the next page shows one.

The machine file

The machine file holds facts about one machine, and they apply to every project on it. On Windows it is %APPDATA%\fettler\fettler.json. On macOS and Linux it is ~/.config/fettler/fettler.json, or the same path under $XDG_CONFIG_HOME when that is set.

Where to go next

You wantPage
To declare your own trees, scopes and tasks, and close the client's built-in toolsGuardrails
To refuse answers that would disclose regulated dataScreenings
Every verb's arguments, every refusal, and every exit codeReference