edit_note
Modify existing notes in an Obsidian vault using full replacement, append, find-and-replace, or section-body edits, preserving frontmatter and writing atomically.
Instructions
Edit an existing note in the Obsidian vault. Requires write permission — a readwrite API key, or an OAuth token
carrying the readwrite scope.
See get_vault_guide for Obsidian syntax and any vault-specific conventions
(naming, folder placement, frontmatter, tags).
Four mutually exclusive modes (set at most one of append/find/section):
Full replace (default): provide only
content.contentbecomes the note's body; an existing valid line-1 YAML frontmatter block is preserved byte-identically ahead of it. Passreplace_frontmatter=Trueto overwrite the entire file, frontmatter included.Append:
append=True;contentis added at the end (preceded by a single newline).Find & replace:
find=<exact text>; replaced withcontent. Must match exactly once unlessreplace_all=True. This mode operates on the raw file, so it is the one mode that can edit frontmatter text in place.Section:
section=<heading>; replaces the whole body under the named ATX heading — see "Section mode: whatcontentreplaces" below. Use the path-style formParent/Childto disambiguate when the same heading appears more than once, or the#Nordinal form ("#7", 1-based document order) — the ordinal is the only form that can address duplicate headings sharing one parent, and it is the selector the outline of a truncatedread_noteadvertises. A bare "#N" always selects by position and is never shadowed by a heading whose text happens to be "#N"; reach such a heading by title with "Parent/#N". A selector resolves to the same section inread_noteas in this tool on any write this tool admits — that parity is about resolution, not about admission: see the two section-mode refusals below, where a section that reads fine is deliberately not writable. Setext (====/----) headings are not matched.
Frontmatter and the round trip. Read a note, edit the content field
of the response, pass it straight back to full replacement: the frontmatter
survives. No property of content's shape changes that — a body whose
first line is a thematic break ---, or which itself begins with a
complete mapping-shaped fenced block, is body. A note with no valid block
(no line-1 fence, or a malformed one) is replaced wholesale by default,
which is the repair path and needs no flag. To change the frontmatter
itself use set_frontmatter, or edit the raw block through find=; a
read_note response's frontmatter JSON view is a lossy convenience and
must never be written back.
The round-trip guarantee covers a complete, unwindowed whole-note read
only — read_note(path) with no section, offset=0 and truncated
false in the response. A truncated read must be paged to the end before it
is written back, or full replacement will replace the whole body with the
fragment.
Section mode: what content replaces.
In section mode
contentis the section's body: the text beginning on the line immediately after the matched heading line, running to the next heading of equal-or-shallower depth or to end of note. The heading line itself is never removed or rewritten.A section write replaces that body whole. Anything
contentdoes not resend is deleted — a blank line, a list, and a fenced code block sitting directly under the heading included. There is no third region between the heading line and the body that survives a write.So a blank line you want between the heading and its content belongs in
content(send"\ntext", not"text").read_note(path, section=...)is the matching read: its response carries the heading line in theheadingfield and the body in thecontentfield, and this tool takes exactly thatcontent. Pass the field through unchanged — there is nothing to split off and nothing to strip.Byte-identity holds for notes whose body newlines are LF. Every non-LF terminator inside the selected body (CRLF, or a lone CR) comes back as LF — the read path normalises and this tool writes raw bytes — whether the note uses one dialect throughout or mixes them. Terminators outside the selected body are untouched, so a round trip can leave a note with more mixed endings than it started with.
Section mode resolves headings over the frontmatter-stripped body, exactly
as read_note does, so #N ordinals agree between the two and a YAML #
comment inside the block is never selectable. A heading inside a fenced
code block is not a heading: fences count with up to three spaces of
indentation and a closer at least as long as the opener, and an unclosed
column-zero fence hides everything below it.
Two shapes refuse a section write outright, naming the problem and writing nothing:
a malformed frontmatter block (unclosed fence, YAML error, non-mapping) — the refusal names the defect and the
replace_frontmatter=Truerepair;a fence opener indented by one to three spaces that nothing below it closes — such an opener may sit inside a list item, whose code block ends where the item does, and this server does not parse container blocks, so it will not guess whether the text below is code or content. Close the fence or unindent it to column zero, then reissue.
Both refusals are asymmetric with reads on purpose: read_note(section=…)
and the truncation outline keep working on such notes, because a read
destroys nothing.
Flags:
operation="append": legacy alias forappend=True. This is accepted to prevent older clients from silently falling through to full replacement.operation="replace"explicitly selects full replacement.replace_all=True: withfind, replace every occurrence rather than failing on multiple matches. Ignored whenfindis unset.replace_frontmatter=True: full replacement overwrites the entire file including the frontmatter block. Combined with append/find/section it is an error and nothing is written.dry_run=True: compute the would-be result and return a unified diff without writing. Works for all four modes, and diffs the composed result.
Writes are atomic: the composed result is staged in the note's own directory,
flushed to disk, and published with a single same-directory rename, so a
crash mid-write cannot truncate the destination. The publish is optimistic,
not locked — the bytes this call read are compared against the file
immediately before that rename, so a note somebody else changed in the
meantime fails with File changed while editing: <name> and nothing is
written; re-read and retry. Structured frontmatter mutation is better done via
set_frontmatter — PyYAML serialization there discards YAML comments. A
path whose final component is a symlink is refused in every mode
(dry_run included), naming the link's target; symlinked folders inside
the vault work normally.
Args:
path: Vault-relative path to the note.
content: New body (full replace), replacement text, text to append, or
section body.
append: If True, append content to the end of the note.
operation: Legacy mode selector; accepts "append" or "replace".
find: Exact text to find and replace.
section: ATX heading text identifying the section whose body to replace.
Use Parent/Child to disambiguate repeated headings, or a "#N"
ordinal ("#7", 1-based document order) for duplicate siblings.
replace_all: With find, replace every match instead of requiring uniqueness.
dry_run: Return a unified diff and do not write.
replace_frontmatter: Full-replace only. If True, content replaces the
entire file including any frontmatter block. Default False
preserves an existing valid block.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| find | No | ||
| path | Yes | ||
| append | No | ||
| content | Yes | ||
| dry_run | No | ||
| section | No | ||
| operation | No | ||
| replace_all | No | ||
| replace_frontmatter | No |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| result | Yes |