Hiding the table is not enough. Your LLM can still see the salary column.
Most text-to-SQL setups protect data at the table level. The analyst cannot read hr_compensation, so that table never goes into the prompt. Good. But a lot of sensitive data does not live in its own table. It lives in o
Most text-to-SQL setups protect data at the table level. The analyst cannot read hr_compensation, so that table never goes into the prompt. Good.
But a lot of sensitive data does not live in its own table. It lives in one column of a table everyone uses. employees.salary. customers.national_id. patients.diagnosis. If the model sees the column name in the schema, it will happily write SELECT salary FROM employees. Your database might block the query, or it might not. Either way, the model has already learned the column exists.
schemagate 1.1.0 adds column rules you can write in a JSON file:
{
"restrict_column": {
"employees": {
"salary": ["hr"],
"national_id": ["hr", "compliance"]
}
}
}
A caller without the hr role now gets the employees table with salary simply not there. Not marked secret. Not ranked low. Absent. The model cannot write a query against a column it was never shown.
Try it in one minute
pip install schemagate
# the bundled demo database; this prints its sqlite:/// URL
python -c "from schemagate.demo_schema import create_demo_db; print(create_demo_db())"
echo '{"restrict_column": {"hr_compensation": {"annual_amount": ["hr"]}}}' > catalog.json
schemagate select --url <url printed above> --prompt "salary by employee"
schemagate select --url <url printed above> --config catalog.json --prompt "salary by employee"
The first prompt shows annual_amount. The second does not.
The same file works everywhere
The command line, the local Studio page and the MCP server all read the same catalog.json. So if you connect Claude, Cursor or any MCP client to your database through schemagate, the column rule applies there too. You state the policy once.
A typo is an error, not a silent no-op
This one matters more than it sounds. Before 1.1.0, a block the loader did not know was quietly ignored. Write "restrict_columns" with an extra s, and you had a file that looked like a policy and did nothing.
Now it stops with one line:
schemagate: --config catalog.json: unknown block(s) 'restrict_columns'; expected any of restrict, restrict_column, hint, describe, groups
The same goes for a table or column name that does not exist. An access rule that "succeeds" on a typo is a rule that is not there.
Where this fits
schemagate is the step before the model writes SQL: it picks the few tables a question needs, and it drops the ones the caller may not read. That keeps prompts about 75% smaller on a big schema, and it means the model never sees what it should not.
It works with Postgres, Oracle, MySQL, SQL Server and SQLite, as a Python library, a CLI, an MCP server, or a LangChain retriever (now listed in the LangChain docs).
- GitHub: https://github.com/ashishsinha1602/schemagate
- Browser demo, no install: https://ashishsinha1602.github.io/schemagate/
pip install schemagate
If you protect columns some other way in your text-to-SQL stack, I would like to hear how. Views per role? Column-level grants? Masking in the database?
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.