Shoddy · Picker
What belongs to each level, every refusal with its level, the query gate, column scoping, DESCRIBE, the caps, doctor's lines and the exit codes.
This page holds the tables you look things up in. pick works at three levels, called tools, guardrails and screenings, and every refusal and every doctor line below says which level it belongs to. A refusal that belongs to the guardrails level can only happen when guardrails are on.
The file .picker.json holds up to three sections, one for each level, and nothing else at the top. The tools level is always on, and the file must name at least one server in its tools section. Each of the other two levels is on when the file has its section.
| Level | Section | What it holds | Which file may hold it |
|---|---|---|---|
| tools | tools | the servers, and for each server the connect value that says how to reach it | .picker.json, while the local file may supply connect and nothing else |
| guardrails | guardrails | the servers, and for each server its databases. Each database has can and scopes, and each scope has object, can and columns. | .picker.json and no other file |
| screenings | screenings | the models folder, and screen, whose keys name a database, or a database, a slash and a scope's object pattern | .picker.json, while the local file may supply models and nothing else |
Here is a full file that uses all three levels:
{
"tools": { "servers": { "main": { "connect": "env:MAIN_SQL" } } },
"guardrails": { "servers": { "main": { "databases": {
"Sales": { "can": ["list", "select", "describe"],
"scopes": [ { "object": "dbo.Customers", "can": ["list", "select"],
"columns": ["-ssn", "-dob"] },
{ "object": "audit.*", "can": [] } ] } } } } },
"screenings": { "models": "../screening-models",
"screen": { "Sales": ["identifiers"], "Sales/dbo.Customers": true } }
}connect is env:NAME, which reads the connection string from an environment variable with that name. A literal string may go in .picker.local.json, but the checked-in file refuses one that holds a password.can lists the verbs that are allowed, out of list, select and describe. An empty list allows nothing, and a scope must always state its can.columns is true for every column, a plain list for only those columns, or a list of names with a leading minus for every column except those. pick refuses a list that mixes the two forms.screen value is true for every category, false for none, or a list of the category words to screen for.list select describe, and a screen key may only name a whole database.pick reads both files before every call, and if either file breaks a rule, pick refuses to load it. The call then ends with exit code 2, invalid, and the message names the file. An unknown word anywhere in a file is refused too, and the message lists the words that are allowed in that place.
| What the file does | What pick says |
|---|---|
It has servers or models at the top, which is the shape from before release 2.1.0. | The file has the old shape, and the message ends with your own entries rewritten into the three sections, ready to paste. |
| It has a key at the top other than the three section names. | The key is not something the file declares, because the file takes three sections, one per level: tools, guardrails and screenings. |
.picker.local.json holds anything other than connect and models. | The local file may supply models and connect only, so declare servers, databases and screens in .picker.json. |
| What the file does | What pick says |
|---|---|
| It names no server at all. | There is nothing to open, so declare a server under tools. |
A server has no connect in either file. | Name an environment variable as env:NAME, or put the connection string in .picker.local.json. |
A connect in .picker.json holds a password. | That file is checked in, so move the string to the local file or use env:NAME. |
A server under tools declares databases. | Databases go under guardrails, and the message shows the line to write. |
.picker.local.json names a server that .picker.json does not declare. | Declare every server in .picker.json. |
| What the file does | What pick says |
|---|---|
A server under guardrails has a connect. | A connection belongs to the tools level, so put it under tools. |
guardrails declares databases on a server that tools does not name. | Declare the server under tools with its connect. |
A scope has no can. | Write the verbs the scope allows, where an empty list allows nothing. |
| What the file does | What pick says |
|---|---|
A database under guardrails has a screen. | A screen now goes under screenings, keyed by the database, and the message shows the line to write. |
A scope under guardrails has a screen. | A screen goes under screenings, keyed by the database, a slash and the scope's object pattern. |
With guardrails on, a screen key names a database that no server declares. | The screen names nothing that is declared under guardrails. |
A key such as Sales/dbo.Orders names a scope that the database does not declare. | Declare the scope under guardrails, or screen the whole database. |
With guardrails off, a key such as Sales/dbo.Orders names a scope. | Scopes belong to the guardrails level, which is off, so screen the whole database or declare the scope under guardrails. |
Take a file in the shape that releases before 2.1.0 read:
{ "servers": { "main": { "connect": "env:MAIN_SQL",
"databases": { "Sales": { "can": ["list", "select"], "screen": ["identifiers"] } } } } }pick refuses it, and the message ends with the rewrite. The message is one line, which is shown here wrapped:
pick: .picker.json has "servers" at the top, which is the shape this version no longer
reads. Rewrite it with three sections, one per level. Your entries, rewritten:
{"tools":{"servers":{"main":{"connect":"env:MAIN_SQL"}}},"guardrails":{"servers":{"main":
{"databases":{"Sales":{"can":["list","select"]}}}}},"screenings":{"screen":{"Sales":["identifiers"]}}}
(C:\github\app\.picker.json)The rewrite moves each connect under tools and each database under guardrails. It moves models and every screen under screenings, where a scope's screen gets a key made of the database, a slash and the scope's pattern.
These rules decide which table a name in a query means. The first group applies at the tools level, so it always applies.
Sales.dbo.Customers, is looked up in the databases that are there, and any other database answers the same as a table that does not exist.dbo.Customers or Customers, is searched for across those databases, and pick never asks the connection for a default database.WITH.The second group applies at the guardrails level, so it applies only when guardrails are on.
list is hidden. It is missing from objects and from the bare-name search, and when you name it, pick refuses it in the same words as an object that does not exist.select is refused plainly, and the refusal names what its scope does grant, because the listing already showed that the object exists.All five verbs belong to the tools level. They work the same way at the command line and through MCP, and the MCP answer equals the answer the command line gives with --json. The flag --config PATH names the file to use, and without it the nearest .picker.json at or above the current folder applies.
| Verb | Takes | Answers |
|---|---|---|
catalogs | nothing | the three level lines, then the servers, databases, verbs, screens and scopes, with each open server marked as one. It opens no connection. |
objects | an optional pattern, either schema.object or a bare name, with * inside a part | the tables and views whose scope grants list, stopping at 500 and saying so |
select | one statement, or --stdin to read it from input, with --limit N to set the row cap | the rows with their types and the column list, or the gate's refusal |
describe | an object name | the columns with their types and whether each may be empty, then the indexes, the foreign keys and a shape hash |
doctor | nothing | the three level lines, and then the checks listed under doctor's lines |
With --json, the catalogs answer adds a levels object. It also gives each server an open field, which is true when guardrails are off.
The gate belongs to the tools level, so it is always on, and every refusal in the table below is a tools-level refusal. The gate parses every statement before anything reaches the server, which means it breaks the text into its parts and checks each one. It checks the parts against a list of the ones it knows, and anything else refuses the whole statement. So a new T-SQL feature is refused until the gate learns about it.
| Refused | Why |
|---|---|
anything but one SELECT | INSERT, UPDATE, DELETE, MERGE, EXEC, DECLARE, USE, transactions and a second statement are each refused, and each with its own reason. |
SELECT … INTO | It writes a new table, so it is a write that looks like a read. |
OPENROWSET, OPENQUERY, OPENDATASOURCE | Each one carries a connection to some other data source inside a single function call. |
| user-defined functions | The gate cannot see the code inside a function, so it cannot check it. |
| variables and table hints | They are outside the parts the gate knows, and each one is refused by name. |
| four-part names | A four-part name reaches a linked server, which is a second server. |
sys.*, INFORMATION_SCHEMA, OBJECT_DEFINITION and similar functions | The system catalog would answer questions about tables outside the rules, while describe answers them inside the rules. |
pick rewrites a statement that passes before it runs it. Every table and view becomes its full three-part name in square brackets, so the statement can only mean what the gate approved. The star in SELECT * becomes the list of columns you may read, and the answer shows that list. The gate walks CTEs, subqueries, derived tables, APPLY and set operations in the same way, and it approves a query whole or refuses it whole.
Column rules belong to the guardrails level. A scope's columns entry says which columns exist for the caller, and Guardrails shows how to write one. This section says where pick enforces the rule.
WHERE, ON, GROUP BY and ORDER BY, and the inside of expressions and subqueries. A filter on a hidden column would reveal its values one guess at a time, so it is refused too.The describe verb belongs to the tools level. It answers from the system catalog with the columns, their types and whether each may be empty, and then the indexes and foreign keys. It never runs the object it describes. With guardrails on, hidden columns are missing from all of it. An index or key that uses a hidden column is left out whole, because a note saying "one column not shown" would show that the column exists.
Every answer carries a shape hash, a short code computed from the table's shape that changes when the shape changes. A view answers with its columns and never with its definition. The text of a view names the tables under it and how they are joined, which may be exactly what the scopes hide. So pick never returns the CREATE VIEW text of any view.
The caps belong to the tools level, and the configuration file cannot change them. Each answer says so when a cap cut it short.
--limit N raises or lowers that number. A capped answer says how many rows it left out.timed-out, which is its own outcome.The doctor verb prints one line for each check. A healthy line starts with ok and a failed one with !!, and the three level lines always come first. A level that is off gets one line, and doctor runs none of that level's checks.
| Line | Level | What it reports |
|---|---|---|
tools | tools | on, always, followed by the files that hold a tools section |
guardrails | guardrails | off, or on, from followed by the file that holds the section |
screenings | screenings | off, or on, from followed by the files that hold the section |
configuration | tools | the path of the .picker.json that pick is using |
server NAME | tools | whether a short test connection opens. With guardrails off, the line says the server is open, and with guardrails on, it counts the declared databases. A failure shows the first line of the server's error. |
models | screenings | the models folder, or that the folder does not exist, shown only when screenings are on and a folder is named |
burler | screenings | where the helper program is, or where pick expected to find it, shown only when screenings are on |
The doctor verb never prints a connection string or a password. It says whether the test connection opened, and not how it signed in.
Each kind of failure has its own exit code, so a script can branch on $LASTEXITCODE without reading any text. pick uses fettle's numbers wherever the words match, so one script can drive both programs. The gaps in the numbers are fettle outcomes that pick cannot produce, such as a stale hash, and no number is reused for a different word. A check in this repository holds this table to the source code, row by row.
| Code | Outcome | Meaning |
|---|---|---|
| 0 | ok | it worked |
| 1 | — | an unexpected internal fault |
| 2 | invalid | the request did not make sense: an unknown flag, a missing value, or a configuration that pick refuses to load |
| 3 | not-found | not there, or not there as far as anyone can learn. An object outside the grant answers exactly as one that does not exist |
| 8 | refused | pick declined, for a stated reason: a verb the grant withholds, or a query the gate will not pass |
| 9 | denied | the server, the network or the system declined, and not pick: a login without rights, or a connection that will not open |
| 10 | timed-out | the query ran past its 30 seconds and was stopped |
| 13 | screened | the answer would have disclosed regulated data from a screened database or scope, or the screen that would judge it could not run. See Screenings |
The outcomes denied and refused are kept apart on purpose. If a login failure looked like a gate refusal, you would spend time debugging rules that are working.
.picker.json and holds the real passwords, and that is by design.SELECT and VIEW DEFINITION, so that even a fault in pick can reach nothing more. Declaring a database grants the login nothing, and doctor reports a connection that fails.