v2.8.3
Gerstep/cybosv2.8.3Aug 16, 2026by github-actions[bot]
AI Summary
This release focuses on security improvements by preventing unauthorized command execution from project-local configurations and mitigating path traversal vulnerabilities. It introduces new trust management commands and updates the MCP server protocol to a newer revision.
Key Highlights
- Added `qmd trust`, `qmd trust list`, and `qmd trust revoke` commands to manage approvals for external paths and model URIs.
- Enhanced security by blocking file symlinks and glob patterns that attempt to resolve outside the collection directory.
- Updated MCP server to protocol revision 2026-07-28, making HTTP sessions stateless.
- Fixed `qmd collection add --glob` to properly handle glob patterns instead of silently falling back to defaults.
Breaking Changes
- MCP server protocol bumped to revision 2026-07-28, making HTTP sessions stateless (no `Mcp-Session-Id`).
- `qmd pull` no longer prints download progress bars by default.
- `search`/`query` behavior changed with `--full-path`: unresolved results now retain their `qmd://` URI and docid, whereas previously the docid was dropped.
New Features
- New `qmd trust`, `qmd trust list`, and `qmd trust revoke` commands to manage approvals.
- Environment variables `QMD_TRUST_UPDATE_HOOKS` and `QMD_TRUST_LOCAL_CONFIG` to opt into unattended execution.
- Environment variables `QMD_ALLOWED_ORIGINS` and `QMD_ALLOWED_HOSTS` to configure MCP server host validation.
Full Release Notes
## [2.8.3] - 2026-08-16
### Security
- `qmd update` no longer runs a project-local `.qmd/index.yml`'s `update:`
commands without approval (#886). That file arrives with a `git clone` and is
adopted automatically for any command run inside the tree, so cloning a
repository and running `qmd update` executed shell commands chosen by whoever
wrote it. On a terminal QMD now lists the commands and asks; with nobody to
ask it skips them and keeps indexing. Approvals are recorded per config file
and per command set in `<config dir>/trusted.json`, so editing a command — or
a `git pull` that rewrites one — asks again. New `qmd trust`,
`qmd trust list` and `qmd trust revoke` manage approvals, and
`QMD_TRUST_UPDATE_HOOKS=1` opts unattended runs back in. Commands in your own
`~/.config/qmd/*.yml`, including anything `qmd collection update-cmd` writes,
are unaffected.
- The same project-local trust gate now covers collection `path` values that
resolve outside the project and non-default `models.embed` / `models.rerank`
/ `models.generate` URIs (#889). In-project paths still index unattended;
out-of-project directories are skipped until `qmd trust`, and custom model
URIs are not loaded or downloaded. `QMD_TRUST_LOCAL_CONFIG=1` opts unattended
runs back in (and `QMD_TRUST_UPDATE_HOOKS=1` still does). `qmd collection
add` records trust as it writes, the same way `update-cmd` does.
- Indexing no longer follows file symlinks or glob `../` / absolute patterns
out of the collection directory. `fast-glob` already skipped symlinked
directories, but a file symlink (or a mask like `../**/*.md`) still resolved
via `realpath` and ingested the target. `qmd://` filesystem resolution uses
the same containment check, so `qmd://collection/../../../etc/passwd` no
longer produces a path outside the collection.
- `qmd mcp --http` now validates the `Origin` and `Host` headers on every
request and answers `403` when they name anything but a loopback address
(#881). Binding to localhost is no defence against the user's own browser:
a page can re-point its hostname at `127.0.0.1` (DNS rebinding) and read
the indexed corpus through `POST /query` or `POST /mcp`. Requests with no
`Origin` (curl, MCP clients, editors) are unaffected. Extend the allowlists
with `QMD_ALLOWED_ORIGINS` / `QMD_ALLOWED_HOSTS`, or set
`QMD_ALLOWED_ORIGINS=*` behind your own authenticating proxy. A wildcard
bind (`--host 0.0.0.0`) skips the host check and warns at startup.
### Changed
- Dependencies: `node-llama-cpp` 3.18.1 → **3.20.0** (llama.cpp b8390 → b10361, 2026-08-11). Also safe patch/minors: `picomatch` 4.0.4 → 4.0.5, `web-tree-sitter` 0.26.8 → 0.26.12, `tsx` 4.21.0 → 4.23.12, `vitest` 3.2.4 → 3.2.7. No zod/vitest major; `@modelcontextprotocol/server` stays 2.0.0 (no 2.x patch). `flake.nix` FOD hashes are not updated here.
- `generate` and query expansion now await `LlamaContextSequence.dispose()` before disposing the parent context. node-llama-cpp 3.20 made sequence dispose async; the library's context-onDispose path does not wait.
- MCP server now speaks protocol revision **2026-07-28** via the official
TypeScript SDK 2.x (`@modelcontextprotocol/server`). HTTP is sessionless
(no `Mcp-Session-Id`, no initialize handshake, no idle-session TTL / #816
reaper). Clients send version and capabilities in `_meta`; `server/discover`
is implemented; Streamable HTTP POST requires `Mcp-Method` / `Mcp-Name`
(mismatch → `-32020`); `tools/list` is deterministic and carries `ttlMs` /
`cacheScope`. 2025-era stdio clients still work (`serveStdio` dual-speak);
2025-era HTTP `initialize` is answered per-request without minting a
session. Existing tools (`query` / `get` / `multi_get` / `status`), stdio
EOF shutdown, and named-index daemon PIDs are unchanged. No release.
- `qmd pull` (and implicit model downloads in `embed`/`query`) no longer print
node-llama-cpp's download progress bar. The bar redraws every few kilobytes
and flooded agent transcripts with thousands of tokens (#776). Pass
`qmd pull --progress` to show it on an interactive terminal.
- `--full-path` no longer degrades silently when a result cannot be resolved on
disk (#785). A fallback there means the file moved or was deleted since the
last index, so `search`, `query`, `get` and `multi-get` now print a notice to
stderr naming how many results fell back and suggesting `qmd update`; stdout
stays machine-readable.
- `search`/`query` now decide per result whether to show the docid under
`--full-path`, matching `multi-get` and `get`: a result that resolved shows
its on-disk path and no docid, one that did not keeps its `qmd://` URI *and*
its docid, so it is still addressable. Previously the docid was dropped for
every row whenever the flag was set, leaving unresolved rows with neither a
usable path nor an identifier.
- `search --format csv` always emits the `docid` column, empty for rows that
resolved to an on-disk path. Under `--full-path` the header previously
dropped the column entirely — which also disagreed with the empty-result
header, always printed with `docid`. Column positions are now stable across
runs and formats.
### Fixed
- Concurrent first-open of a cold index no longer fails with
`table documents_fts already exists` on Bun/macOS. FTS5
`CREATE VIRTUAL TABLE IF NOT EXISTS` is not atomic across WAL
connections: two processes can both see a missing table on their
schema snapshot and the loser throws. Table create and legacy-schema
repair now use the same `BEGIN IMMEDIATE` + double-check as the FTS
sync triggers, and treat a concurrent "already exists" as success
when the table is present.
- Nix flake `qmd-node-modules` FOD hashes updated for x86_64-linux and
aarch64-darwin after the MCP SDK 2.0 bump. `nix build` / Nix GHA was
failing with a fixed-output hash mismatch.
- CJK FTS rebuild no longer skips leftover `fts5(name, body, content='documents')`
tables when `fts_cjk_normalized_version` is already stamped, and schema
repair now checks live FTS columns (`PRAGMA table_info`) as well as
`sqlite_master.sql`. The MCP HTTP test helper still seeds that legacy
table; `startMcpHttpServer` / `createStore` on it must not throw
`no such column: T.name` (#792 regression).
- `qmd collection add --glob` is no longer silently ignored. parseArgs ran
with `strict: false`, so OpenClaw's `--glob memory.md` (and any other
`--glob`) fell through, the default `**/*.md` was used, and a second
collection on the same path collided as a duplicate instead of indexing
the requested mask (#536). `--glob` is now an alias for `--mask`.
- The `bin/qmd` trampoline now execs `process.execPath` instead of
re-resolving `node` from PATH. Native addons (`better-sqlite3`) are
compiled for the Node that installed qmd; a version manager (nvm, fnm,
mise) selecting a different major in the working directory used to spawn
that other binary and fail with `NODE_MODULE_VERSION` / `ERR_DLOPEN_FAILED`
(#577 leftover; #319). `bun bin/qmd` still resolves `node` from PATH so
Node-ABI addons are not loaded into bun.
- `qmd update` / `qmd collection add` no longer swallow unreadable files
silently (#460). `readFileSync` failures (ETIMEDOUT on APFS compressed
files, EAGAIN, EACCES, …) still skip the file so the rest of the collection
indexes, but the CLI now warns with the path and error code and reports
the skip count. The SDK `update()` result includes `skipped`.
- The architecture diagram no longer draws Vec expansions into BM25 search
(#680). `lex` expansions are FTS-only; `vec` and `hyde` expansions are
vector-only. The original query still goes to both backends.
- `qmd bench` no longer runs to a wall of 0.00 when the fixture collection is
missing or empty (#716). It errors up front with the same "Collection not
found" / index hint as `qmd search -c`, and if every backend still scores
zero it warns on stderr to check `qmd ls`.
- `qmd cleanup` now reclaims the content and FTS space left behind after a
wrong-directory `qmd update`. Deactivating files (the next update in the
right directory) only tombstoned the `documents` rows; cleanup deleted those
rows and vacuumed, but never dropped the unreferenced `content` hashes and
never ran FTS5 `optimize`, so `documents_fts_data` kept the old bodies
(#550). Cleanup now deletes inactive docs, then orphaned content, then
compact FTS, then vacuum. `--dry-run` reports the content hashes too.
- Concurrent `query` calls with `rerank: true` on a cold MCP server no longer
race `ensureRerankContexts()`. Embed already serialized context creation;
rerank did not, so two overlapping first queries both saw an empty pool,
both created ranking contexts, and the inactivity timer disposed the loser
(`Object is disposed`, #682). Callers now await the in-flight create.
- Embedding-context pool size no longer assumes every GGUF costs 150 MB of
VRAM (the nomic-embed figure). Larger models such as Qwen3-Embedding-0.6B
are ~1190 MB per 2048-token context; opening 8 of those exhausted an 8 GB
card so `qmd query` failed with `Failed to create any rerank context` even
though the reranker itself was fine. The pool is now sized from the weight
file, and 1 GB is reserved for the reranker (#799). Default
embeddinggemma/nomic throughput is unchanged. `QMD_EMBED_PARALLELISM` still
overrides.
- Multi-collection `-c A -c B` (and SDK/MCP `collections: [A, B]`) no longer
searches globally then post-filters. A large unrelated collection could fill
the FTS/ANN top-k so the requested collections vanished, yielding false-empty
results even though each collection matched on its own. `searchFTS` /
`searchVec` now search each requested collection, then merge by score
(#775). Single-collection exact-scan (#791, #803) is unchanged.
- Query expansion no longer consumes caller `intent`, and a cached expansion
whose sub-queries all miss is dropped instead of replaying forever (#818).
Intent still steers reranking and snippet/chunk selection; it just no longer
enters the expansion prompt or cache key, where the model copied meta-language
("so I can compare spend settings") into lex/vec terms that matched nothing.
- Files whose names differ only in the characters the legacy slug collapsed to
`-` (spaces, underscores) no longer evict each other from the index (#717).
The handalized-path migration now skips any row whose path is still owned by
a file in the current scan, so it only adopts genuinely stale pre-2.6 rows.
`qmd get` and `qmd ls <prefix>` also match `_` and `%` in paths literally
instead of as SQL `LIKE` wildcards, so `qmd get 2026_06_16.md` no longer
returns a sibling `2026-06-16.md`.
- CLI `multi-get` and SDK/MCP `multi_get` now share one comma-list resolver.
Collection-prefixed paths (`qmd/docs/SYNTAX.md`) work in both transports,
unanchored `LIKE '%name'` no longer silently fetches a different document
for a filename fragment (`NTAX.md` ≠ `SYNTAX.md`), and ambiguous names
across collections error with the candidate list instead of `LIMIT 1` (#759).
- The Nix flake wrapper now seeds the same pre-import env as `bin/qmd`.
Nix installs exec `bun src/cli/qmd.ts` directly, so they previously skipped
the launcher: `qmd mcp` could leak llama/ggml native logs onto JSON-RPC
stdio, and Darwin CLI exits dumped a ggml Metal residency-set stack trace
after an otherwise successful query. The wrapper now quiets those logs for
`mcp` and sets `GGML_METAL_NO_RESIDENCY=1` on Darwin unless
`QMD_METAL_KEEP_RESIDENCY=1` (#723).
- Rerank context creation no longer swallows the real failure. A VRAM OOM or
corrupt model previously produced only `Reranker unavailable — skipping
reranking` (and a dead identical retry whose comment claimed it disabled
flash attention, which ranking contexts never supported). The warning now
includes the underlying message so the two cases are distinguishable (#782).
- The Nix flake package now ships `skills/` next to `src/` in `$out/lib/qmd/`.
`findPackageRoot()` walks up from the wrapped `src/cli/qmd.ts` looking for a
sibling `skills/` directory; without it, `qmd skill show` and `qmd skills list`
always failed with "QMD skill not found" on Nix-installed binaries (#722).
- `/release` step 1 no longer points at a missing script. `skills/release/scripts/release-context.sh`
now exists: it silently installs git hooks and prints version info, working-tree
status, commits and files since the last tag, `[Unreleased]`, and the previous
changelog entry. The skill's process list also drops the duplicate step 7 and
checks dependency updates before cutting the release (#796).
- `store.searchVec()` (and SDK `searchVector()`) now embed the query with the
store's pinned embed model instead of the global `QMD_EMBED_MODEL`. A store
created with a non-default `models.embed` previously failed with
`Dimension mismatch ... Expected N ... received M` and loaded the wrong
(often much larger) model at query time. Hybrid/precomputed/session search
paths were unaffected and stay unchanged (#690).
- `qmd collection add --mask "a.md,*.txt"` now indexes the union of each
pattern. The comma-separated form was documented and commonly guessed, but
the joined string was passed to fast-glob as one literal glob, so it
matched zero files with no error. Brace form `{a.md,*.txt}` is unchanged.
The same split applies on `qmd update` for stored comma-list masks (#557).
- NixOS / immutable-root installs no longer crash `qmd embed` with EACCES
when node-llama-cpp tries to compile llama.cpp into a read-only
`node_modules`. The flake wrapper puts Nix's glibc and libstdc++ on
`LD_LIBRARY_PATH` so prebuilt binaries can `dlopen` them, and
`getLlama()` uses `build: "never"` when the llama directory is not
writable (#574).
- CJK FTS rebuild no longer raises "database is busy" when flushing insert
batches. The streaming `.iterate()` cursor stayed open across
`BEGIN` on the same connection; the scan now uses keyset-paginated
`LIMIT` batches so each SELECT finalizes before the insert transaction
starts (#797).
- Quoted FTS phrases containing dotted tokens (e.g. `"1.0.21"`) now match the
indexed document. The porter unicode61 tokenizer stores dotted strings as
adjacent parts, but phrase sanitization stripped the dots into a single
token (`1021`) that could never hit. Dotted tokens inside quotes are split
into adjacent phrase terms, matching the bare-term rewrite from #563 (#757).
- `scripts/build.mjs` no longer passes `shell: true` to `spawnSync` on Windows.
With the default Node install path (`C:\Program Files\nodejs\node.exe`),
`cmd.exe` split the unquoted `process.execPath` at the space, the `tsc`
spawn failed, and `prepare` could still report success with no `dist/` —
leaving `bin/qmd` at "not built". The helper always receives a real binary
path plus an args array, so no shell is needed. A spawn error now prints
the missing binary path instead of failing silently. (#681)
- Case-sensitive collections no longer collapse distinct document identities that
differ only by path casing. The implicit `COLLATE NOCASE` legacy migration was
unsafe for filesystems that contain both `README.md` and `readme.md`; case-only
legacy migrations must now be explicit and operator-reviewed (#801).
- `cleanupOrphanedVectors` now runs its orphan count and both DELETEs in a
single immediate transaction. An interruption between the two DELETEs
(crash, `SQLITE_BUSY`) could desync `vectors_vec` from `content_vectors`,
leaving stale metadata rows that make a later reactivation of the same
content hash look already-embedded — so `qmd embed` skips it and the
document becomes silently unsearchable by vector, with no orphan left to
clean up (#766).
- `qmd embed` no longer splits a UTF-16 surrogate pair (emoji, etc.) across a
chunk boundary. A chunk ending or starting mid-pair produced an unpaired
surrogate in the chunk text, which some remote embedding APIs reject as
invalid JSON — permanently failing that chunk on every retry, since the
boundary calculation is deterministic. This covers both the character-based
chunker and `chunkDocumentByTokens`'s recursive re-splitting for
astral-plane-dense content, which could previously drive the char budget
low enough to reproduce the same split (#777).
- `insertContext` looks up `store_collections` by name. #754 retargeted the
query from the dropped `collections` table but left `WHERE id = ?`, and
`store_collections` has `name TEXT PRIMARY KEY` with no `id` column — the
call threw `no such column: id`. Matches `deleteContext` /
`updateStoreContext` (#853).
- `qmd collection add` with no path argument now errors with usage instead of
silently indexing the current working directory (#684). Pass `.` to index
CWD, matching the documented examples.
- `qmd status` reports orphaned embedding chunks, `qmd update` hints when they
exceed 10% of vectors, and `qmd cleanup --dry-run` previews what would be
removed. Incremental update still does not auto-prune vectors (a transient
empty mount would otherwise force a full re-embed) (#768).
- `qmd --index <name> mcp --http --daemon` now scopes PID/log files per index
(`mcp-<name>.pid`) and passes the resolved database path to the child, so a
named-index daemon no longer collides with the default `mcp.pid` or opens
the default store (#772).
- Opening a store no longer throws `SQLiteError: no such column: T.name` when
`documents_fts` is still the legacy `fts5(name, body, content='documents')`
schema. `CREATE VIRTUAL TABLE IF NOT EXISTS` left that table in place, and
the CJK FTS rebuild's `DELETE FROM documents_fts` compiled against
`documents.name`, which does not exist (#792).
- Rerank cache keys now include the resolved `models.rerank` URI, so swapping
the configured reranker no longer serves the previous model's cached scores
(#764).
- `vsearch -c <collection>` no longer returns empty results for small
collections crowded out of the global ANN candidate pool. `searchVec` now
exact-scans the collection's vectors with `vec_distance_cosine` when the
set is within 20k rows (ANN + post-filter cannot see collections that never
enter global top-k, and sqlite-vec caps `k` at 4096 so a larger multiplier
alone is not enough). Larger collections still use capped ANN over-fetch
(#791, #803).
- `multi-get --format files` now emits the docid as its own CSV field
(`#docid,path,...`) instead of prepending it into the path field with a
space (`#docid path,...`), matching `search --format files` and keeping
naive comma-splitting usable (#760).
- `qmd embed` now takes an exclusive process lock (`.qmd-embed.lock` next to
the index DB) so concurrent invocations no longer race on `vectors_vec`
and fail with `UNIQUE constraint failed: vectors_vec.hash_seq`. A second
embed exits early with `Another embed process is already running. Skipping.`
Stale locks from crashed processes are recovered via PID identity checks
(#825).
- windows prepare fix #778 keeps dist build #824
- `qmd mcp` (stdio) now shuts down gracefully when stdin reaches EOF instead
of orphaning to PID 1 when the parent MCP client dies (#751): the server
closes its transport, gives in-flight request handlers a bounded window to
settle, closes the store (which disposes its llama.cpp instance), and lets
the process drain via `process.exitCode` (no forced `process.exit()`, which
has caused exit-time native crashes before).
- `qmd --version` no longer reports an unrelated repository's commit (#787).
The commit was discovered at runtime with `git -C <installDir> rev-parse`,
and `git -C` walks *up*, so any install nested inside another checkout
reported that checkout's HEAD — a global npm install under a git-managed
prefix such as Homebrew's `/opt/homebrew` claimed Homebrew's commit as
qmd's. Identical tarballs reported different "commits" depending only on
where they were installed, which is why one issue can collect three distinct
hashes for the same published build. `scripts/build.mjs` now stamps the
commit it built from into `dist/cli/build-info.json` (suffixed `-dirty` when
built from a modified tree), and the runtime prefers that. Source checkouts
still resolve their own HEAD, but only after confirming the enclosing
repository is qmd's; anything else reports no commit rather than a
misleading one. The lookup also no longer interpolates the install path into
a shell string, so a path containing a space stops silently dropping the
commit, and `--version` from a build runs no subprocess at all.
- `qmd doctor` no longer false-positives `.etag` HTTP sidecars (written by
`qmd pull` next to each download) as invalid GGUF models. The model-cache
check now only inspects real `.gguf` files, so a sidecar that happens to
sort before the blob no longer poisons the report. #812
- `qmd mcp stop` and `qmd mcp --http --daemon` now verify that a pidfile PID still belongs to a qmd process before signalling it or refusing to start. Recycled PIDs (common after reboot) are treated as stale: the pidfile is unlinked instead of SIGTERM'ing an unrelated process or blocking daemon start with a false "Already running" error (#806).
- `qmd collection add` now rejects missing paths and regular files before
creating collection configuration or index state. The error reports both the
received and resolved path so malformed shell arguments can be corrected.
- Claude Code plugin: scope the plugin `source` to `./skills` so installs
copy just the skills (~50 KB) instead of the entire repository. Previously a
canonical install materialized ~230 MB / 9,000+ items into
`~/.claude/plugins/cache/` — including a full npm dependency install
triggered by the repo-root `package.json`. Both skills (`qmd` and `release`)
still ship, unchanged. (#790)
- Claude Code plugin: releases now bump the plugin version in
`.claude-plugin/marketplace.json` in lockstep with `package.json`. The
plugin cache is keyed on this version, and it had been stuck at `0.1.0`
since February — so installed plugins never received skill updates
(users who installed in February are still being served that snapshot
today, despite the qmd skill nearly tripling in size since). Also bumps
the plugin to `2.6.3` as a one-time catch-up so existing installs pick
up the current skill on their next `claude plugin update`. (#789)
### Changed
- `--full-path` no longer degrades silently when a result cannot be resolved on
disk (#785). A fallback there means the file moved or was deleted since the
last index, so `search`, `query`, `get` and `multi-get` now print a notice to
stderr naming how many results fell back and suggesting `qmd update`; stdout
stays machine-readable.
- `search`/`query` now decide per result whether to show the docid under
`--full-path`, matching `multi-get` and `get`: a result that resolved shows
its on-disk path and no docid, one that did not keeps its `qmd://` URI *and*
its docid, so it is still addressable. Previously the docid was dropped for
every row whenever the flag was set, leaving unresolved rows with neither a
usable path nor an identifier.
- `search --format csv` always emits the `docid` column, empty for rows that
resolved to an on-disk path. Under `--full-path` the header previously
dropped the column entirely — which also disagreed with the empty-result
header, always printed with `docid`. Column positions are now stable across
runs and formats.