Shoddy · Fettler
Stop a regulated document leaving a tree by accident. The first tier needs nothing installed and one line of configuration.
This page assumes Fettler is installed and turned on in a project. That is installing it and in a project, in that order. The screen is the last thing you switch on, and most projects never switch it on at all.
# 1. decide whether you need it at all <- most trees do not
# 2. add "screen" to the tree in .fettler.json <- works at once
# restart the assistant, and test a refusal
fettle roots # confirm what is screened
fettle read one-of-the-records.pdf # confirm it refusesThat is the whole feature for most people who want it. Names, addresses, conditions and free text need a model, which is a second program and a download you fetch yourself — installing the models is that half, and nothing on this page depends on it.
Switch 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 — the formats regulated data actually lives in. What it returns goes into an assistant's context, then into a transcript, a log, and whatever ships those onward. The screen sits between the file and that chain.
| Your tree holds | Switch it on? |
|---|---|
| Patient notes, discharge summaries, lab results | Yes. This is the case the screen was built for. |
| Customer records with names, addresses, card or account numbers | Yes, with identifiers at least — which catches the card and account numbers. The names and addresses need a model; see the categories. |
| Signed contracts, credit files, student records | Probably. Read the caveat in step 11 first — two of those three regimes are not covered categories. |
| Source code, build scripts, documentation | No. The email-address detector alone will refuse a great deal of it. See the warning below. |
| Anonymised or synthetic data | No — but if it sits inside a screened tree, give it a scope that turns the screen off. Step 2 shows how. |
Note Do not put
"screen": true on a source tree to see what happens. One
of the patterns matches any email address, and source trees are full of
them — in comments, in test fixtures, in package metadata. Every
read that touches one will refuse. That is the detector working
correctly, in a tree nobody meant to screen.
The two commands that hand content back: read and
search. Nothing else returns file content, so nothing
else is screened. find returns paths, roots
returns your own configuration, and extract writes files
inside the tree rather than sending them out.
It judges what is being sent, not what is in the file. A document may hold a patient's details on page 40. Read page 2, and page 2 is sent, because page 2 discloses nothing. Judging the whole file would lock every useful tree, and the way out of that is to turn the screen off.
A search is judged one file at a time, as the matched
lines plus whatever context brings with them. The screen
sees exactly what you would have seen.
This needs nothing installed and works the moment you restart the
assistant. Add "screen" to the tree that holds the
records, in .fettler.json:
{
"trees": {
"work": { "path": "." },
"records": {
"path": "../records",
"can": ["list", "read"],
"screen": ["identifiers", "clinical"]
}
}
}Three ways to write it, and one that is refused:
| You write | It screens |
|---|---|
"screen": true | All four categories. Forgetting to name one cannot go wrong this way. |
"screen": ["identifiers", "clinical"] | Only those two. A bare list includes. |
"screen": ["-scientific"] | Everything except scientific. A list of - words excludes. |
"screen": false | Nothing. This is also what a tree that says nothing gets. |
"screen": ["identifiers", "-scientific"] | Refused when the file is read. It reads either as "identifiers only" or as "everything but scientific", and those differ by two whole categories. Nothing here guesses which you meant. |
A misspelt category is refused too. "lgal" would quietly
subtract a screen, and you would not find out until data left a
tree you believed was covered.
Note Three
regulation-shaped words are refused outright, and the refusal says
what to write instead. phi, pii and
sci name a regulation rather than what actually
runs, so each promises coverage that only arrives once a model is
installed while reading as though it were there from the start. They are
refused rather than quietly translated, because a translation would keep
them alive in configurations indefinitely, still over-promising.
| Refused | Write instead |
|---|---|
phi | identifiers for the labelled patterns, clinical for model screening of clinical text — it does duty for both |
pii | identifiers |
sci | scientific |
| Word | Concern | What carries it | Which model |
|---|---|---|---|
identifiers | Structured identifiers, whoever they belong to | Six patterns, running inside fettle. The only category that works with nothing installed. | None, ever. It never crosses the sidecar's pipe and never asks for a model. |
clinical | Clinical text — the HIPAA concern | A model you install, and nothing until you do. | A de-identification fine-tune from the BioBERT or ClinicalBERT family. The manifest names the exact checkpoint and revision, and fettle roots reports it back to you. |
legal | Contract and consumer-report content | A model you install, and nothing until you do. | A contract-clause model trained on CUAD, or LegalBERT. Named in the manifest, reported by roots. |
scientific | Research and scientific data | A model you install, and nothing until you do. | A SciBERT entity-recognition fine-tune. Named in the manifest, reported by roots. |
clinical, legal and
scientific screen nothing until you install a model.
None of the three has a pattern behind it. A tree that names one of them
and has no models directory is screened in name only. fettle
roots says so in as many words, and step 6 is
the act that changes it.roots name exactly which model that is. A tree screening
identifiers alone is protected against the first list and
nothing beyond it.These run inside fettle itself. There is no model, no
second program, and no new download.
| Detector | Category | It matches |
|---|---|---|
ssn | identifiers | A US social security number, written with hyphens, in a range the Social Security Administration actually issues. |
credit-card | identifiers | 13 to 19 digits, written in groups the way cards are printed, that pass the Luhn check digit. |
phone | identifiers | A number with a leading +, or one written in separated groups. |
email | identifiers | An email address. |
medical-record-number | identifiers | An identifier introduced by its own label — MRN, patient id, medical record no. |
date-of-birth | identifiers | A date introduced by its own label — DOB, date of birth, born on. |
All six are one category, and that is the point of the name.
They are all identifiers with a shape somebody fixed or a label
announcing them. None of them reads clinical language, and a refusal
from any of them reports as identifiers — never as a
judgement about what kind of document it came from.
They are narrow on purpose, and two of them are deliberately label-driven. No two hospitals agree on the shape of a record number, so the only honest thing to match is the label that introduces it. Matching bare identifier-shaped text would refuse every part number in every spreadsheet — and a check that cries wolf is a check somebody switches off.
Nine bare digits are an order number far more often than a social security number, so nine bare digits do not match. Neither do ten bare digits as a phone number, nor sixteen bare digits as a card.
A scope's screen replaces the tree's, exactly as
its can does. That is what lets a scope take screening
away:
{
"trees": {
"records": {
"path": "../records",
"can": ["list", "read"],
"screen": true,
"scopes": {
"anonymised": { "can": ["list", "read"], "screen": false },
"exports": { "can": ["list", "read"], "screen": ["identifiers"] }
}
}
}
}screen
inherits. Every scope anybody wrote before this feature existed says
nothing, and reading that as "screen nothing here" would punch a hole in
the first tree anyone switched it on for.screen is not a permission. The seven permissions
are named after what happens to a file. A screen is a property of
content leaving the boundary, and it is not a verb.Restart the assistant first. The server reads
.fettler.json when it launches and does not re-read it, so
a screen you just added is not on yet. Then ask for the roots:
$ fettle roots
work /work/shoddy (default)
can: list read create update rename delete
records /work/records
can: list read
screen: identifiers clinical
anonymised can: list read screen: nothing
no "models" directory is declared, so clinical, legal, scientific screen nothing here - only the identifier patterns run
declared by /work/shoddy/.fettler.jsonThe screen line appears only where a screen is on, and a scope shows one only where the scope itself states one. A scope with no screen line is inheriting.
clinical, and
clinical is judged by a model that is not installed yet, so
right now it screens nothing at all. roots says so rather
than letting the word imply otherwise — and once you install one
it names the checkpoint instead. That is step 6.Now read a file you know holds an identifier:
$ fettle read ward-round.docx
refused: this response would disclose regulated data: 2 in identifiers. What was found is
deliberately not quoted back, because a refusal that named it would put it in the
log and the transcript, which is the disclosure being refused. Read a narrower
range, or take the screen off this scope if the content is not what it looks like.
$ echo $LASTEXITCODE
13A 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
— the exact harm the screen exists to prevent, delivered by the
screen. Every screening refusal answers 13.
The two findings here are the labelled MRN: line and the
labelled DOB: line, which is why the count is two and the
category is identifiers. The clinical prose around them was
not read by anything — no model is installed.
clinical, legal and scientific
ever screen anything.clinical,
legal and scientific screen nothing at all
until one is installed, and fettle roots says so rather than
letting the category name imply otherwise.
Installing the models →The configuration being right and the screen holding are two different claims, and only the second 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 roots | the screened tree carries a screen line naming the categories, and your carve-out scope shows screen: nothing |
| read a file holding a social security number or a card number | refused, exit 13, naming a count in identifiers and never the number |
read a file carrying a date of birth under its own label — the literal word DOB, a colon, and a date | refused, a count in identifiers |
| read the same file from the unscreened scope | served — a scope's screen replaces the tree's |
| search the tree for a common word that appears near an identifier | refused, because the matched lines and their context are what would have been sent |
| read an image in a screened scope | refused — nothing here reads pixels, and a photographed record is exactly what this is for |
| read an ordinary file with no identifiers in it | served. If this one refuses, the screen is on a tree it does not belong on. |
When the model tier is the part in doubt, run burler yourself. It reads one request per line and answers one line:
$ burler --models C:/models/screening
{"op":"screen","categories":["clinical"],"payload":"the weather was fine"}
{"ok":true,"findings":[]}Type the request line, press return, and read the answer.
| What comes back | What it tells you |
|---|---|
{"ok":true,"findings":[]} | Everything works. It loaded the model, ran it, and found nothing in that sentence. |
{"ok":true,"findings":[{"category":"clinical","count":1,… | It found something. Try a payload 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. The message names the path it looked at. |
{"ok":false,"error":"the model at … would not load…"} | The file is there and is not a model burler can run. Usually a bad export, or a file that did not finish downloading. |
| Nothing at all, and it exits | Check the message on the error stream. Started with no --models, it says burler needs --models DIRECTORY, naming where the screening models are. and exits 2. |
An empty findings array means it checked and found
nothing. A missing one is a fault. The two are never spelled the
same way, on either side of the pipe.
Every refusal names its own cause. Read the first few words: they say whether something was found, or whether the screen could not run. Those are different problems.
| It says | Do this |
|---|---|
this response would disclose regulated data: |
Working as intended. Read a narrower range, or take the screen off that scope if the content is not what it looks like. The count tells you how many entities, and the category tells you which kind. |
this response could not be screened, so it is not being served: |
The screen broke. What follows is one of the rows below. Nothing was checked, so nothing is served. |
… the screening sidecar is not installed |
burler is not beside fettle. The message says where it looked. Redo step 3, or take "screen" off the scope. |
… would not start: |
The file is there and will not launch. Usually the wrong platform archive, or no permission to execute it. On macOS, the quarantine attribute. |
… refused: and then burler's own words |
burler declined and said why — a category with no directory, a model that would not load, a manifest missing a field. Its message names the file. Drive it by hand to see the whole thing. |
… did not answer within 30 seconds |
It is alive and stuck. The child is killed, so the next read pays a fresh start rather than reusing a process nobody can account for. A payload far larger than a page can do this on a slow machine. |
… failed: and then it said: |
It started and then died, and whatever it printed on the way out is quoted. That is usually the real answer. |
… answered something that is not JSON, or four similar |
The pipe carried something that is not the protocol. Almost always a fettle and a burler from different releases. Unpack matching versions. |
this scope is screened, and this is an image |
Expected. Nothing here reads pixels. Move the image to an unscreened scope if it genuinely holds nothing. |
The reference page lists every message the screen can produce, with what each one means.
Serving content because the check broke is the one outcome this must never produce. A sidecar that will not start, a model that will not load, an answer nobody can parse and an inference that outran its clock all refuse and say which. There is no path through it that fails any other way — including a fault nobody anticipated, which becomes a refusal too.
It reads as an outage, and that is the intended trade. A screen that quietly stopped screening would be worse in exactly the way that matters.
| When | What happens |
|---|---|
| Every screened read, always | The patterns run in fettle itself. Each has a one-second ceiling, and in practice they are not measurable. |
| The first screened read after a quiet spell | burler starts and loads a model. Seconds, and the wait is allowed up to 90 of them, because a timeout here would deny a disclosure that was going to be served. |
| Every read after that | Tens of milliseconds. The child is held warm. Thirty seconds is the point at which something is wrong rather than slow. |
| Five minutes with nothing to screen | The child is killed and its memory released. A tool sitting in an editor all day should not hold hundreds of megabytes for a document somebody read at breakfast. |
| The same content twice | Answered from memory. The last 256 verdicts are kept, keyed on the content and on the manifests of the models that judged it. |
clinical loads one model, not three — and a tree
screening only identifiers loads none and never starts
burler at all.What it stops is the ordinary accident: a regulated document read into a transcript by something that was only trying to help. It is not built to withstand anybody trying.
| It does not | Because |
|---|---|
| Cover FCRA or FERPA | Consumer-report and education-record data is largely structured identifiers, and those are caught under identifiers by the patterns. That is all. Nothing here is "FERPA compliance", and reading it that way is the misunderstanding this sentence exists to prevent. |
| Offer a confidence setting | Any detection refuses. A dial gets turned until the check is off. |
| Read images | Nothing here reads pixels, so an image in a screened scope is refused rather than served unchecked. |
| Redact, mask or edit | It refuses the whole response. Handing back a document with holes in it would look like a safe copy of the original, and would not be one. |
| Screen what you write into a tree | That is the credential net, which judges what a write adds. The refusals section covers it. |
| Stop anything outside Fettler | A shell command, an allowed built-in reader, or a person with the file open reads it as normal. The screen sits on Fettler's answers, and only there. fettle doctor is what finds the ways round. |
| You want | Page |
|---|---|
| Installing the models the second tier needs | Screening: the models |
| Declaring trees, scopes and tasks in the first place | In a project |
Every exit code, including 13 | The reference |
| What Fettler defends against, and what it does not | The reference |