Structure-aware edits to Markdown documents

Address a heading, block, or frontmatter field โ€” then read it back, or append, replace, and move content. No line numbers, no regex, no sed.

$ npm install markdown-patch
Use the library Use the CLI One engine, two front doors โ€” mdpatch ships in the box.
Examples

The instruction

notes.md โ€” after


        

Now run it on your own document

Everything above is a canned example. This is the actual npm package, bundled for the browser and running on this page โ€” paste your own Markdown, write your own instruction, and watch the engine work. Nothing is uploaded; there is no server.

โ— Try it markdown-patch, running in your browser

Document map โ€” the addresses projectMap / print-map give you. Click one to target it.

Options โ€” every value the engine accepts for these fields. Click one to set it.

result.document


            
          

The playground bundle isn't built in this checkout โ€” npm run build:site produces it, and the Pages workflow builds it on every deploy.

Errors are the engine's own โ€” InvalidInstructionError, TargetNotFoundError, PreconditionFailedError โ€” thrown by the same code an npm install gets you. Click the version token to send it as ifMatch, then edit the document out from under the instruction to watch a stale write fail.

The failure modes you already know, closed off

The obvious ways to edit Markdown programmatically all break on contact with real documents. Each one is handled by the engine, not by your code.

Whitespace is library-owned

The engine supplies every separator, so "X", "X\n", and "\nX\n" all produce the same document. One missing newline can never merge two paragraphs again.

Heading levels are relative

A # Section in your content lands as a direct child of wherever it's written โ€” the engine rebases levels, so pasted subtrees always fit their new depth.

Stale writes fail cleanly

Pass ifMatch with the document's version token. If the file changed under you, the patch throws instead of landing in the wrong place.

Duplicates are addressable

Two identical ### Fixed headings? The document map hands each occurrence a distinct address โ€” a regex matches both, a path names exactly one.

Tables are structured

Append rows as string[][] โ€” cells are content, not source. Pipes are escaped for you and column counts are checked.

Reads mirror writes

readTarget takes the same address as patch. Read at a scope, replace at that scope with the value unchanged: a guaranteed no-op.

Write it โ€” or just read it

Every address you can patch, you can also query. The bundled mdpatch CLI and the TypeScript API drive the same engine: two commands write, two retrieve.

commandwhat it does
mdpatch patchwritequick flag-based form for common single edits
mdpatch applywritefull instruction JSON โ€” moves, table rows, ifMatch pipelines
mdpatch queryreadprint a target's content: a section, a block, a frontmatter value
mdpatch print-mapreadshow everything addressable โ€” headings, blocks, fields, version token

In the library, readTarget is the mirror image of patch โ€” the same (targetType, target) address, read instead of written. Pull one section out of a note without parsing anything yourself. Full signatures for every function and type live in the API reference.

Built for editors that aren't people

An LLM agent shouldn't re-emit a 4,000-token file to add one paragraph โ€” and with markdown-patch it can't mangle the 3,900 tokens it had no business touching. This is the engine behind Obsidian Local REST API's PATCH endpoints and MCP tools, where exactly that kind of client is the norm.

Output tokens to add one paragraph to a 4,000-token note

Re-emit the whole file
~4,000
Re-emit the section
~950
One patch instruction
~60

Illustrative counts for a typical meeting note. Retrieval is cheap too: the agent reads the compact document map, not the whole file.

1 ยท Map

print-map hands the model a few dozen tokens describing everything addressable โ€” including distinct addresses for duplicate headings โ€” plus a version token.

2 ยท Target

The model picks an address straight off the map and, if it needs context, query-reads just that section โ€” no full-file round trips.

3 ยท Patch

One small instruction lands the edit. Sent with ifMatch, a stale write throws instead of landing in the wrong place โ€” and retrying from a fresh map is the agent's native gesture.