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.

Settings › Safety, the self-test titled Verify, don’t trust. It reads: saferow is closed source, so it proves itself on your own database. It tries twelve ways out of the read-only side and shows the database’s own words for each, then writes one canary row as you and rolls it back. A result panel shows twelve green orbs and the number 12 of 12 blocked: every way out was closed, on saju, by the read-only role, saferow’s kernel and the server.
Settings › Safety runs the self-test against the connection you pick. Example data.

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 :password placeholder; saferow generates the password, keeps it in your Keychain, and fills it in only as the statement runs.
  • 3Check it any time. SHOW GRANTS on MySQL, or \du and \dp in 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.
MySQL · the role saferow proposes
CREATE USER 'saferow_ro'@'%' IDENTIFIED BY :password;
GRANT SELECT, SHOW VIEW ON `shop`.* TO 'saferow_ro'@'%';
Postgres
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.

The escapes, what a read-only session alone did, and what saferow does
#EngineThe attemptRead-only mode aloneWith saferow
1MySQL 8.0START TRANSACTION READ ONLY, then DROP TABLEDroppedBlocked
2MySQL 8.0A read-only transaction, COMMIT, then UPDATEAppliedBlocked
3MySQL 8.0SET SESSION TRANSACTION READ WRITE after read-onlyAppliedBlocked
4MySQL 8.0SELECT … INTO OUTFILE in a read-only sessionWrote a fileBlocked
5MySQL 8.0/*!80000 UPDATE … */, an executable commentRanBlocked
6Postgres 17A writable CTE, a temp table and nextval() inside BEGIN READ ONLYBlockedBlocked
7Postgres 17BEGIN READ ONLY; COMMIT; UPDATEAppliedBlocked
8Postgres 17Turn default_transaction_read_only offAppliedBlocked
9Postgres 17A SELECT-only role with every read-only flag flippedHeldBlocked
10SQLite 3.54PRAGMA query_only = 1, then turn it offAppliedBlocked
11SQLite 3.54Opened read-only, then VACUUM INTO a new fileWrote a fileBlocked
12SQLite 3.54mode=ro, then ATTACH … mode=rwcNew databaseBlocked

3 · approvals

An approval is bound to the rows it counted.

4 · rewind and the ledger

A way back, and a record of what happened.

The ledger: every write by you, the agent and MCP clients, kept on this Mac. #0142 CREATE INDEX on Appointments, approved, by the agent on doclife, with Rewind; #0141 DELETE on Sessions, 212 rows, Touch ID, by you on saju production, with Rewind conflicts, Review; #0140 UPDATE Coupons, 1 row, Touch ID, by Claude Code through MCP, rewound; #0139 UPDATE Appointments, 2 rows, direct, by you, with Rewind.
The ledger, on example data.

5 · saferow mcp

Agents get a connection name, never a password.

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 Mac
  • dl.saferow.appupdate checks and update downloads
  • huggingface.comodel downloads you start
  • openrouter.aionly when you choose an OpenRouter model
  • api.anthropic.comonly when you choose a Claude model
  • api.openai.comonly when you choose an OpenAI model
  • 127.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.