Repository navigation
Fix(mcp): assistants stay inside the note folders - #23
Merged
Merged
Conversation
zn mcp's note and folder tools only checked that a path stayed inside the
vault. In a root-mode vault the inbox is the vault root, so .zennotes, the
attachements folder and the other buckets were all "subfolders of the
inbox", and the sources of moves and deletes were not classified at all. An
assistant could move .zennotes/vault.json into a note folder and read it,
plant notes in .zennotes, delete the attachments folder or the archive, move
.zennotes into the notes, archive, trash or delete vault.json, and with
delete_folder on "x/.." delete the whole vault: the subpath cleaned to the
bucket root and RemoveAll took it.
Every create, move, rename and delete of a note or folder now resolves both
ends the way the listings classify them, through the vault's
systemFolderPaths: no dot directory at any depth, and in root mode no
internal dir, attachments dir or other bucket reached through a subpath (a
case variant counts when it is the same directory on disk). The folder tools
refuse a subpath that resolves to the bucket root. read_note reads Markdown
notes only. A note outside the note folders is refused as before ("Note not
in a known folder"); a folder gets "Not a note folder: <subpath>". zn's own
commands share the vault code and get the same rules.
rename_folder now carries the folder's icon and color with it, as desktop
main and the server do, not only its Tasks entries.
The desktop's bundled MCP engine gets the same rules (ZenNotes/zennotes,
2.80.0). Symlinks that stay inside the vault, case-variant note paths and
absolute paths in errors are left for scoped access (#21).
adibhanna
added a commit
to ZenNotes/zennotes
that referenced
this pull request
Oct 30, 2026
The bundled CLI is the floor a desktop build guarantees, and 0.13.0 is the newest release (ZenNotes/tui#24). It answers ZenNotes/tui#21: zn mcp takes --allow-path, --deny-path, --read-only and --access and enforces them on every tool, the same contract this app's bundled MCP server follows since 969f50e. It also keeps every assistant out of hidden and system folders through moves, renames and subpaths (ZenNotes/tui#23), and puts a row zn base add makes into the picked rows of the view the database opens on, as + Row does in the grid. The pin is the release's terminal-release.json copied byte for byte after it verified against the zn-release-1 key this app trusts (cli-update-core.ts), naming source commit cae76aff (the v0.13.0 tag), integration protocol 1, and four archives that match both the manifest and checksums.txt. Staged through `npm run terminal:stage`; the native darwin-arm64 binary reports zn v0.13.0. The in-app manual's CLI card says 0.13.0 and what it adds.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 0 of the scoped MCP access design (#21): the hardening that has to land first, because a scope built on today's path checks is not a boundary.
What was wrong
zn mcp's note and folder tools only checked that a path stayed inside the vault. In a root-mode vault the inbox is the vault root, so
.zennotes, theattachementsfolder and the other buckets were all "subfolders of the inbox", and the sources of moves and deletes were not classified at all. Probed over stdio JSON-RPC on scratch vaults (both MCP engines behaved the same):move_note {path: ".zennotes/vault.json", targetFolder: "inbox", targetSubpath: "Projects"}, thenread_noteNote not in a known folder, vault.json in placecreate_note {folder: "inbox", subpath: ".zennotes"}.zennotes/planted.mdwrittenNot a note folder: .zennotesdelete_foldersubpathattachements/archive/Attachements(case variant, APFS)Not a note folder: ...rename_folder .zennotes -> Projects/internals,Projects -> .zennotes/xNot a note folder: ...delete_folder {folder: "inbox", subpath: "x/.."}(root mode)Cannot delete the top-level folderdelete_folder {folder: "archive", subpath: "x/.."}delete_note/archive_note/move_to_trashon.zennotes/vault.jsonarchive/vault.md/trash/vault.mdNote not in a known foldermove_noteinto subpathattachements,create_folder inbox "trash/x",write_note Projects/.hidden/x.mdread_note Projects/notes.txtNot a Markdown note: Projects/notes.txtrename_folder Projects -> WorkfolderIconsandfolderColorsrewrittenOrdinary create, move, archive, read, rename and delete of notes and folders behave as before.
What changed
All in
internal/vault, sozn's own commands get the same rules aszn mcp:notePath: every note a tool reads, edits, renames, moves, archives, trashes, duplicates, deletes or toggles a task in must classify as a note before it is touched: no dot directory at any depth, top segment classified throughsystemFolderPaths(never literal names), and in root mode not the internal dir, an attachments dir or another bucket. A case variant counts when it is the same directory on disk.bucketDir: every (bucket, subpath) a create, move, rename or delete names must classify back into that bucket by the same rules. The folder tools refuse a subpath that resolves to the bucket root, with their existing messages.ReadNotereads.mdfiles only.RenameFolderrewritesfolderIconsandfolderColors(<bucket>:<subpath>keys) along with the Tasks lists, as desktop main and the server do.The desktop's bundled MCP engine gets the same rules in ZenNotes/zennotes on the 2.80.0 branch, plus a realpath step in its path resolution (it followed a symlink out of the vault, which
SafeJoinhere always refused). Both engines now answer all 23 probe cases identically.Left for #21 Phase 1, unchanged here: symlinks that stay inside the vault, case-variant note paths (
journal/DIARY.md), and absolute paths in missing-note errors.Tests
internal/vault/path_safety_test.goandinternal/mcp/path_safety_test.go(the probe cases, through the vault API and through the real tools over the in-memory transport). All 7 fail onmainand pass here.go test -race ./...,go vet ./...,git diff --check,bash scripts/sandbox.sh --smoke: pass.