Shoddy · Picker

Screenings

The third level. Stop regulated rows leaving a database by accident. The first tier needs nothing installed, and the burler checks the rest before they leave.

This page describes the screenings level. It is off until .picker.json has a screenings section. With it on, pick checks every answer for regulated data before the answer leaves. Regulated data means facts about people that a law protects, such as a social security number or a medical record, and one match refuses the whole answer with exit code 13.

Most projects never turn this level on. The guardrails and the login already decide what can be asked for, and the screen is for the few databases that hold records about people.

The pick screenings level. A screenings section screens the database Sales. The rows of one SELECT pass the tier one patterns, which run inside pick, and then the models that burler runs. A clean answer is returned. One detection by either refuses the whole answer with exit 13. With no models directory named, tier one is the whole screen.
The screenings level. The rows pass two tiers: the patterns, which run inside pick, and the models, which run in burler, a second program. One detection by either refuses the whole answer with exit 13.
Read Do you need this? before you turn it on. The screen refuses whole answers, so on a database it does not belong on, it refuses a great deal of ordinary work, starting with every customer table that has an email column. People escape that by turning it off, which leaves you worse off than never turning it on.

Do you need this?

Turn it on when a database holds records about people, and someone outside your team could read the transcript. What select returns goes into the assistant's context, and from there into a transcript, a log, and wherever those are sent. The screen sits between the rows and all of that.

The database holdsTurn it on?
patient records, visit notes, lab resultsYes. This is the case the screen was built for.
customer records with names, addresses, card or account numbersYes, with identifiers at least. That catches card and account numbers. Names and addresses need a model, as the categories show.
free-text columns nobody checks: notes, comments, support ticketsYes. A free-text column is where an identifier turns up that no scope expected.
reference data, lookups, order lines with no personal columnsNo. If these sit inside a screened database, turn the screen off for their scope. Carving out a scope shows how.
anonymised or made-up dataNo. The screen would refuse invented identifiers all day and protect nobody.

Note Do not screen a working database with true just to see what happens. One of the patterns matches any email address, and customer tables are full of them, so every select that returns one will refuse. The detector is working, but on a database nobody meant to screen.

What gets screened

A screenings file, and what it gives you

Add a screenings section with a screen object. Each key names what to screen, and each value says which categories to screen for. Here a database is screened, one scope is taken out, and one scope screens for more.

.picker.json

{
  "tools": {
    "servers": {
      "main": { "connect": "env:MAIN_SQL" }
    }
  },
  "guardrails": {
    "servers": {
      "main": {
        "databases": {
          "Sales": {
            "can": ["list", "select", "describe"],
            "scopes": [
              { "object": "ref.*",
                "can": ["list", "select"] },
              { "object": "dbo.Notes",
                "can": ["list", "select"] }
            ]
          }
        }
      }
    }
  },
  "screenings": {
    "screen": {
      "Sales": ["identifiers"],
      "Sales/ref.*": false,
      "Sales/dbo.Notes": ["identifiers", "clinical"]
    }
  }
}

pick catalogs

levels
  tools       on, always; settings from C:\github\app\.picker.json
  guardrails  on, from C:\github\app\.picker.json
  screenings  on, from C:\github\app\.picker.json

main  (connect: env:MAIN_SQL)
  Sales: list select describe  [screen: identifiers]
    ref.*: list select  [screen: nothing]
    dbo.Notes: list select  [screen: identifiers clinical]
source: C:\github\app\.picker.json

What a screen key names

A key names a database, or a database and one of its scopes. To name a scope, write the database, a slash, and the scope's object pattern exactly as the scope writes it: "Sales/dbo.Customers" or "Sales/ref.*".

KeyWith guardrails onWith guardrails off
"Sales"screens the declared database Sales. It must be declaredscreens Sales if the login can reach it
"Sales/dbo.Customers"screens that scope. The scope must be declared under Salesrefused. Scopes belong to the guardrails level, so a screen names a whole database only

Here is a screen at the tools level, which screens one whole database and leaves every other reachable database alone.

{
  "tools": {
    "servers": { "main": { "connect": "env:MAIN_SQL" } }
  },
  "screenings": {
    "screen": { "Clinic": true }
  }
}

What a screen value says

The value says which categories to screen for. There are three ways to write it, and one that is refused.

You writeIt screens
trueall four categories. You cannot forget one this way
["identifiers", "clinical"]only those two. A plain list includes
["-scientific"]everything except scientific. A list of - words excludes
falsenothing. Use it to take the screen off a scope
["identifiers", "-scientific"]refused when the file is read. It could mean "identifiers only" or "all but scientific", and those differ by two categories

A misspelt category is refused too. Otherwise "lgal" would quietly take a screen away, and you would not find out until data left a database you thought was covered.

Note Three words that name a law are refused, and the message says what to write instead. phi, pii and sci each promise more than runs until a model is installed.

RefusedWrite instead
phiidentifiers for the patterns, and clinical for the model that reads clinical text
piiidentifiers
sciscientific

The four categories

WordWhat it is aboutWhat checks itWhich model
identifiersstructured identifiers, whoever they belong tosix patterns inside pick. The only category that works with nothing installednone, ever
clinicalclinical text, the concern of US health privacy lawa model you install, and nothing until you doa de-identification model from the BioBERT or ClinicalBERT family
legalcontract and consumer-report texta model you install, and nothing until you doa contract-clause model trained on CUAD, or LegalBERT
scientificresearch and scientific dataa model you install, and nothing until you doa SciBERT model trained to label entities
Out of the box, the screen catches structured identifiers 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. clinical, legal and scientific screen nothing until then.

What the patterns catch

These run inside pick itself, with no model and no second program. They are the same six patterns that fettle runs, and a check in this repository holds the two sets equal.

PatternMatchesLeft alone on purpose
ssnthe hyphenated form, leaving out every range that is never issuednine bare digits, which are more often an order number than a person
credit-card13 to 19 digits in card groupings, passing the Luhn check digitthe same shape failing Luhn, and bare digit runs
phonea leading +, or a separated groupingten bare digits
emailthe ordinary address shapenothing
medical-record-numberan identifier under its own label, such as MRN: or patient idbare strings of that shape, which would refuse every part number
date-of-birtha date under its own label, such as DOB: or date of birthbare dates, which a database is full of

All six report as one category, identifiers. Each is an identifier with a fixed shape or a label in front of it, and none of them reads clinical language. The patterns run over the rows exactly as they would leave, so an MRN: line inside one notes cell is found.

Carving out a scope

A scope's screen replaces its database's screen. That lets you take the screen off reference data inside a screened database, or have one scope screen for more than the rest.

"screenings": {
  "screen": {
    "Sales": true,
    "Sales/ref.*": false,
    "Sales/dbo.Notes": ["identifiers", "clinical"]
  }
}

Prove it refuses

A correct file and a screen that holds are two different claims. Test the second one with made-up rows, such as invented numbers and fake customers, never real ones. Then select something you know holds an identifier.

$ pick select "SELECT name, notes FROM dbo.Intake"
pick: this response would disclose regulated data: 1 in identifiers. The matches are not
quoted, because the refusal would then disclose them. Select fewer columns or rows, or take
the screen off this scope if the content is not regulated.
$ echo $LASTEXITCODE
13

The refusal gives a category and a count, never the value. A refusal that showed the value would write it into the log, which is the harm the screen exists to stop. No rows are trimmed, because an answer with the bad rows quietly removed would look complete and would not be.

Each line below tests one edge:

Select rows holding…ExitBecause
nothing regulated0the screen is on and the answer is clean. Test this too: a screen that refuses everything looks like a working one
a made-up SSN, 123-45-6789131 in identifiers, and the number appears nowhere in the refusal
the same SSN, from a scope whose screen is false0the scope's screen replaced the database's
the same SSN, in a column the scope hides0the column rule removed it before the screen saw it
a screened table joined to an unscreened one13any screened scope the query read screens the whole answer
anything, when a model category is screened and its model is missing13it fails closed. The refusal names what could not run, as below
clinical free text, with the clinical model installed13N in clinical. No pattern matches prose, so only the model can have refused it

When it refuses and you did not expect it

Every refusal names its own cause. The first few words say whether something was found or whether the screen could not run, and those are different problems.

It saysDo this
this response would disclose regulated data: The screen is working. Select fewer columns or rows, or, if the content is not regulated, take the screen off that scope.
this response could not be screened, so it is not being served: The second tier broke, and the rest of the message names the part. It may be burler missing from beside pick, a model missing from the directory, or a model that stopped or ran out of time. Run pick doctor first, and then run burler by hand.

Why every one of these refuses

Serving an answer because the check broke is the one outcome the screen must never produce. A burler that will not start, a model that will not load, an answer that will not parse, and a model that runs out of time all refuse with exit 13. So does a pattern that runs out of time on a very large answer, because a served answer must always mean the check ran.

This looks like an outage, and that is the intended trade. A screen that quietly stopped screening would be worse, because nobody would know.

What it costs

WhenWhat happens
every screened answerthe patterns run inside pick. Each has a one-second limit, and in practice they are too fast to notice next to a network trip
the first model-screened answer after a quiet spellburler starts and loads a model. Seconds. pick waits up to 90 of them before it gives up
every answer after thatTens of milliseconds. burler stays running between answers
five minutes with nothing to screenburler is stopped and its memory released
a database screening identifiers aloneburler never starts. The patterns were the whole check

What it does not do

The screen is a safety check, not a boundary. Models miss names, and patterns miss anything written in a way they do not expect. An identifier split across two cells defeats every one of them. A clean answer is evidence that nothing was there, never proof. The boundary is the column rule, the grant and the narrow login.
It does notBecause
offer a confidence settingany detection refuses. A setting gets turned down until the check is off
trim, redact or mask rowsit refuses the whole answer. An answer with holes in it would look like a safe copy, and would not be one
judge what stays in the databaseit reads the answer leaving, never the tables
stop anything outside pickSSMS, sqlcmd, a reporting tool or a person with the connection string reads the same rows unscreened. The narrow login holds for every client
certify that you meet a lawthe categories name what runs, never a statute. Nothing here is "HIPAA compliance"

The models: the second tier

Note None of this is needed for identifiers. That category runs inside pick with nothing installed. This half covers the other three categories, clinical, legal and scientific, each of which needs a model.

A model here is a trained program that labels the words in a piece of text. It is handed the rows, and answers which words are a person, a date, an identifier, and so on. Setting one up takes five steps: install burler, choose the models, prepare them, lay out the files, and name the directory.

Install burler, the model host

burler is a second program, in its own download. The name is the mill trade's word for the person who picks the faults out of finished cloth before it leaves the mill, and this program's job is the same. It runs the models, and pick talks to it over a pipe.

It is separate because of what the models need. Running them needs ONNX Runtime, a library with native parts for each operating system, while pick stays a single file you copy onto a machine. Putting the models inside it would make every download carry a feature most projects never use.

Unpack it into the same directory as pick, because that is the only place pick looks for it. pick does not search PATH, so an environment variable cannot swap in a different program.
Windows · PowerShell
$dir = "$env:LOCALAPPDATA\Programs\pick"      # wherever pick.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 pick.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 pick

Downloads come for the same four platforms as pick: win-x64, linux-x64, osx-x64 and osx-arm64. Take the one that matches your pick, because the two run together and must be built for the same machine.

Nothing happens yet. pick does not start burler until a models directory is named in the last step, so installing it early does no harm.

Note Already running fettle with models on this machine? Then burler is installed beside it, and the models directory exists. Put a copy of burler beside pick too, because each program looks only beside itself, and skip to naming the directory. One set of models serves both programs.

Choose the models

Nothing here ships a model, and nothing downloads one for you. The models come to a few hundred megabytes, against a pick download of a few megabytes. Several useful models also carry terms that would forbid this project from passing them on, so you fetch them, you read their licence, and you decide.

You need a model trained to label words, not a base model. BioBERT and SciBERT are base models, which know clinical or scientific language but have not been taught to label anything. What you need is a fine-tune: a base model trained further on a labelling task, such as finding patient details in clinical notes. Look for a model card that names a task, not only a subject, before you download anything.

Any model in the BERT family works, as long as it labels each word (a token classifier) and can be saved as ONNX.

CategoryFamilyWhat to look for
clinicalBioBERT, ClinicalBERTa de-identification fine-tune. Models trained on the i2b2 and n2c2 data label patient names, dates and record numbers, none of which any pattern finds
scientificSciBERTa fine-tune that labels entities in scientific text, not the base
legala general model with an open licence, or LegalBERTcontract-clause models trained on CUAD are the easiest start. LegalBERT is stronger, and has a licence condition, shown below

There is no row for identifiers, because it is the pattern tier and takes no model. 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

Check the licence before you use a model. Some clinical models carry terms that forbid passing them on, and a models directory you share with a colleague is passing them on.

These were checked against the model cards and source repositories on 20 August 2026, so check again before you rely on them. Terms change, and this table is a starting point, not 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 tagusable. Record the GitHub repository as the source, not the copy you downloaded
SciBERTApache-2.0, on allenai/scibert. The copies carry no tag eitherusable, with the same rule for the source
ClinicalBERT, and the i2b2 de-identification fine-tunestagged MIT by the groups that held the data agreementsusable on that grant. Record the revision as well as the source
LegalBERTCC-BY-SA-4.0, a share-alike licenceusable as your own add-on. Do not fine-tune from it, because share-alike carries into whatever you build on it
anything with no licence anywherenonedo not use it, however good its scores

Prepare them

burler runs models saved as ONNX, a file format that lets programs other than the one that trained a model run it. Most published models are not in that format yet, so you convert each one once, and Hugging Face's optimum tool does it in one command. The task name token-classification means a model that labels each word.

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

Quantising afterwards is optional, and worth doing. Quantising stores the model's numbers less precisely, usually in 8 bits instead of 32. The file shrinks to about a quarter and runs several times faster, at a small cost in accuracy. The same tool can do this, and the sizes on this page are for quantised models.

Lay the files out

Make one directory per category, named for the category, under one parent:

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 directory. A category no screen names is never looked for, and models load one at a time, when first needed.

FileWhat it is
model.onnxthe converted model
vocab.txtthe list of word pieces the model was trained on. Take it from the same model. A mismatched list gives 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
manifest.jsonwhat the model says about itself. Required, as shown below

A directory missing any of the first three is refused by name, so you learn which file is missing, not just that something failed.

Write the manifest

The manifest has four fields, and each one is required and may not be empty.

{
  "checkpoint": "dmis-lab/biobert-base-cased-v1.1",
  "revision":   "a1b2c3d",
  "licence":    "Apache-2.0",
  "provenance": "https://github.com/dmis-lab/biobert"
}

Name the directory

Put the models directory in the local file, under screenings. Where the models sit is a fact about one machine, not about the project, and the local file stays out of source control.

// .picker.local.json, beside .picker.json
{
  "screenings": { "models": "C:/models/screening" }
}

Write the path with forward slashes, and know that a relative path is read from the folder of the file it is written in. If both files name a directory, the local file wins. As before, no restart is needed.

With the directory named, pick catalogs gains a models: line, and pick doctor checks the models directory and burler. The doctor runs those two checks only when screenings are on.

$ pick catalogs
levels
  tools       on, always; settings from C:\github\app\.picker.json
  guardrails  on, from C:\github\app\.picker.json
  screenings  on, from C:\github\app\.picker.json and C:\github\app\.picker.local.json

main  (connect: env:MAIN_SQL)
  Sales: list select describe  [screen: identifiers clinical]
models: C:\models\screening
source: C:\github\app\.picker.json

$ pick doctor
pick X.Y.Z
  ok tools: on, always; settings from C:\github\app\.picker.json
  ok guardrails: on, from C:\github\app\.picker.json
  ok screenings: on, from C:\github\app\.picker.json and C:\github\app\.picker.local.json
  ok configuration: C:\github\app\.picker.json
  ok server main: connects (1 database(s) declared)
  ok models: C:\models\screening
  ok burler: C:\Users\you\AppData\Local\Programs\pick\burler.exe

If burler is not beside pick, the doctor marks that line !! and says where it looked. The message adds that only model screening needs it, so a project that screens identifiers alone can ignore that line.

Naming this directory changes what a missing model means. Before, the patterns are the whole screen and a clean answer is served, because nobody has said a model is installed. After, a screened model category with no model refuses, because someone has now said they expect it to be there.

That is why pick does not check the directory when it reads the file. A file opened on a machine without the models is not broken, and refusing the whole file would take away every database instead of only the screen. The refusal comes at the answer instead, where it can name what could not run.

Running burler by hand

When the model tier is in doubt, run burler yourself. It reads one request per line and answers each one 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. It loaded the model, ran it, and found nothing in that sentence
{"ok":true,"findings":[{"category":"clinical","count":1,…it 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. The message names the path it looked at
{"ok":false,"error":"the model at … would not load…"}the file is there, but it is not a model burler can run. Usually a bad export, or a download that did not finish
nothing at all, and it exitscheck the error output. With no --models, it says burler needs --models DIRECTORY, naming where the screening models are. and exits with code 2

An empty findings list means it checked and found nothing, and a missing one is a fault. The two are never written the same way.

More than one model in a category

Give the category a directory of subdirectories 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. burler looks for model.onnx directly inside the category, and if it is there, that is the one model. If not, it loads every subdirectory that holds one.

A shared models directory

The models directory may be one that other programs or machines use. One set can serve a whole team, or serve fettle and pick side by side, but know what you are trusting when the directory is not yours alone.

Where the rest is written down

You wantPage
scopes, column rules and the grant wordsGuardrails
every exit code, including 13The reference
what pick defends against, and what it does notThe reference