Skip to content

chat.db authorization denied on macOS 26.3 — needs immutable=1 SQLite flag #83

@cedricjanssens

Description

@cedricjanssens

Summary

imsg fails to open ~/Library/Messages/chat.db on macOS 26.3.1 (Tahoe) with authorization denied (code: 23). This is caused by a new macOS TCC restriction that blocks SQLite WAL locking on chat.db for non-system processes, even with Full Disk Access granted.

The fix is to open chat.db in read-only immutable mode (?mode=ro&immutable=1).

Environment

  • macOS 26.3.1 (Darwin 25.3.0, arm64, Apple M4 Pro)
  • imsg 0.5.0 (Homebrew, steipete/tap/imsg)
  • SQLite 3.51.0
  • Full Disk Access: granted for imsg binary AND parent process (node)
  • Automation > Messages: granted

Reproduction

# imsg fails
$ imsg chats
permissionDenied(path: "/Users/.../Library/Messages/chat.db", underlying: authorization denied (code: 23))

# Direct sqlite3 also fails (normal mode tries to acquire WAL lock)
$ sqlite3 ~/Library/Messages/chat.db "SELECT COUNT(*) FROM message;"
Error: unable to open database "...": authorization denied

# BUT: file read works fine (FDA is active)
$ file ~/Library/Messages/chat.db
SQLite 3.x database, last written using SQLite version 3051000...
$ wc -c ~/Library/Messages/chat.db
1527808

# AND: sqlite3 read-only immutable mode works perfectly
$ sqlite3 "file:$HOME/Library/Messages/chat.db?mode=ro&immutable=1" "SELECT COUNT(*) FROM message;"
344

# AND: copying the file then opening the copy works
$ cp ~/Library/Messages/chat.db /tmp/chat-copy.db
$ sqlite3 /tmp/chat-copy.db "SELECT COUNT(*) FROM message;"
344

Root cause analysis

macOS 26.3.x introduced stricter protection on ~/Library/Messages/chat.db. The change does not block file reads (cp, cat, file all work) but blocks SQLite WAL locking operations that sqlite3 performs when opening a database in normal mode.

When SQLite opens a database, it attempts to acquire a shared lock for WAL (Write-Ahead Logging) mode. macOS now intercepts this lock attempt on chat.db and denies it for non-Apple processes, even with Full Disk Access enabled.

The evidence:

  1. ls, file, wc, head, cp all work → FDA is effective, file-level read is allowed
  2. sqlite3 chat.db fails → lock acquisition is blocked
  3. sqlite3 "file:chat.db?mode=ro&immutable=1" works → bypasses locking entirely
  4. sqlite3 /tmp/copy.db works → the data is not corrupted, only in-place locking is blocked

Suggested fix

Open chat.db with the SQLite URI flag immutable=1 (or at minimum mode=ro). This tells SQLite to skip all locking, which is safe for read-only access.

In Swift (GRDB/SQLite.swift):

// Before (broken on macOS 26.3+)
let db = try DatabaseQueue(path: chatDbPath)

// After
let db = try DatabaseQueue(path: chatDbPath, configuration: Configuration { config in
    config.readonly = true
})
// Or via URI:
let db = try DatabaseQueue(path: "file:\(chatDbPath)?mode=ro&immutable=1")

In raw C SQLite API:

// Before
sqlite3_open(path, &db);

// After
sqlite3_open_v2(path, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_URI, NULL);
// Or:
sqlite3_open_v2("file:path?immutable=1", &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_URI, NULL);

Since imsg only reads from chat.db (never writes), immutable=1 is the correct flag.

Impact

This breaks imsg completely on macOS 26.3+ — no commands that read Messages work. The binary was installed via Homebrew on 2026-03-17 (before the macOS update), and the issue appeared after updating to macOS 26.3.1.

This likely affects all users who upgrade to macOS 26.3+.


Investigated with Claude Code (Opus 4.6). All reproduction steps verified on a Mac mini M4 Pro running macOS 26.3.1.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions