Adds a single-file V2 plugin that exposes eight tools under the `sqlite` namespace (open, close, list, query, exec, script, tables, schema) so a model can read and write SQLite databases from a session. - OpenCode discovers it automatically from ~/.config/opencode/plugins/. - New runtime dependency: better-sqlite3 (installed in this repo's node_modules so the host's module resolution picks it up). - README documents install, copy-to-another-machine, options, and troubleshooting; SETUP.md is a copy-paste checklist. - Schema/options were trimmed down while authoring: the initial draft registered a no-op SQLite function on every open as a 'future hook'; removed, it was dead code.
sqlite
An OpenCode V2 plugin that adds a small set of sqlite_* tools so a model
can read and write SQLite database files from a session.
It is intentionally not a replacement for the existing app. It registers
extra, opt-in tools under the sqlite namespace, leaving every other part of
OpenCode unchanged.
- OpenCode V2 plugin docs: https://opencode.ai/v2/docs/build/plugins/
- OpenCode plugin config: https://opencode.ai/v2/docs/plugins
- better-sqlite3 (sync API, used under the hood): https://github.com/WiseLibs/better-sqlite3
What you get
Eight tools, all under the sqlite namespace (effective names shown):
| Tool | Purpose |
|---|---|
sqlite_open |
Open or create a database, bind it to a short alias. |
sqlite_close |
Close a previously opened connection. |
sqlite_list |
List all currently open connections and their modes. |
sqlite_query |
Run a SQL query (SELECT) and return rows. |
sqlite_exec |
Run a non-SELECT statement (INSERT/UPDATE/DELETE/DDL). |
sqlite_script |
Run a multi-statement SQL script in one transaction. |
sqlite_tables |
List user-visible tables (and optionally views). |
sqlite_schema |
Show columns and the original CREATE statement. |
Every tool except list is addressed by the alias returned from
sqlite_open, so a single session can keep several databases live at once.
Install
The plugin source is a single TypeScript file at
~/.config/opencode/plugins/sqlite/index.ts. It has two npm dependencies
that must be installed into the OpenCode config's own node_modules (not
the project you are running OpenCode in). The OpenCode service loads them
from ~/.config/opencode/node_modules regardless of the project directory.
The host redirects module resolution to that location, so if you copy this plugin to another machine you must run the install steps there too.
1. Match @opencode/plugin to your OpenCode version
The package name and the API shape change between releases. The docs' import specifiers are correct, but only at the matching version.
opencode --version # e.g. v2.0.24
cd ~/.config/opencode
npm i @opencode/plugin@$(opencode --version | sed 's/^v//')
A stale @opencode-ai/plugin (V1-era name) sitting in node_modules is a
leftover user-side dependency and does not reflect what the host
provides. Ignore its export list.
2. Install the runtime dependency (better-sqlite3)
better-sqlite3 ships native bindings; the install step runs the right
prebuilt binary for your Node/electron ABI on most platforms.
cd ~/.config/opencode
npm i better-sqlite3
If the prebuilt binary does not match your platform, the install will print
node-gyp output. On Linux you may need build-essential, python3, and
make:
sudo apt-get install -y build-essential python3 make
If you need to pin a specific version, the same version OpenCode's bundled
Node loads is usually the safest choice. The plugin was developed against
better-sqlite3@13.0.3; anything >= 11.0.0 exposes the same surface.
3. Restart the OpenCode service
The background service caches module resolution, so a freshly installed package is invisible until the service restarts.
opencode service restart
opencode plugin list # expect a row for "sqlite" with source = local
If opencode plugin list does not show the plugin, check
~/.local/share/opencode/log/opencode.log and filter on the plugin
directory name. The two messages you are most likely to hit are:
Cannot find package '@opencode/plugin' …— step 1 was skipped.Cannot find package 'better-sqlite3' …— step 2 was skipped.
4. (Optional) Configure the plugin
In ~/.config/opencode/opencode.jsonc (or a project-local
.opencode/opencode.jsonc), add the plugin. If you are using a relative
path, remember the path resolves against the config file, not the working
directory.
{
"$schema": "https://opencode.ai/config.json",
"plugins": [
"./plugins/sqlite"
]
}
You can also pass options here:
{
"plugins": [
{
"package": "./plugins/sqlite",
"options": {
"maxRows": 5000,
"maxBytes": 10485760,
"defaultMode": "create",
"allowedRoots": ["/home/me/data", "/tmp"]
}
}
]
}
| Option | Default | Meaning |
|---|---|---|
maxRows |
1000 |
Hard row cap on sqlite_query. Caller can ask for less but not more. |
maxBytes |
5242880 (5 MiB) |
Hard byte cap on the JSON-serialised result of sqlite_query. |
defaultMode |
"create" |
Open mode when sqlite_open is called without mode. |
allowedRoots |
[] (no restriction) |
Absolute or cwd-relative roots; paths outside are rejected. |
If you copied this plugin to a different location, replace the path in the example above with wherever you put it. The plugin files are pure TypeScript — they are loaded by OpenCode's own transformer, so no separate build step is needed.
Usage
A typical session goes: open → inspect → query → modify → close.
> open ./data/app.db as alias "app"
> tables on "app"
> schema of users on "app"
> query "SELECT id, email FROM users WHERE created_at > ?"
on "app" with params ["2026-01-01"]
> exec "UPDATE users SET email = ? WHERE id = ?"
on "app" with params {"email": "new@example.com", "id": 42}
> script "BEGIN; DELETE FROM sessions WHERE expired < strftime('%s','now');
INSERT INTO audit(message) VALUES ('cleanup'); COMMIT;"
on "app"
> close "app"
Modes for sqlite_open
| Mode | Behaviour |
|---|---|
create (default) |
Open if exists, otherwise create. Read-write. |
rw |
Open read-write; fail if the file does not exist (fileMustExist: true). |
ro |
Open read-only. sqlite_exec and sqlite_script reject writes; sqlite_query works. |
memory |
In-memory database regardless of path. |
Relative path values resolve against OpenCode's current working directory
for the session (ctx.location.directory). Absolute paths are taken as-is.
Use :memory: to skip filesystem entirely.
Parameters
sqlite_query and sqlite_exec accept a params field that is either:
- An array of values for
?placeholders (in order), or - An object of
{ name: value, … }for:name/@name/$nameplaceholders. The name is given without the leading sigil; better- sqlite3 maps any of the three prefix styles in the SQL to the same name.
Examples:
// positional
params: [42, "alice"]
// named (matches ":id", "@id", or "$id" in the SQL)
params: {"id": 42, "name": "alice"}
Reading large results
sqlite_query returns rows lazily. When the result is truncated, the
output ends with (truncated by cap of N rows / M bytes). To get the
rest, either:
- Add a tighter
WHEREclause and re-run, or - Raise
maxRows/maxBytesin the plugin config, or - Run several
sqlite_querycalls withLIMIT/OFFSETin the SQL itself.
Reading BLOBs
BLOB columns are returned as {"__b64__": "...", "__bytes__": N}. The
base64 string is the raw bytes; the byte count is provided so the model
can decide whether to read them or skip.
Errors
Every tool returns a string. Errors are reported on a single line starting
with Error: and include the SQLite error code when available, for
example:
Error: SQLite error SQLITE_CONSTRAINT_UNIQUE: UNIQUE constraint failed: users.email
The model should be told (or already knows) that this prefix means "the operation failed; do not pretend it succeeded."
File layout
This plugin is a single directory:
~/.config/opencode/plugins/sqlite/
├── index.ts # the plugin
├── README.md # this file
└── SETUP.md # the install steps in copy-paste form
To move it to another machine, copy the whole directory, then run the
steps in SETUP.md (or the "Install" section above) on the new machine.
Nothing in the plugin hard-codes the absolute path of the OpenCode config.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
opencode plugin list does not show sqlite |
Plugin directory is not in plugins; or .git was created inside plugins/sqlite (that turns the directory into a different plugin shape and OpenCode fails to load it). |
Add ./plugins/sqlite to plugins in opencode.jsonc; remove any .git inside the plugin dir. |
failed to load plugin … Cannot find package '@opencode/plugin' |
Step 1 of the install was skipped. | cd ~/.config/opencode && npm i @opencode/plugin@<version>. |
failed to load plugin … Cannot find package 'better-sqlite3' |
Step 2 was skipped, or npm i was run in the wrong directory. |
cd ~/.config/opencode && npm i better-sqlite3. |
Plugin loads but sqlite_open reports Cannot find module 'better-sqlite3' at runtime |
Same as above, but only surfaces when the tool actually runs. | Re-run npm i better-sqlite3 inside ~/.config/opencode. |
node-gyp build fails during install |
Missing C toolchain. | sudo apt-get install -y build-essential python3 make (Debian/Ubuntu), or the equivalent for your distro, then npm i better-sqlite3 again. |
SQLITE_READONLY_DBMOVED or attempt to write a readonly database |
Connected in ro mode. |
Re-open with mode: "create" (the default). |
sqlite_open reports Path … is outside the configured allowedRoots |
The plugin is configured with allowedRoots and the path is outside them. |
Add the parent directory to allowedRoots, or remove the option. |
sqlite_exec reports SQLite returned SQLITE_SCHEMA … |
A cached prepared statement went stale because the schema changed under it. | Re-run the same call; the plugin retries with a fresh prepare. If it persists, the statement is genuinely incompatible with the current schema. |
For server-side plugin logs, use
opencode --standalone --print-logs (the --print-logs flag alone prints
client logs; plugin loading shows up in the service log at
~/.local/share/opencode/log/opencode.log).
Notes
- The plugin does not enable or disable any of OpenCode's existing providers, models, hooks, or tools. It only adds eight new tools.
- better-sqlite3 is synchronous. Open runs in milliseconds; a single
query on a normal-sized database runs in well under a second. Heavy
long-running writes are not a great fit — they will block the plugin's
executeuntil they complete, which is the only model the better- sqlite3 API offers. - All write tools refuse to run on a connection opened in
romode, including the implicit-write operations insidesqlite_script. - The plugin keeps connections in memory. They are closed on OpenCode shutdown. The cleanup hook also runs on plugin reload, so editing the file in place does not leak file descriptors.