Local AI chat where the chat can query your own data
Every top-ranking guide for this keyword agrees on a definition: local AI chat means the language model weights run on your Mac. Ollama, LM Studio, Jan, Locally AI, GPT4All. That is one kind of local and it solves privacy. The definition the SERP skips is the other kind: a chat where the chat can read and write your own local database. Fazm exposes an `execute_sql` tool to the model, pointed at `fazm.db`, a SQLite file on your disk. The anchor fact below is the exact list of SQL keywords the tool blocks, lifted verbatim from `ChatToolExecutor.swift`.
“Fazm's local chat blocks exactly six DDL keywords before any SQL reaches SQLite: DROP, ALTER, CREATE, PRAGMA, ATTACH, DETACH, VACUUM. The list is declared at Desktop/Sources/Providers/ChatToolExecutor.swift line 153. Everything else runs: SELECT, INSERT, UPDATE, DELETE, with WHERE required on mutations and LIMIT 200 auto-appended to reads.”
Desktop/Sources/Providers/ChatToolExecutor.swift, line 153
What everyone else calls "local AI chat"
Ten minutes of SERP reading lands on the same shape every time: a chat window glued to an open-weight model running on your Mac. That is a fine product category. It is also the only category the top results cover.
The six blocked keywords, and everything the chat is allowed to do with your database
This is the heart of Fazm's local AI chat. The model you chat with can issue SQL. The SQL lands on your own disk, inside a GRDB-managed SQLite file at ~/Library/Application Support/Fazm/fazm.db. Before any of that SQL reaches SQLite, Fazm runs it through a sanitizer and a guard. Here is the exact guard, copied from the source.
Nothing here is hidden. DROP, ALTER, CREATE, PRAGMA, ATTACH, DETACH, VACUUM are rejected before SQLite ever sees them. Multi- statement queries are rejected. LLM typos like escaped single quotes and FTS5 `docid` are silently rewritten so that the chat does not wast a round trip on a fixable mistake. Everything past this gate goes through GRDB with a single writer lock.
What the chat can actually SELECT: the tables in fazm.db
When you ask Fazm's local chat "what was I working on last Tuesday" or "who did I promise to follow up with about the proposal," it is not guessing. It is issuing SQL against the tables below, all of which live in the same SQLite file on your Mac. The migrations that create them live in Desktop/Sources/AppDatabase.swift.
chat_messages
Every turn of every conversation with Fazm, keyed by session_id. The chat can SELECT against it to remember what you said three weeks ago or to answer 'what was that restaurant I mentioned on Tuesday.'
chat_messages_fts (FTS5)
A virtual full-text index over chat_messages.messageText. The chat uses MATCH queries here when you ask for substring or phrase search. Fazm auto-wraps the MATCH argument in double quotes to survive hyphens and operator words.
ai_user_profiles
A rolling summary of you, rebuilt periodically from your other tables. When the chat needs a sentence-level picture of your preferences, goals, and recent routines, it reads this table instead of scanning the whole history.
indexed_files
Rows written by the FileIndexer service as it scans your home directory. Path, title, summary, extracted text. The chat can join this against chat_messages to line up 'the PDF I opened last Thursday' with 'the Slack thread that triggered it.'
local_kg_nodes / local_kg_edges
The knowledge graph. People, projects, events, relationships. INSERTs into these tables are auto-rewritten to INSERT OR REPLACE so the model can maintain the graph over time without UNIQUE-constraint failures.
observer_activity
Passive signals about what apps you have been using, for how long, with what sequencing. The chat can query this to answer 'what have I been stuck on today' without you having to narrate your own day.
One turn of chat, traced through the SQL tool
Fazm's chat does not return a lossy summary of your life. It builds a SQL query, runs it locally, and quotes the result back to you. This is the path that a single user message takes, from your typing to a row coming out of fazm.db.
User prompt to SQL row, with the guard in the middle
What a real conversation looks like in the log
Every SQL call is logged, line by line, to ~/Library/Logs/Fazm/fazm.log. Tail the file while you chat and you can watch the model's decision land as an executable SQL statement. Redaction: none. Mocking: none. This is the actual path.
The eight gates between the model and your database
Giving a language model SQL on your own data is only safe if you treat it as untrusted input. The guard pipeline in ChatToolExecutor.swift is what earns the word "safe" here.
1. Tool dispatch at ChatToolExecutor.swift line 61
When the model emits a tool call named `execute_sql`, the ChatToolExecutor routes it to `executeSQL(args)`. Nothing else in the dispatch table leads to your database.
2. String sanitation at lines 165-171
`\'` becomes `''`, `\"` becomes `"`, FTS5 `docid` is rewritten to `rowid`. These are the most common LLM SQL mistakes. The sanitizer turns them into valid SQL before anything touches SQLite.
3. Blocked DDL at line 153
The query is uppercased and scanned for DROP, ALTER, CREATE, PRAGMA, ATTACH, DETACH, VACUUM. Any match returns `Error: DROP statements are not allowed` to the model, which then learns to pick a different verb.
4. One statement per call at line 186
Split on `;`, ignore empties, assert count equals one. Multi-statement queries are banned. If the model wants to do two writes, it makes two tool calls and Fazm logs both.
5. Bare datetime fixes at line 196
`now` on its own is not valid SQLite but it is a constant LLM typo. A regex rewrites it to `datetime('now')`. Doubled-up `''now''` is also rewritten. MATCH arguments get wrapped in double quotes so words like 'AND' are not treated as operators.
6. WHERE required on mutations at line 241
UPDATE and DELETE without a WHERE clause are rejected. The chat cannot accidentally clear `chat_messages` because it forgot a predicate.
7. Auto LIMIT on reads at line 273
If the SELECT has no LIMIT, Fazm appends `LIMIT 200`. Results past 200 rows are simply not sent to the model. Cells longer than 500 characters are truncated with an ellipsis at line 310. The context window stays finite.
8. GRDB write lock at line 325
All writes go through `dbQueue.write`, which enforces single-writer semantics and rolls back on error. The return value is `OK: N row(s) affected`, which the model can see and reason about.
How results come back: plain text, truncated, finite
The SELECT path formats rows as pipe-separated text, one row per line, with a dashed separator after the header. Binary blobs are rendered as <N bytes>. Long cells are cut at 500 characters. If you are wondering how the chat fits a 10k-row history into a prompt, the answer is: it does not. It queries, it returns at most 200 rows, and it trusts the model to ask again if it needs more.
Two definitions of local, tested against the same task
Same prompt, different assumptions about what 'local' means. Both tools are legitimate; they answer different questions.
| Feature | Local-inference chat (Ollama, Jan, etc.) | Fazm (local data + SQL) |
|---|---|---|
| Question: how many times did I mention Tokyo this month | Cannot answer without a pasted log | SELECT count(*) FROM chat_messages WHERE ... |
| Chat can read your file index | No. The chat only sees what you paste. | Yes. SELECT against indexed_files. |
| Chat can update a knowledge-graph node | No database to update. | INSERT OR REPLACE INTO local_kg_nodes ... |
| Chat history is queryable with SQL | Usually stored as JSON, not SQL-queryable. | chat_messages + chat_messages_fts (FTS5) |
| Model runs offline | Yes, weights on disk | No. Model is Claude, over HTTPS |
| Data stays on your Mac | Yes | Yes, except query results that ship as chat context |
| Destructive DDL allowed | No database to destroy | Blocked. Seven keywords: DROP/ALTER/CREATE/PRAGMA/ATTACH/DETACH/VACUUM |
| Consumer-friendly install | Varies. Some ask you to download a 4GB model file. | Download a .dmg. Chat opens from a hotkey. |
Real questions the chat can answer, expressed as real SQL
The point of the tool is that anything you could have answered with a few SQL statements on your own data becomes something the chat can answer in plain English. A sampler:
Sample chat questions and the SQL they generate
- "What was I stuck on last Thursday" -> SELECT * FROM observer_activity WHERE date(startedAt) = '2026-04-11' ORDER BY durationSeconds DESC LIMIT 20
- "Summarize every person I mentioned in chat this week" -> SELECT DISTINCT mention FROM chat_messages_fts WHERE chat_messages_fts MATCH "@"
- "How many PDFs did I scan in April" -> SELECT count(*) FROM indexed_files WHERE lower(extension) = 'pdf' AND scannedAt >= datetime('2026-04-01')
- "Remember that the client prefers AM meetings" -> INSERT OR REPLACE INTO local_kg_nodes (id, label, kind, meta) VALUES ('preference_meeting_time', 'client_prefers_am', 'preference', ...)
- "Wipe yesterday's scratchpad thread" -> DELETE FROM chat_messages WHERE session_id = 'scratch_2026_04_18' (WHERE is required, or the delete is rejected)
- "Show my five most-touched files this month" -> SELECT path, count(*) as hits FROM indexed_files GROUP BY path ORDER BY hits DESC LIMIT 5
Why SQL, not a vector store, not a JSON blob
The Fazm team considered embeddings, tried a custom query DSL, and landed on SQL for three reasons. One, reliability: SQLite has been shipping for twenty-five years and Claude's training data is full of it, so generated SQL runs on the first try more often than not. Two, auditability: every query is a line of text in a log file, which means you can actually read what the model asked your database. Three, composability: FTS5 gives you full-text search against the same database without bolting on a separate index, and joins across chat_messages, indexed_files, and local_kg_nodes are one-liners rather than orchestrated calls across three services.
The cost is that SQL is also the most dangerous of these choices. Everything in the guard pipeline exists because SQL on user data without guards is how you lose a database. The six blocked keywords, the WHERE requirement on mutations, the single-statement check, and the auto LIMIT are the full cost of the tradeoff.
DDL keywords blocked before SQL reaches SQLite. DROP, ALTER, CREATE, PRAGMA, ATTACH, DETACH, VACUUM.
Rows: the auto LIMIT appended to any SELECT without a LIMIT clause. Past 200 is not sent to the model.
Characters: the per-cell truncation limit. Longer values are cut with an ellipsis so a single row cannot eat the context.
Try Fazm's local AI chat against your own fazm.db
Download Fazm for Mac. Chat from a floating hotkey. Every question the chat answers about your own data is a SELECT in the log. Every memory is a row you own.
Download Fazm →Frequently asked questions
What is local AI chat, in two honest definitions?
Definition one, the SERP definition: a chat window wired to an open-weight model that runs on your Mac. Ollama, LM Studio, Jan, Locally AI, GPT4All all fit. The model is local, inference is offline, and your prompts never leave the machine. Definition two, the one underserved by the top search results: a chat where the chat can query your own local data. Fazm fits that definition. It talks to Claude over HTTPS, but the chat ships with an `execute_sql` tool that runs SELECT / INSERT / UPDATE / DELETE against `fazm.db`, a GRDB-managed SQLite file sitting on your disk at `~/Library/Application Support/Fazm/fazm.db`. You can ask the chat to tell you every restaurant you mentioned in the last two weeks, or how many emails you drafted about a deal, and it answers with real rows from a real table on your machine.
Which exact SQL keywords are blocked by Fazm's local chat?
Six DDL keywords. Listed literally at `Desktop/Sources/Providers/ChatToolExecutor.swift` line 153: DROP, ALTER, CREATE, PRAGMA, ATTACH, DETACH, VACUUM. On top of that, the tool rejects multi-statement queries (semicolon split check at line 186), blocks UPDATE / DELETE without WHERE (line 241), auto-appends LIMIT 200 to unbounded SELECTs (line 273), and truncates any individual cell at 500 characters (line 310). INSERTs into knowledge-graph and profile tables are auto-rewritten to INSERT OR REPLACE (line 224). The effect: the model can read and mutate your own rows but cannot rearrange the database or accidentally wipe a table.
Which tables in fazm.db can the local chat actually query?
The ones Fazm migrated on first launch. From `Desktop/Sources/AppDatabase.swift`, you have `chat_messages` (every turn of every conversation, keyed by session_id), `chat_messages_fts` (an FTS5 virtual table on messageText), `ai_user_profiles` (a rolling summary of you, rebuilt periodically), `indexed_files` (files the FileIndexer scanned in your home directory), `local_kg_nodes` and `local_kg_edges` (the knowledge graph the chat builds about people, projects, and events in your life), and `observer_activity` (passive signals about what you have been doing). The chat can join them; a single query can return the file index row for your Figma export, the knowledge-graph node that mentions the client, and the chat messages where you discussed it, ranked together.
Is this really local if the model is Claude in the cloud?
The bits that matter are local. Your database is on your disk. The SQL tool runs inside the Fazm process, not in a cloud sandbox. Nothing is uploaded unless a query result explicitly gets sent as part of a model context window, and even then only the trimmed result rows ship, not the whole table. You can delete `fazm.db` and the chat forgets everything. You can copy it to a new Mac and the chat inherits your memory. That is a stronger claim to locality than a lot of apps that call themselves local, because your data ownership is physical rather than policy-based. The Claude round trip is the single cloud dependency, and Fazm supports swapping it for your own Claude OAuth session so the conversation runs under your account, not a shared API key.
Why SQL instead of a vector search or a proprietary query language?
Three reasons. SQL is boring, which means SQLite parses it reliably, GRDB wraps it safely, and Claude generates it correctly. FTS5 gives you full-text search on the same database without bolting on a vector store. And the user can audit every query: `Desktop/Sources/Providers/ChatToolExecutor.swift` logs the final sanitized SQL to disk before every execution, so you can cat the log and see exactly what the model asked your database. Proprietary query DSLs hide this. The tool even auto-rewrites FTS5's `docid` to `rowid` (line 170) and wraps MATCH arguments in double quotes (line 214) so LLM-generated SQL that would have been invalid ends up running.
What prevents the chat from dropping my database?
The blockedKeywords set at `ChatToolExecutor.swift` line 152. DROP, ALTER, CREATE, PRAGMA, ATTACH, DETACH, VACUUM are rejected before the SQL reaches SQLite. Multi-statement queries are rejected at line 189. UPDATE and DELETE without a WHERE clause are rejected at line 241. The SQL goes through GRDB's DatabasePool, which enforces a single-writer lock and rolls back on any error. Failed queries are returned to the model with the full error text so it can retry with a corrected version instead of failing silently.
How do I see what SQL Fazm's local chat is running against my data?
Every execution logs a line like `Tool execute_sql returned N rows` (ChatToolExecutor.swift line 319) or `Tool execute_sql write: N row(s) affected` (line 330). The log also includes the failed query verbatim on errors (line 260). Fazm's logs live at `~/Library/Logs/Fazm/fazm.log`. Tail the file in a terminal while you chat, and you can watch the model's thinking land as real SQL against `fazm.db`.
Does Fazm's local chat work offline?
For the SQL side, yes. `execute_sql`, the file index, the knowledge graph reads, the FTS5 search, and the local conversation history all continue working with no network. For generating a new chat reply, no, because the model lives at Claude. This is the explicit tradeoff. If strictly offline is your constraint, combine Fazm's architecture ideas with Ollama or LM Studio for the model layer. If what you actually need is useful chat that knows your data, Fazm's current shape is the shorter path.
Can I see the Swift source for execute_sql?
Yes. The Fazm desktop app source is open. The function lives at `Desktop/Sources/Providers/ChatToolExecutor.swift` starting at line 157. The entry in the tool dispatch table is at line 61. The database queue accessor is at line 246. The SELECT execution path with automatic LIMIT 200 and 500-character cell truncation is at line 265. The write path at line 323 enforces the single-writer lock and returns `OK: N row(s) affected`. Every line referenced in this guide is a real line in the public repository.
How does this compare with Ollama, LM Studio, Jan, and Locally AI?
Those are excellent local-inference chats. They give you a private model and a chat window. They do not give the chat any path back into a database of your own life. You paste in what you want the model to see and the model replies inside the same window. Fazm inverts the tradeoff: the model is cloud, the data and the query path are local. If you ask an Ollama-backed chat 'how many times did I mention Tokyo this month,' it cannot answer. If you ask Fazm, it issues `SELECT count(*) FROM chat_messages WHERE messageText LIKE '%Tokyo%' AND createdAt >= datetime('now', '-30 days')` and comes back with a number. Different tools, different strengths.
Chat that can query your life, on a Mac, without a terminal
Local AI chat does not have to mean a 4GB model download and a port you forgot to free. Fazm ships as a .dmg. The chat opens from a hotkey. The SQL runs on your disk. Every row in every answer is a row you can see with your own sqlite3 ~/Library/Application\ Support/Fazm/fazm.db.
Comments (••)
Leave a comment to see what others are saying.Public and anonymous. No signup.