security
Verify, don’t trust.
saferow is closed source. So every safety claim on this page is one you can check yourself, on your own database and your own Mac, without reading a line of its code.
- Read-only roles
- The 12 escapes
- Counted approvals
- Rewind and the ledger
- saferow mcp
- The network list
- What it can’t stop
- Report a problem
1 · read-only roles
The guard is a GRANT you can see.
Reading SQL text can’t make a connection read-only; database permissions can. saferow reads through a role that can only SELECT, and the database itself refuses everything else.
- 1saferow writes the SQL; you run it. It appears as a held change on your own credential, so you read every statement before it executes.
- 2The password never appears in it. The statement carries a
:passwordplaceholder; saferow generates the password, keeps it in your Keychain, and fills it in only as the statement runs. - 3Check it any time.
SHOW GRANTSon MySQL, or\duand\dpin psql, prove what saferow can do. On SQLite, the read side opens the file read-only, behind an authorizer that refuses writes, ATTACH and VACUUM INTO.
CREATE USER 'saferow_ro'@'%' IDENTIFIED BY :password;
GRANT SELECT, SHOW VIEW ON `shop`.* TO 'saferow_ro'@'%';
CREATE ROLE "saferow_ro" LOGIN PASSWORD :password;
GRANT CONNECT ON DATABASE "shop" TO "saferow_ro";
GRANT USAGE ON SCHEMA "public" TO "saferow_ro";
GRANT SELECT ON ALL TABLES IN SCHEMA "public" TO "saferow_ro";
ALTER DEFAULT PRIVILEGES IN SCHEMA "public"
GRANT SELECT ON TABLES TO "saferow_ro";
ALTER ROLE "saferow_ro" SET default_transaction_read_only = on;
The database and schema names are yours; the role name can be changed.
2 · the 12 escapes
Twelve ways out of “read-only”. All twelve blocked.
Each of these got past a read-only transaction, a session flag or a read-only file mode when it was tried on MySQL 8.0, PostgreSQL 17 and SQLite 3.54. saferow blocks every one, twice: its kernel refuses the statement before the database sees it, and the read-only role or SQLite authorizer refuses it again with the database’s own error.
| # | Engine | The attempt | Read-only mode alone | With saferow |
|---|---|---|---|---|
| 1 | MySQL 8.0 | START TRANSACTION READ ONLY, then DROP TABLE | Dropped | Blocked |
| 2 | MySQL 8.0 | A read-only transaction, COMMIT, then UPDATE | Applied | Blocked |
| 3 | MySQL 8.0 | SET SESSION TRANSACTION READ WRITE after read-only | Applied | Blocked |
| 4 | MySQL 8.0 | SELECT … INTO OUTFILE in a read-only session | Wrote a file | Blocked |
| 5 | MySQL 8.0 | /*!80000 UPDATE … */, an executable comment | Ran | Blocked |
| 6 | Postgres 17 | A writable CTE, a temp table and nextval() inside BEGIN READ ONLY | Blocked | Blocked |
| 7 | Postgres 17 | BEGIN READ ONLY; COMMIT; UPDATE | Applied | Blocked |
| 8 | Postgres 17 | Turn default_transaction_read_only off | Applied | Blocked |
| 9 | Postgres 17 | A SELECT-only role with every read-only flag flipped | Held | Blocked |
| 10 | SQLite 3.54 | PRAGMA query_only = 1, then turn it off | Applied | Blocked |
| 11 | SQLite 3.54 | Opened read-only, then VACUUM INTO a new file | Wrote a file | Blocked |
| 12 | SQLite 3.54 | mode=ro, then ATTACH … mode=rwc | New database | Blocked |
-
Run them on your database
Settings › Safety tries all twelve against the connection you pick and shows the database’s own error for each. Every attempt is harmless even if a guard were missing: writes match no rows, DROP names a table that doesn’t exist, files go to paths that can’t be written.
-
Then one canary
A no-op UPDATE on the read-write side, inside a transaction that is rolled back, proves the write path works and leaves nothing behind.
-
The same twelve are its test suite
saferow’s own tests run all twelve against real MySQL 8.0, Postgres 17 and SQLite databases, next to the kernel’s classifier tests.
3 · approvals
An approval is bound to the rows it counted.
-
3Counted, then held
Every UPDATE and DELETE is counted with the same WHERE before it runs, inside a transaction that is rolled back. The approval carries that count. If the count differs when it runs, the transaction rolls back and nothing changes.
-
Touch ID on production
Writes on a production connection need Touch ID, and the prompt repeats the count and the connection. Plain Return never approves. Statements that can’t be narrowed are held until you type the count.
-
No agent can approve
In the app, approving is a capability only the main process holds. No code path open to an agent, an MCP client or the interface can grant a held change; the kernel in the database worker makes the decision.
-
One statement, one transaction
Several statements in one string are refused, as is code hidden in a MySQL
/*! … */comment. saferow manages transactions itself: each write runs in its own, with its restore point.
4 · rewind and the ledger
A way back, and a record of what happened.
-
Rewind
Before a write, saferow saves the rows it will change, on your Mac. Rewind proposes the way back as a change of its own, held and counted like any other, and says when rows changed since. 7 days free, 30 days with Pro.
-
The ledger
Every write — by you, saferow’s agent or an MCP client — with its statement, its count, how it was approved and when. SQL you edited in an agent’s plan is marked as yours.
-
Shadow copies
Schema changes on production are rehearsed on a shadow copy first, so an error shows up before it reaches your data.
5 · saferow mcp
Agents get a connection name, never a password.
-
A signed shim, a private socket
Claude Code, Cursor and Codex start
saferow-mcp, a small binary inside the app, signed with it. It holds no logic and no secrets: it pipes to a Unix socket inside saferow’s own folder, readable only by you. No network port is opened. -
The caller’s signature is checked
On every connection saferow checks that the process on the other end of the socket is its own shim: the right identifier, a valid code signature, saferow’s own Team ID. A new client is shown to you, with the app that started it, before it can see anything.
-
You choose what it sees
Pairing asks which connections a client may use. Its reads run under each connection’s read-only role; its writes are proposals, counted and held for you, with Touch ID on production.
-
Every call is on the record
Reads and proposals from a client appear in the ledger as “MCP · Claude Code” or whichever client it was. Revoke a client in Settings › saferow mcp and it is cut off.
6 · the network list
Every host the app talks to on its own.
This is the whole list, the same one Settings › Network shows. Watch it with Little Snitch or LuLu: saferow connects to these and nothing else, and to your databases only when you connect to them.
saferow.applicense activation, once per Macdl.saferow.appupdate checks and update downloadshuggingface.comodel downloads you startopenrouter.aionly when you choose an OpenRouter modelapi.anthropic.comonly when you choose a Claude modelapi.openai.comonly when you choose an OpenAI model127.0.0.1Ollama and LM Studio on this Mac
- No telemetry, no analytics, no crash reports sent anywhere. No account, no sign-in to saferow.
- Deny any of it and saferow keeps working with a local model; only license activation, downloads and the remote model you denied stop.
- Licenses are verified offline. A license is a signed file the app checks with a public key it carries. Activation records the Mac with saferow.app once; if that fails, the license works anyway and activation is retried later.
- Passwords live in your Keychain. They pass from the window to the app’s main process once and are read only there; the database worker asks for them, and nothing sends them back to the interface or to a model.
7 · what it can’t stop
The limits, said plainly.
- Instructions hidden in your data. A row can contain text written to steer a model. That can change what the agent proposes; it can’t make a write run unseen. Every write is still counted, held and shown to you, and agents are refused DROP, TRUNCATE and DELETE without WHERE on production.
- What you approve. saferow shows the count, the rows and the statement; it can’t judge whether the change is the one you meant. Read the plan: that is where it puts your attention.
- An account you give it. The read-only guarantee is the role’s. If you point the read side at an account that can write, the kernel still refuses writes, but the database no longer backs it up. The self-test will tell you.
- Your own SQL tab. You can still run any statement yourself, through the same kernel: it is held, counted and recorded, but it is your call.
- A remote model you choose. Its provider sees what you send it. Every turn shows how many bytes left the Mac; lock a connection to local models to make that zero.
8 · report a problem
Found a way around the kernel?
Write to security@saferow.app with what you did and what happened. Please don’t include real data or credentials. You will get a reply, and a fix will name you if you want.