Shoddy · Fettler

Screenings

The third level. Stop a regulated document leaving a tree by accident. The first tier needs nothing installed and one line of configuration.

This page describes the screenings level, which is off until a configuration file has a screenings section. With it on, Fettler checks what a read or a search is about to send back. If the answer holds regulated data, such as a patient's record number, Fettler refuses the whole answer. Most projects never turn this level on, and you can turn it on with or without guardrails.

There are four screens, and you name each one by a single word. The first runs inside fettle with nothing installed. The other three run a model you fetch, through burler, a second program that comes in its own archive.

ScreenWhat it catchesWhat does the checking
identifiersSocial security numbers, valid card numbers, formatted phone numbers, email addresses, labelled record numbers and labelled dates of birthSix patterns inside fettle. Nothing to install, and no model.
clinicalPatient names, dates and record numbers in clinical text, which no pattern findsA de-identification model from the BioBERT or ClinicalBERT family, run by burler
legalContract clauses and consumer-report contentA contract-clause model trained on CUAD, or LegalBERT, run by burler
scientificNamed entities in research and scientific dataA SciBERT model trained to find entities, run by burler

No model ships with Fettler, and none is downloaded for you. Choose the models says what to look for and what each licence allows.

Read do you need this before anything else. The screen refuses whole answers, so in a tree it does not belong in, it refuses a great deal of ordinary work. People get out of that by turning it off, which leaves them worse off than never turning it on. Point it at the tree that holds the records, and at nothing else.

Here is the path one read takes with the screen on. The patterns run first and the models second. One detection by either refuses the whole answer, and a clean answer is served as it would be at any other level.

The screenings level. A read from a screened tree passes the tier one patterns, then the models burler runs. One detection refuses the whole answer with exit 13. A clean answer is served.
The screenings level. A read from a screened tree passes the tier one patterns first, then the models that burler runs. One detection refuses the whole answer with exit 13, and a clean answer is served.

Turning it on is two steps, and two commands confirm them.

# 1. decide whether you need it at all             <- most trees do not
# 2. add a "screenings" section to .fettler.json      <- works on the next call
fettle roots                                        # confirm what is screened
fettle read one-of-the-records.pdf                  # confirm it refuses

For most people who want the screen, those two steps are all of it. Names, addresses, conditions and free text need a model, which is a second program and a download you fetch yourself. The models, in the second half of this page, covers that part. Nothing in the first half depends on it.

Do you need this?

Turn it on when a tree holds records about people, and a person outside your team could read the transcript. Fettler reads PDF, Word and Excel files, which are the formats regulated data lives in. What it returns goes into the assistant's context, then into a transcript, a log, and wherever those are sent. The screen sits between the file and that chain.

Your tree holdsTurn it on?
Patient notes, discharge summaries, lab resultsYes. This is the case the screen was built for.
Customer records with names, addresses, card or account numbersYes, with identifiers at least, which catches the card numbers. The names and addresses need a model. See the categories.
Signed contracts, credit files, student recordsProbably. Read what it does not do first, because two of those three kinds of record are not covered categories.
Source code, build scripts, documentationNo. The email-address detector alone will refuse a great deal of it. See the note below.
Anonymised or made-up dataNo. If it sits inside a screened tree, give its folder a screen of false. Carving out a folder shows how.

Note Do not screen a source tree to see what happens. One of the patterns matches any email address, and source trees hold many of them, in comments, test files and package details. Every read that touches one will refuse. That is the detector working correctly, in a tree nobody meant to screen.

What gets screened

The screen judges the two verbs that hand content back, read and search. No other verb returns file content, so no other verb is screened. find returns paths, roots returns your own configuration, and extract writes files inside the tree rather than sending them out.

The screen judges what is being sent, not the whole file. A document may hold a patient's details on page 40. If you read page 2 and it discloses nothing, page 2 is sent. Judging the whole file would lock every useful tree, and people would turn the screen off to get out of that.

A search is judged one file at a time. The screen sees the matched lines plus whatever context you asked for, which is exactly what you would have seen.

Turn on the first tier

The first tier needs nothing installed, and it works on the next call. Add a screenings section with a screen entry that names the tree holding the records:

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

Each key in screen is a tree name, or a tree name, a slash and a scope. A scope is a folder inside a tree that the guardrails section declares. The name must be a declared tree or scope, or else the file is refused. With guardrails off, the only tree is work, the launch folder, so "screen": { "work": true } is the one key you can write.

There are four ways to write a value, and one mixed form that is refused:

You writeIt screens
trueAll four categories. Forgetting to name one cannot happen this way.
["identifiers", "clinical"]Only those two. A list of plain words includes what it names.
["-scientific"]Everything except scientific. A list of words that start with a minus sign excludes what it names.
falseNothing. A tree that no key names gets this too.
["identifiers", "-scientific"]Refused when the file is read. It could mean identifiers only, or everything but scientific, and those differ by two whole categories. fettle does not guess which you meant.

A misspelt category is refused too. If "lgal" were accepted, it would quietly remove a screen, and you would not find out until data left a tree you believed was covered.

Note Three words that name a regulation are refused, and the message says what to write instead. phi, pii and sci each promise coverage that arrives only once a model is installed. They are refused rather than translated, because a translation would keep them alive in configurations for good.

RefusedWrite instead
phiidentifiers for the labelled patterns, and clinical for model screening of clinical text
piiidentifiers
sciscientific

A screen written inside a tree entry is refused. In the last release a screen sat inside the tree it screened. Now it lives only in the screenings section, so you can see whether a project screens anything by reading one place. The refusal shows where it goes, as {"screenings":{"screen":{"records":true}}}. A file written entirely in the old flat shape is refused too, and the message holds its entries rewritten under the three sections, ready to paste.

The four categories

WordConcernWhat does the checking
identifiersIdentifiers with a fixed shape, whoever they belong toSix patterns that run inside fettle. It is the only category that works with nothing installed, and it never uses a model.
clinicalClinical text, which United States health privacy law (HIPAA) coversA model you install, and nothing until you do. Look for a de-identification model from the BioBERT or ClinicalBERT family.
legalContract and consumer-report contentA model you install, and nothing until you do. Look for a contract-clause model trained on CUAD, or LegalBERT.
scientificResearch and scientific dataA model you install, and nothing until you do. Look for a SciBERT model trained to find entities.
Out of the box, the screen catches identifiers with a fixed shape or a label, and nothing else. That means a social security number, a valid card number, a formatted phone number, an email address, a labelled record number and a labelled date of birth. Names, addresses, conditions and free text are caught only by a model you install. A tree that names clinical, legal or scientific with no models directory is screened in name only, and roots says so.

What the patterns catch

These patterns run inside fettle itself, with no model, no second program and no download.

DetectorIt matches
ssnA United States social security number, written with hyphens, in a range that is actually issued.
credit-card13 to 19 digits, written in groups the way cards are printed, that pass the card number's check digit.
phoneA phone number that starts with a plus sign, or one written in separated groups.
emailAn email address, written in the usual form of a name, an at sign and a domain.
medical-record-numberAn identifier that comes after its own label, such as MRN, patient id or medical record no.
date-of-birthA date that comes after its own label, such as DOB, date of birth or born on.

All six belong to one category, identifiers. Each one is an identifier with a shape somebody fixed, or a label announcing it. None of them reads clinical language, so a refusal from any of them reports as identifiers.

The patterns are narrow on purpose, and two of them depend on a label. No two hospitals agree on the shape of a record number, so the reliable thing to match is the label in front of it. Matching any number of the right length would refuse every part number in every spreadsheet, and a check that refuses too often is a check somebody turns off. So nine digits with no hyphens do not match as a social security number, because they are an order number far more often.

Carving out a folder that does not need it

A scope's screen replaces the tree's screen, in the same way its can list does. That is what lets one folder take the screen away. Declare the folder as a scope under guardrails, then give it its own key:

{
  "guardrails": {
    "trees": {
      "records": {
        "path": "../records",
        "can": ["list", "read"],
        "scopes": {
          "anonymised": { "can": ["list", "read"] },
          "exports":    { "can": ["list", "read"] }
        }
      }
    }
  },
  "screenings": {
    "screen": {
      "records":            true,
      "records/anonymised": false,
      "records/exports":    ["identifiers"]
    }
  }
}

Run fettle setup screenings

fettle setup screenings <client> [--local] [--dry-run]

setup screenings checks what the screen needs, and writes a starter section if the file has none. It checks that burler is in the same folder as fettle. It checks the models directory, if the file names one. Then, at --local, it writes a starter screenings section that screens the default tree for every category.

$ fettle setup screenings claude-code --local
create, with a "screenings" section: C:\github\app\.fettler.json
  screening the tree 'work' for every category
burler is beside fettle at C:\tools\fettle\burler.exe
no models directory is named, so only identifiers are screened. Name one as "models" under "screenings"

Edit the starter section before you rely on it. It screens the default tree, which is often a source tree, and a source tree is the case the note above warns about. Change the key to the tree or scope that holds the records. Setup never changes a screenings section that already exists. At --global it writes no project file, and it tells you to run it with --local in each project that wants screenings.

Check it, then prove it

Ask for the roots first. On the left is a whole file that screens a records tree and carves out one folder. On the right is what roots says with it in place.

// C:\github\app\.fettler.json
{
  "guardrails": {
    "trees": {
      "work": { "path": "." },
      "records": {
        "path": "../records",
        "can": ["list", "read"],
        "scopes": {
          "anonymised": {
            "can": ["list", "read"]
          }
        }
      }
    }
  },
  "screenings": {
    "screen": {
      "records": ["identifiers",
                  "clinical"],
      "records/anonymised": false
    }
  }
}
$ fettle roots
levels
  tools         on, always
  guardrails    on, from C:\github\app\.fettler.json
  screenings    on, from C:\github\app\.fettler.json

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

screens
records             identifiers clinical
records/anonymised  nothing

no "models" directory is declared, so the clinical, legal, scientific categories screen nothing. Only identifiers are screened

found by fettle
scratch       C:\Users\you\AppData\Local\Temp\claude\...\scratchpad
              ...

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

The screens heading lists every tree and scope that a key names. A scope with no line of its own is inheriting the tree's screen. The heading appears only when something is screened or a models directory is named.

Read the line under the screens before you believe the screens. The tree says it screens clinical, but a model judges clinical, and none is installed yet. So right now it screens nothing, and roots says so. Once you install a model, roots names it instead, as naming the directory shows.

Now read a file you know holds an identifier:

$ fettle read records:ward-round.docx
refused: this response would disclose regulated data: 2 in identifiers. The matches are
not quoted, because the refusal would then disclose them. Read a narrower range, or
take the screen off this scope if the content is not regulated.
$ echo $LASTEXITCODE
13

The refusal gives a category and a count, and never the text itself. A refusal that named what it found would write it into the log and the transcript, which is the harm the screen exists to prevent. Every screening refusal exits with 13, the outcome screened.

The two findings are the labelled MRN: line and the labelled DOB: line. That is why the count is two and the category is identifiers. Nothing read the clinical text around them, because no model is installed.

If the first tier is all you need, stop here. It is real protection with no new program, no download and no second process. It is also the whole of what the screen catches until you install a model. The models is the only way clinical, legal and scientific ever screen anything.

Prove it refuses

A correct configuration and a screen that holds are two different claims, and only the second one matters. Ask the assistant to do each of these, and watch what comes back.

Ask the assistant to…What a working screen does
ask for the rootsThe screenings line says on. The screens heading names your categories, and your carved-out scope shows nothing.
read a file holding a social security number or a card numberRefused, exit 13, with a count in identifiers and never the number.
read a file with a date of birth after its own label, such as DOB:Refused, with a count in identifiers.
read the same file from the carved-out scopeServed, because a scope's screen replaces the tree's.
search the tree for a common word that appears near an identifierRefused, because the matched lines and their context are what would have been sent.
read an image in a screened scopeRefused, because nothing here reads pictures, and a photographed record is the case this exists for.
read an ordinary file with no identifiers in itThe file is served. If this one refuses, the screen is on a tree it does not belong on.

When it refuses and you did not expect it

Every refusal names its own cause in its first few words. They say whether something was found, or whether the screen could not run. Those are two different problems with two different fixes.

It saysDo this
this response would disclose regulated data:The screen worked. Read a narrower range, or take the screen off that scope if the content is not regulated. The count says how many it found, and the category says which kind.
this response could not be screened, so it is not served:The screen broke. One of the rows below follows. Nothing was checked, so nothing is served.
… burler, the screening sidecar, is not installedburler is not in the same folder as fettle, and the message says where it looked. Redo the burler install, or take the screen off that tree.
… would not start:The file is there and will not start. Usually it is the wrong platform's archive, or the file has no permission to run. On macOS, check the quarantine mark.
… refused: and then burler's own wordsburler declined and said why: a category with no folder, a model that would not load, or a manifest with a missing field. Drive it by hand to see the whole message.
… did not answer within 30 secondsburler is running and stuck. fettle stops it, so the next read starts a fresh one. A very large payload on a slow machine can cause this.
… failed: and then it said:burler started and then stopped, and its last words are quoted. Those words are usually the real answer.
… answered something that is not JSON, or a similar lineThe two programs did not understand each other. Almost always that means a fettle and a burler from different releases, so unpack matching versions.
this scope is screened, and the screen cannot read an imageThis is expected, because nothing here reads pictures. Move the image to a scope with no screen if it holds nothing regulated.

When it refuses, on the reference page, lists every message the screen can produce.

Why every one of these refuses

Sending content because the check broke is the one outcome this must never produce. A burler that will not start, a model that will not load, an answer nobody can read and a check that ran out of time all refuse, and each says which. A fault nobody planned for becomes a refusal too. That looks like an outage, and it is the intended trade, because a screen that quietly stopped screening would be worse.

What it costs

WhenWhat happens
Every screened readThe patterns run inside fettle. Each one has a limit of one second, and in practice they take too little time to measure.
The first screened read after a quiet spellburler starts and loads a model, which takes a few seconds. fettle allows up to 90 seconds for this, because a timeout would refuse an answer that was going to be served.
Every read after thatTens of milliseconds, because burler stays running. After 30 seconds with no answer, something is wrong rather than slow.
Five minutes with nothing to screenburler is stopped and its memory released. A tool open in an editor all day should not hold hundreds of megabytes for one document.
The same content twiceAnswered from memory. The last 256 verdicts are kept, matched on the content and on the manifests of the models that judged it.

What it does not do

The screen is a safety net, not a boundary. Models miss entities, and patterns miss anything written in a way they do not expect. A number split across two lines gets past every one of them. A clean verdict is evidence that nothing is there, never proof of it. Anyone who believes a screened tree cannot leak has been misled, and that belief is the real hazard.

What it stops is the ordinary accident: a regulated document read into a transcript by an assistant that was only trying to help. It is not built to stop a person who is trying to get the data out.

It does notBecause
Cover credit reports or student records as suchThose records are mostly identifiers with a fixed shape, and identifiers catches those. That is all it does. Nothing here makes a project meet the laws that cover those records.
Offer a sensitivity settingAny detection refuses. A setting would get turned down until the check was off.
Read imagesNothing here reads pictures, so an image in a screened scope is refused rather than sent unchecked.
Hide or blank out parts of an answerIt refuses the whole answer. A document with holes in it would look like a safe copy of the original, and it would not be one.
Screen what you write into a treeThat is the credential check, which judges what a write adds. When it refuses covers it.
Stop anything outside FettlerA shell command, an allowed built-in reader, or a person with the file open reads it as normal. fettle doctor is what finds those routes.

The models

Note None of this second half is needed for the first tier. identifiers runs inside fettle with nothing installed. This half covers the other three categories, clinical, legal and scientific, and each of them needs a model.

A model here is a trained program that reads text and marks the words that name something. It might mark a person, a date or a record number. Three things make the second tier work: burler, the program that runs the models; the model files you fetch; and a models entry that says where those files are.

Install burler, the model host

burler is a second program, in its own archive. In a woollen mill, the burler was the worker who picked the faults out of finished cloth before it left the mill. This program does the same job for answers leaving a tree. fettle talks to it over a pipe, and the two share no code.

It is separate because of what the models need. Running them needs ONNX Runtime, a library with separate native files for each operating system. fettle is built so that it stays one file you copy onto a machine, and those native files would break that. Folding the models in would also make every download pay for a feature most trees never use.

Unpack burler into the same folder as fettle. That is the only place fettle looks for it, and it never searches PATH. A program that decides what may leave your tree should not be one an environment variable can swap out.
Windows · PowerShell
$dir = "$env:LOCALAPPDATA\Programs\fettle"   # wherever fettle.exe already is
Expand-Archive burler-X.Y.Z-win-x64.zip -DestinationPath $dir -Force
Get-ChildItem $dir -Recurse | Unblock-File
Get-ChildItem $dir\burler.exe                # it should be beside fettle.exe
macOS and Linux
tar -xzf burler-X.Y.Z-osx-arm64.tar.gz -C ~/.local/bin
xattr -d com.apple.quarantine ~/.local/bin/burler   # macOS only
ls -l ~/.local/bin/burler                           # beside fettle

burler is published for the same four platforms as fettle: win-x64, linux-x64, osx-x64 and osx-arm64. Take the one that matches the fettle you installed, because the two run as a pair on one machine.

Nothing happens yet, because burler with no models does nothing, and fettle does not start it until a models directory is named. Installing it early does no harm, and fettle setup screenings confirms where it is.

Choose the models

Nothing here ships a model, and nothing here downloads one for you. The models come to a few hundred megabytes, and fettle's own download is a few megabytes. Several of the useful models also carry terms that would forbid this project passing them on. So you fetch them, you read their licence, and you decide.

What kind of model this needs

It needs a model that labels each word of a sentence. It is handed a piece of text and answers which words are a person, a date, an identifier, and so on. burler reads those labels, works out where each entity starts and stops, and reports how many it found.

A base model is not one of these, so check before you download. BioBERT and SciBERT are base models, which understand clinical or scientific language but have not been taught to label anything. What you need is a model that somebody has trained further on a labelling task, such as removing patient details from clinical notes. Look for a model card that names a task, not only a subject.

Any model in the BERT family works, as long as it labels words and can be exported to the ONNX format. Nothing here is tied to one model.

Where to start looking

CategoryFamilyWhat to look for
clinicalBioBERT, ClinicalBERTA model trained to find patient details. The i2b2 and n2c2 research datasets are the usual training data, and such models label names, dates and record numbers that no pattern here finds.
scientificSciBERTA model trained to find entities in scientific text, rather than the base model.
legalA general model with an open licence, or LegalBERTContract-clause models trained on CUAD are the readiest. LegalBERT is stronger, and its licence has a condition, which the table below explains.

There is no row for identifiers, and there cannot be. It is the pattern tier, so it takes no model and no folder. If you want names of people and places caught, that is a model under one of the three categories above.

Read the licence at the source, every time

Read the licence before you adopt a model. Some clinical models carry data-use terms that forbid passing them on, and a models folder you share with a colleague is passing them on. Two rules make the rest simple:

These were checked against the model cards and source repositories on 20 August 2026. Check again before you rely on them, because terms change, and this table is a starting point rather than legal advice.

ModelWhat was foundWhat that means for you
BioBERTApache-2.0, in the licence file of dmis-lab/biobert on GitHub. The copies on model hosts carry no tag.Usable. Record the GitHub repository as provenance, not the copy you downloaded.
SciBERTApache-2.0, on allenai/scibert, with the same untagged copies.Usable, with the same provenance rule.
ClinicalBERT, and the i2b2 models trained to find patient detailsTagged MIT by the groups that held the agreements for the underlying data.Usable on that grant. Record the revision as well as the source, because the grant covers what they published.
LegalBERTCC-BY-SA-4.0, a licence that passes its own terms on to anything built from it.Usable as your own add-on. Do not train a new model from it, because its terms would carry into whatever you build.
Anything with no licence anywherenoneDo not adopt it, however good its scores are.

Prepare them

burler runs models in the ONNX format, so you convert each model once. ONNX is a portable way of saving a trained model, so that programs other than the one that trained it can run it. Most published models are not in that format yet.

Export to ONNX

Hugging Face's optimum tool does the conversion in one command. The task name matters, because token-classification is the name for a model that labels each word.

pip install optimum[onnxruntime]
optimum-cli export onnx --model ORG/CHECKPOINT --task token-classification out/

Shrinking the model afterwards is optional, and it pays. This step, called quantising, stores the model's numbers with less precision, typically 8 bits instead of 32. It cuts the file to about a quarter of its size and makes it several times faster, at a small cost in accuracy. optimum can do this too, and the sizes on this page assume you did.

Lay the files out

Give each category its own folder, named for the category, under one parent folder:

screening-models/
  clinical/
    model.onnx
    vocab.txt
    labels.json
    manifest.json
  legal/
    model.onnx
    vocab.txt
    labels.json
    manifest.json

Only the categories you screen need a folder. A category that no screen names is never looked for, and identifiers never needs a folder.

FileWhat it is
model.onnxThe exported model.
vocab.txtThe list of word pieces the model was trained on. Take the one from the same model, because a list from another model produces confident nonsense.
labels.jsonA JSON array of label names, one per class, in order. The position in the array is the class number. An object that maps numbers to names is refused, because a gap in the numbers means a broken export.
manifest.jsonWhat the model says about itself. It is required, and the next section describes it.

A folder missing any of the first three files is refused by name, so you learn which file is absent.

Write the manifest

The manifest has four fields, and each one must be a string that is not empty:

{
  "checkpoint": "obi/deid_roberta_i2b2",
  "revision":   "a1b2c3d",
  "licence":    "MIT",
  "provenance": "https://huggingface.co/obi/deid_roberta_i2b2"
}

Name the directory

Name the models directory as models under screenings, in the personal overlay or the machine file. Where several hundred megabytes of models sit is a fact about one disk, not about the project. So keep it out of the checked-in file.

// .fettler.local.json - beside .fettler.json, kept out of source control
{
  "screenings": {
    "models": "C:/models/screening"
  }
}

With the directory named and a model in it, roots names the model that does the judging:

$ fettle roots
levels
  tools         on, always
  guardrails    on, from C:\github\app\.fettler.json
  screenings    on, from C:\github\app\.fettler.json and C:\github\app\.fettler.local.json
...
screens
records             identifiers clinical
                    clinical: obi/deid_roberta_i2b2 @ a1b2c3d
records/anonymised  nothing
models              C:\models\screening
...
declared by C:\github\app\.fettler.json (with C:\github\app\.fettler.local.json)

Each category line gives the model and its revision, read from the manifest. A category whose folder holds no model says no model installed; reads here are refused, which is the state to catch before a read does. The thing doing the judging is always named, never hidden behind the category word.

doctor reports the same facts in its screenings section, one line per category:

screenings  on, from C:\github\app\.fettler.json and C:\github\app\.fettler.local.json
  burler: C:\tools\fettle\burler.exe
  models: C:\models\screening
  clinical: obi/deid_roberta_i2b2 @ a1b2c3d
  legal: no model installed
  scientific: no model installed
Naming the directory puts the models to work, and it changes what a failure means. Before, the patterns are the whole screen, and a clean answer is served. After, a screened category with no model refuses every read, because somebody has now said they expect the model to be there.

That is why fettle does not check that the directory exists when it reads the file. A file opened on a machine without the models is not broken, and refusing the whole file would take the tree away rather than the screen. The refusal comes at the read instead, where it can name the category that wanted the model.

Then read something. The first screened read starts burler and loads a model, which takes a few seconds. Every read after that takes tens of milliseconds.

Driving burler by hand

When you doubt the model tier, run burler yourself. It reads one request per line and answers with one line:

$ burler --models C:/models/screening
{"op":"screen","categories":["clinical"],"payload":"the weather was fine"}
{"ok":true,"findings":[]}

Type the request line, press Enter, and read the answer.

What comes backWhat it tells you
{"ok":true,"findings":[]}Everything works. burler loaded the model, ran it, and found nothing in that sentence.
{"ok":true,"findings":[{"category":"clinical","count":1,…burler found something. Try a sentence with a name or a date in it to see this.
{"ok":false,"error":"'clinical' is screened and there is no model for it…"}The models directory has no clinical folder, and the message names the path it looked at.
{"ok":false,"error":"the model at … would not load…"}The file is there, and it is not a model burler can run. Usually the export went wrong, or the download did not finish.
Nothing at all, and it exitsRead the error output. Started with no --models, burler says it needs a directory naming where the models are, and exits with 2.

An empty findings array means burler checked and found nothing. A missing one is a fault. The two are never written the same way, on either side of the pipe.

Optional extras

More than one model in a category

Give the category a folder of folders instead of one model:

screening-models/
  legal/
    cuad-clauses/       model.onnx, vocab.txt, labels.json, manifest.json
    legalbert-ner/      model.onnx, vocab.txt, labels.json, manifest.json

Their findings add together. A detection by any of them refuses, so a second model can only make the screen stricter. That lets you run a model with an open licence alongside a stronger add-on, rather than choosing one.

burler decides which shape a category has by looking for model.onnx directly inside its folder. If the file is there, that is the one model. If not, burler loads every folder inside that holds one.

A shared models directory

models may name a network share, and for a team that saves each person the download. Know what you are trusting when you do:

Where the rest of it is written down

You wantPage
To declare trees and scopes, which a screen can nameGuardrails
Every exit code, including 13Exit codes
What Fettler defends against, and what it does notThe reference