| Name | Modified | Size | Downloads / Week |
|---|---|---|---|
| Parent folder | |||
| README.md | 2026-08-16 | 23.8 kB | |
| v2.8.3 source code.tar.gz | 2026-08-16 | 1.3 MB | |
| v2.8.3 source code.zip | 2026-08-16 | 1.3 MB | |
| Totals: 3 Items | 2.6 MB | 5 | |
[2.8.3] - 2026-08-16
Security
-
qmd updateno longer runs a project-local.qmd/index.yml'supdate:commands without approval (#886). That file arrives with agit cloneand is adopted automatically for any command run inside the tree, so cloning a repository and runningqmd updateexecuted 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 agit pullthat rewrites one — asks again. Newqmd trust,qmd trust listandqmd trust revokemanage approvals, andQMD_TRUST_UPDATE_HOOKS=1opts unattended runs back in. Commands in your own~/.config/qmd/*.yml, including anythingqmd collection update-cmdwrites, are unaffected. -
The same project-local trust gate now covers collection
pathvalues that resolve outside the project and non-defaultmodels.embed/models.rerank/models.generateURIs (#889). In-project paths still index unattended; out-of-project directories are skipped untilqmd trust, and custom model URIs are not loaded or downloaded.QMD_TRUST_LOCAL_CONFIG=1opts unattended runs back in (andQMD_TRUST_UPDATE_HOOKS=1still does).qmd collection addrecords trust as it writes, the same wayupdate-cmddoes. -
Indexing no longer follows file symlinks or glob
..// absolute patterns out of the collection directory.fast-globalready skipped symlinked directories, but a file symlink (or a mask like../**/*.md) still resolved viarealpathand ingested the target.qmd://filesystem resolution uses the same containment check, soqmd://collection/../../../etc/passwdno longer produces a path outside the collection. -
qmd mcp --httpnow validates theOriginandHostheaders on every request and answers403when 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 at127.0.0.1(DNS rebinding) and read the indexed corpus throughPOST /queryorPOST /mcp. Requests with noOrigin(curl, MCP clients, editors) are unaffected. Extend the allowlists withQMD_ALLOWED_ORIGINS/QMD_ALLOWED_HOSTS, or setQMD_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-cpp3.18.1 → 3.20.0 (llama.cpp b8390 → b10361, 2026-08-11). Also safe patch/minors:picomatch4.0.4 → 4.0.5,web-tree-sitter0.26.8 → 0.26.12,tsx4.21.0 → 4.23.12,vitest3.2.4 → 3.2.7. No zod/vitest major;@modelcontextprotocol/serverstays 2.0.0 (no 2.x patch).flake.nixFOD hashes are not updated here. -
generateand query expansion now awaitLlamaContextSequence.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 (noMcp-Session-Id, no initialize handshake, no idle-session TTL / [#816] reaper). Clients send version and capabilities in_meta;server/discoveris implemented; Streamable HTTP POST requiresMcp-Method/Mcp-Name(mismatch →-32020);tools/listis deterministic and carriesttlMs/cacheScope. 2025-era stdio clients still work (serveStdiodual-speak); 2025-era HTTPinitializeis 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 inembed/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). Passqmd pull --progressto show it on an interactive terminal. --full-pathno 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, sosearch,query,getandmulti-getnow print a notice to stderr naming how many results fell back and suggestingqmd update; stdout stays machine-readable.search/querynow decide per result whether to show the docid under--full-path, matchingmulti-getandget: a result that resolved shows its on-disk path and no docid, one that did not keeps itsqmd://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 csvalways emits thedocidcolumn, empty for rows that resolved to an on-disk path. Under--full-paththe header previously dropped the column entirely — which also disagreed with the empty-result header, always printed withdocid. 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 existson Bun/macOS. FTS5CREATE VIRTUAL TABLE IF NOT EXISTSis 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 sameBEGIN 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-modulesFOD 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 whenfts_cjk_normalized_versionis already stamped, and schema repair now checks live FTS columns (PRAGMA table_info) as well assqlite_master.sql. The MCP HTTP test helper still seeds that legacy table;startMcpHttpServer/createStoreon it must not throwno such column: T.name(#792 regression). -
qmd collection add --globis no longer silently ignored. parseArgs ran withstrict: false, so OpenClaw's--glob memory.md(and any other--glob) fell through, the default**/*.mdwas used, and a second collection on the same path collided as a duplicate instead of indexing the requested mask (#536).--globis now an alias for--mask. -
The
bin/qmdtrampoline now execsprocess.execPathinstead of re-resolvingnodefrom 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 withNODE_MODULE_VERSION/ERR_DLOPEN_FAILED(#577 leftover; [#319]).bun bin/qmdstill resolvesnodefrom PATH so Node-ABI addons are not loaded into bun. -
qmd update/qmd collection addno longer swallow unreadable files silently (#460).readFileSyncfailures (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 SDKupdate()result includesskipped. -
The architecture diagram no longer draws Vec expansions into BM25 search (#680).
lexexpansions are FTS-only;vecandhydeexpansions are vector-only. The original query still goes to both backends. -
qmd benchno 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 asqmd search -c, and if every backend still scores zero it warns on stderr to checkqmd ls. -
qmd cleanupnow reclaims the content and FTS space left behind after a wrong-directoryqmd update. Deactivating files (the next update in the right directory) only tombstoned thedocumentsrows; cleanup deleted those rows and vacuumed, but never dropped the unreferencedcontenthashes and never ran FTS5optimize, sodocuments_fts_datakept the old bodies (#550). Cleanup now deletes inactive docs, then orphaned content, then compact FTS, then vacuum.--dry-runreports the content hashes too. -
Concurrent
querycalls withrerank: trueon a cold MCP server no longer raceensureRerankContexts(). 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 queryfailed withFailed to create any rerank contexteven 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_PARALLELISMstill overrides. -
Multi-collection
-c A -c B(and SDK/MCPcollections: [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/searchVecnow 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 getandqmd ls <prefix>also match_and%in paths literally instead of as SQLLIKEwildcards, soqmd get 2026_06_16.mdno longer returns a sibling2026-06-16.md. -
CLI
multi-getand SDK/MCPmulti_getnow share one comma-list resolver. Collection-prefixed paths (qmd/docs/SYNTAX.md) work in both transports, unanchoredLIKE '%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 ofLIMIT 1(#759). -
The Nix flake wrapper now seeds the same pre-import env as
bin/qmd. Nix installs execbun src/cli/qmd.tsdirectly, so they previously skipped the launcher:qmd mcpcould 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 formcpand setsGGML_METAL_NO_RESIDENCY=1on Darwin unlessQMD_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 tosrc/in$out/lib/qmd/.findPackageRoot()walks up from the wrappedsrc/cli/qmd.tslooking for a siblingskills/directory; without it,qmd skill showandqmd skills listalways failed with "QMD skill not found" on Nix-installed binaries (#722). -
/releasestep 1 no longer points at a missing script.skills/release/scripts/release-context.shnow 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 SDKsearchVector()) now embed the query with the store's pinned embed model instead of the globalQMD_EMBED_MODEL. A store created with a non-defaultmodels.embedpreviously failed withDimension mismatch ... Expected N ... received Mand 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 onqmd updatefor stored comma-list masks (#557). -
NixOS / immutable-root installs no longer crash
qmd embedwith EACCES when node-llama-cpp tries to compile llama.cpp into a read-onlynode_modules. The flake wrapper puts Nix's glibc and libstdc++ onLD_LIBRARY_PATHso prebuilt binaries candlopenthem, andgetLlama()usesbuild: "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 acrossBEGINon the same connection; the scan now uses keyset-paginatedLIMITbatches 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.mjsno longer passesshell: truetospawnSyncon Windows. With the default Node install path (C:\Program Files\nodejs\node.exe),cmd.exesplit the unquotedprocess.execPathat the space, thetscspawn failed, andpreparecould still report success with nodist/— leavingbin/qmdat "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 NOCASElegacy migration was unsafe for filesystems that contain bothREADME.mdandreadme.md; case-only legacy migrations must now be explicit and operator-reviewed (#801). -
cleanupOrphanedVectorsnow runs its orphan count and both DELETEs in a single immediate transaction. An interruption between the two DELETEs (crash,SQLITE_BUSY) could desyncvectors_vecfromcontent_vectors, leaving stale metadata rows that make a later reactivation of the same content hash look already-embedded — soqmd embedskips it and the document becomes silently unsearchable by vector, with no orphan left to clean up (#766). -
qmd embedno 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 andchunkDocumentByTokens's recursive re-splitting for astral-plane-dense content, which could previously drive the char budget low enough to reproduce the same split (#777). -
insertContextlooks upstore_collectionsby name. [#754] retargeted the query from the droppedcollectionstable but leftWHERE id = ?, andstore_collectionshasname TEXT PRIMARY KEYwith noidcolumn — the call threwno such column: id. MatchesdeleteContext/updateStoreContext(#853). -
qmd collection addwith 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 statusreports orphaned embedding chunks,qmd updatehints when they exceed 10% of vectors, andqmd cleanup --dry-runpreviews 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 --daemonnow 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 defaultmcp.pidor opens the default store (#772). -
Opening a store no longer throws
SQLiteError: no such column: T.namewhendocuments_ftsis still the legacyfts5(name, body, content='documents')schema.CREATE VIRTUAL TABLE IF NOT EXISTSleft that table in place, and the CJK FTS rebuild'sDELETE FROM documents_ftscompiled againstdocuments.name, which does not exist (#792). -
Rerank cache keys now include the resolved
models.rerankURI, 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.searchVecnow exact-scans the collection's vectors withvec_distance_cosinewhen the set is within 20k rows (ANN + post-filter cannot see collections that never enter global top-k, and sqlite-vec capskat 4096 so a larger multiplier alone is not enough). Larger collections still use capped ANN over-fetch (#791, [#803]). -
multi-get --format filesnow emits the docid as its own CSV field (#docid,path,...) instead of prepending it into the path field with a space (#docid path,...), matchingsearch --format filesand keeping naive comma-splitting usable (#760). -
qmd embednow takes an exclusive process lock (.qmd-embed.locknext to the index DB) so concurrent invocations no longer race onvectors_vecand fail withUNIQUE constraint failed: vectors_vec.hash_seq. A second embed exits early withAnother 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 viaprocess.exitCode(no forcedprocess.exit(), which has caused exit-time native crashes before). -
qmd --versionno longer reports an unrelated repository's commit (#787). The commit was discovered at runtime withgit -C <installDir> rev-parse, andgit -Cwalks 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/homebrewclaimed 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.mjsnow stamps the commit it built from intodist/cli/build-info.json(suffixed-dirtywhen 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--versionfrom a build runs no subprocess at all. -
qmd doctorno longer false-positives.etagHTTP sidecars (written byqmd pullnext to each download) as invalid GGUF models. The model-cache check now only inspects real.gguffiles, so a sidecar that happens to sort before the blob no longer poisons the report. [#812] -
qmd mcp stopandqmd mcp --http --daemonnow 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 addnow 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
sourceto./skillsso 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-rootpackage.json. Both skills (qmdandrelease) still ship, unchanged. (#790) -
Claude Code plugin: releases now bump the plugin version in
.claude-plugin/marketplace.jsonin lockstep withpackage.json. The plugin cache is keyed on this version, and it had been stuck at0.1.0since 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 to2.6.3as a one-time catch-up so existing installs pick up the current skill on their nextclaude plugin update. (#789)
Changed
--full-pathno 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, sosearch,query,getandmulti-getnow print a notice to stderr naming how many results fell back and suggestingqmd update; stdout stays machine-readable.search/querynow decide per result whether to show the docid under--full-path, matchingmulti-getandget: a result that resolved shows its on-disk path and no docid, one that did not keeps itsqmd://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 csvalways emits thedocidcolumn, empty for rows that resolved to an on-disk path. Under--full-paththe header previously dropped the column entirely — which also disagreed with the empty-result header, always printed withdocid. Column positions are now stable across runs and formats.