Safe mode
Safe mode is on by default. With it on, anything that is not read-only SQL is refused before it reaches your database.
This page is about what “refused” actually means, because the guarantee is stronger for some databases than others and pretending otherwise would be the wrong kind of reassurance.
What counts as read-only
A statement is read-only when it starts with SELECT, SHOW, DESCRIBE, EXPLAIN, or WITH, and contains no write keyword outside a string or comment.
The write keywords are ALTER, CALL, COPY, CREATE, DELETE, DROP, EXEC, EXECUTE, GRANT, INSERT, INSTALL, LOAD, MERGE, REPLACE, REVOKE, SET, TRUNCATE, UPDATE, and VACUUM.
Two more rules matter:
- Multiple statements in one call are refused for agents. One call, one statement.
- An unknown SQL dialect is refused, not guessed at. Grain fails closed.
The classifier tokenises per dialect rather than pattern-matching the raw string, so a DROP inside a string literal or a comment is not mistaken for a write — and a write hidden behind dialect-specific quoting is not mistaken for a comment. MySQL backslash escapes, PostgreSQL dollar-quoting and nested block comments, # line comments in MySQL and BigQuery: each dialect gets its own lexer settings.
Agents and humans are treated differently
| Agent | You | |
|---|---|---|
| Read-only statement | runs | runs |
| Write statement | refused | runs after you confirm |
| Several statements at once | refused | runs after you confirm |
| Unknown dialect | refused | runs after you confirm |
An agent gets a hard refusal. You get a confirmation prompt, because it is your database and you are the one who typed the statement.
Enforced versus advised
Classification is one boundary. Some connectors have a second one — a session or engine-level guarantee that holds even if the classifier were wrong.
| Connector | Safe mode is |
|---|---|
| PostgreSQL | enforced — the session sets default_transaction_read_only, so the engine itself rejects a write |
| SQLite | enforced — the file is loaded into memory and never written back, so a write cannot persist |
| MySQL, SQL Server, DuckDB, Trino, Snowflake, BigQuery, Elasticsearch | advised — classification is the only boundary |
Both are on by default and both refuse the same statements. The difference is what happens in the case where the classifier is wrong: on an enforced connector nothing happens, because the database itself declines. On an advised one, there is no second net.
If that distinction matters for a particular database, the usual answer is not to turn safe mode off but to connect as a role that has no write grants. A permission your credentials do not have cannot be misclassified.
Beyond SQL
Safe mode is about statements. Tool permissions are a separate limit, and they apply whether or not safe mode is on.
An agent attached through the packaged bridge cannot change your saved connections and cannot ask for your stored credentials. Those tools are still advertised and still answer, with an explicit refusal naming the profile and the reason — so an agent learns “not allowed here” rather than concluding the capability does not exist and trying something more creative.
Turning it off
Set grain.mcpSafeMode to false in settings, or GRAIN_MCP_SAFE_MODE=false in the MCP server’s environment.
Do this when you have a reason to — a migration you want an agent to run, a scratch database, a local DuckDB file. It is a real setting, not a warning label. It is on by default because the common case is an agent exploring a database it should not be able to change.