Skip to content

fix(db): keep a read-only database read-only - #547

Merged
kevintseng merged 12 commits into
mainfrom
fix/readonly-db-stays-readonly
Oct 1, 2026
Merged

kevintseng merged 12 commits into
mainfrom
fix/readonly-db-stays-readonly

Conversation

@kevintseng

@kevintseng kevintseng commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Refs #520.

Opening a database you made read-only (for example chmod 444 on a snapshot or backup) no longer makes it writable. memesh set the database file, its -wal/-shm files and its folder to fixed modes (600/700) on every open, which gave the owner back the write permission, so every later hook, CLI or MCP process wrote into it. It now only removes access for other users and leaves the owner's permissions as they are. A world-readable database is still tightened to owner-only.

A database whose folder is also read-only cannot be opened (a WAL database needs to create its -shm file), so it is now refused with an error instead of being made writable.

Tests

New tests/db-readonly-mode.test.ts: a 444 database stays non-writable across two opens, a 644 database is still tightened to 600, and a read-only database in a read-only folder is refused with nothing made writable. The first two fail on the previous code. npm run verify: green.

Opening the database set its file mode to 600, which gave the owner
back the write permission on a database made read-only, so every later
process wrote into it. Only access for other users is removed now; the
owner's permissions are left as they are.

Closes #520
A WAL database cannot be read without creating its -shm file, so a
database in a folder the owner made read-only is refused with an error;
opening it used to work only by making the folder writable again.
Everywhere MeMesh sets permissions on its data folder and files (the
database and its -wal/-shm, every hook's database connection, `serve
--host`, the remote token, the install id, the config, the message router
and the Codex companion), it now only removes group and other access and
keeps the owner's bits as they are. Before, a fixed 0600/0700 gave a
read-only database, folder or token its owner write bit back. A folder or
token MeMesh creates is still created private (0700 / 0600).

- A -wal or -shm whose owner bits differ from the database's in a way
  SQLite would change or trip over is refused before anything is opened,
  with the chmod command to run; MeMesh never changes a sidecar's owner
  bits itself. This applies to the CLI, MCP, HTTP, every hook including
  read-only opens, and `npm run audit:memory`, and follows a symlinked
  database path to the real files.
- A database opened read-only says which files to make writable to write
  to it again, and `memesh doctor` shows it as read-only.
- A read-only folder whose -wal and -shm exist is opened read-only and
  read, as SQLite supports; writes are refused. If a sidecar is missing,
  the error names the folder and the command to run.
- An existing remote token is read as it is; a read-only one stays
  read-only.
- A permission change that cannot be applied prints the file and the
  command to run, once; a folder that belongs to another user is named as
  such instead.
- The router and the Codex companion stop with a one-line reason on a
  read-only data folder or companion lifecycle folder instead of making it
  writable.
- `memesh doctor` shows MeMesh's own reason and command for a permission
  error, including the -wal/-shm, instead of suggesting to move the
  database away; `doctor --fix` no longer sets the database to 600.

Refs #520
- A database path that is a symlink is judged by the real file's folder:
  a read-only folder holding only the link no longer refuses a database
  whose -wal/-shm sit, writable, beside the real file.
- The maintainer measurement scripts under scripts/audit check the
  -wal/-shm modes before they open a database, as memesh itself does.
- `memesh doctor` no longer suggests a full disk for a permission
  problem MeMesh refused on: Hook activity and Capture liveness point to
  the Database row and its chmod command.
- A file or folder owned by another user is answered with "Point
  MEMESH_DB_PATH at a database you own, in a folder you own."

Refs #520
…lone

- A read-only file or folder that belongs to another user is answered with
  "point MEMESH_DB_PATH at a database you own, in a folder you own" instead
  of a chmod command the user cannot run. Root, which can change any file,
  still gets the chmod.
- For a symlinked database path only the link's folder loses group and
  other access; the real database's folder is checked but never changed,
  since it may be a folder the user shares on purpose.
- `memesh doctor` lists the real file's -wal/-shm when the database path is
  a symlink, and prints "Run:" only in front of a command.

Refs #520
…-readonly

# Conflicts:
#	CHANGELOG.md
#	dist/mcp/THIRD_PARTY_NOTICES.txt
#	dist/mcp/server.js.map
#	dist/skills-manifest.json
#	dist/transports/cli/cli.js.map
#	scripts/hooks/pre-edit-recall.js
#	scripts/hooks/session-start.js
#	scripts/hooks/stop-message-gate.js
Comment thread tests/db-readonly-mode.test.ts Fixed
Comment thread tests/db-readonly-mode.test.ts Fixed
Comment thread tests/hooks/data-folder-permissions.test.ts Fixed
Comment thread tests/hooks/data-folder-permissions.test.ts Fixed
Root is never told a file belongs to another user: it is given the chmod,
which root can run. A test now holds that, with a stubbed uid.

Two tests read the -wal before they check its size or mode, so the read
no longer follows a check of the same file.

Refs #520
… yours

When a -wal or -shm has different owner permissions from the database and
belongs to another user, the refusal told the user to chmod a file they
cannot change. It now keeps the refusal and its code but says to point
MEMESH_DB_PATH at a database of their own, as the other permission errors
already do.

Refs #520
An empty -wal with more owner permissions, and a mismatched -wal next to
a database that belongs to another user, now each have a test for the
"database you own" advice.

Refs #520
…-readonly

# Conflicts:
#	CHANGELOG.md
#	dist/db.d.ts.map
#	dist/db.js
#	dist/db.js.map
#	dist/mcp/THIRD_PARTY_NOTICES.txt
#	dist/mcp/server.js
#	dist/mcp/server.js.map
#	dist/transports/cli/cli.d.ts.map
#	dist/transports/cli/cli.js
#	dist/transports/cli/cli.js.map
#	src/db.ts
…-readonly

# Conflicts:
#	scripts/audit/memory-invariants.mjs
Comment thread tests/core/printed-command-quoting.test.ts Dismissed
Comment thread tests/core/printed-command-quoting.test.ts Dismissed
@kevintseng
kevintseng merged commit 37fbb68 into main Oct 1, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants