Skip to content
This repository was archived by the owner on May 13, 2026. It is now read-only.
This repository was archived by the owner on May 13, 2026. It is now read-only.

[Bug] patch_vault_file replace corrupts sections containing markdown tables with inline code-span heading refs #83

Description

@folotp

Summary

When patch_vault_file is called with operation: "replace" and targetType: "heading" on a section whose body contains a markdown table, and one of the table cells contains a code span that looks like a heading (e.g. `## Other section` or `# Foo`), the replace operation does not anchor the section's end-boundary correctly. A fragment of the old cell content escapes the table and is rendered as an orphan ## … heading after the operation. Calling replace a second time to clean up the orphan duplicates the fragment instead of removing it.

This is distinct from #71 (universal silent append-EOF on targetType: "heading" due to leaf-only target lookup): the corruption fires inside the existing section structure rather than appending a new section at EOF, and the visible signature (orphan H2 from cell content, deduplication failure on retry) is consistent with a section-boundary scan that treats ^##\s lines as boundaries without respecting table or code-span syntactic context.

Environment

  • obsidian-mcp-tools 0.2.27
  • obsidian-local-rest-api 3.6.1
  • Obsidian 1.12.7
  • macOS Tahoe 26.4.1, Apple Silicon
  • Consumed via an MCP client

Reproduction (minimal fixture)

Note fixture-table-codespan.md:

---
title: fixture-table-codespan
---

## Journal

| Date       | Event                                                                                  |
| ---------- | -------------------------------------------------------------------------------------- |
| 2026-01-01 | Initial entry. Discusses the `## Links` section below and how `patch_vault_file` anchors section boundaries. |

## Links

- Parent

Call:

await mcp.call("patch_vault_file", {
  filename: "fixture-table-codespan.md",
  operation: "replace",
  targetType: "heading",
  target: "Journal",
  content: "| Date | Event |\n| --- | --- |\n| 2026-01-02 | Updated entry |\n"
});

Observed

  1. Response: "File patched successfully" (no error).
  2. The original ## Journal section is partially modified, but a fragment of the old cell content — including the literal `## Links` text — escapes the table and appears as an orphan ## Links H2 heading at the position where the table used to end.
  3. The legitimate ## Links section that already existed below remains in place, so the file now has two ## Links headings.
  4. Calling replace a second time on ## Journal to clean up: instead of removing the orphan, the fragment is duplicated. The orphan ## Links heading appears a second time. Each subsequent retry compounds the duplication.

The only way to recover the file is a full rewrite via create_vault_file.

Expected

  • ## Journal section content (between ## Journal and the next sibling heading ## Links) is fully replaced by the supplied content.
  • The body of the table cell is treated as opaque text — the `## Links` code span is not interpreted as a section boundary.
  • Repeated calls converge to the same final state, not duplicate.

Root-cause hypothesis

The end-boundary detection of a section appears to scan the body line-by-line and match any line satisfying ^#{1,6}\s as a sibling-or-ancestor heading boundary, without respecting:

  • Code-span context: text between backticks within a paragraph or table cell is not a real heading. A markdown-aware tokenizer (remark, marked, markdown-it) maintains this context; a regex-based boundary scan does not.
  • Table-row context: text inside a |...| table cell is a cell value, not block-level markdown.

Once the boundary is detected too early (at the `## Links` code-span line within the cell), the rewrite truncates the section before the table actually ends. The trailing portion of the cell content — everything between the false boundary and the real next heading — is left in the file as an orphan, and gets re-promoted to H2 on the next render because the surrounding table structure is no longer well-formed.

The duplication-on-retry is consistent with the same logic applied to an already-corrupted file: the retry's boundary scan now matches both the original `## Links` (still present in the body) and the orphan ## Links from the previous failed pass.

A proper fix probably lives upstream in markdown-patch (the section parser used by Local REST API), but the wrapper could mitigate by validating that the resolved section boundary is not inside a fenced code block / code span / table cell before issuing the replace.

Workaround

For any section containing a markdown table with code-span heading references:

  • Avoid patch_vault_file replace on that section entirely. Use create_vault_file to rewrite the whole note instead. Acceptable cost on small notes; for larger notes a two-step pattern (create_vault_file chunk 1 with overwrite, then append_to_vault_file chunk 2) works.
  • Avoid writing `## Foo` code spans inside table cells if you anticipate the section will be patched later. Reformulate as e.g. "the Foo section".
  • If the file is already corrupted, do not retry replace — it will compound. Skip directly to create_vault_file.

Related

Happy to provide additional fixtures or test against PR #72 once it lands.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions