set_frontmatter
Modify YAML frontmatter keys in an Obsidian note while leaving the body intact; add or update fields, remove keys, and use an optional content hash to guard against overwriting concurrent changes.
Instructions
Mutate a note's YAML frontmatter without touching its body. Requires write permission — a readwrite API key, or an OAuth token
carrying the readwrite scope.
Parses the existing frontmatter, merges in updates (overwriting matching
keys, adding any new ones), then drops keys listed in remove. The note
body is preserved byte-for-byte. If the note has no frontmatter (no ---
fence on line 1), a fresh block is prepended ahead of the unchanged body.
A malformed block is refused, never worked around. An unclosed line-1
fence, YAML that fails to parse, and YAML that is not a mapping (null,
~, comments only, a list, a scalar) each return an error naming the
defect and pointing at edit_note(path, content, replace_frontmatter=True)
as the repair. Nothing is written — in particular no second block is
prepended above the broken one — and remove= refuses identically rather
than silently doing nothing. This is reported even for a call with no
updates and no remove. An empty fenced block (--- immediately
followed by ---) is valid: it is a valid empty mapping and is updated
in place.
Only an effective change writes. updates that set every named key to
the value it already holds (compared type-sensitively, so true is not
1) together with remove naming only absent keys report no changes and
leave the file byte-identical. Removing the last key removes the block
entirely — no fences, no separator, exactly the prior body.
Re-serialization uses yaml.safe_dump(default_flow_style=False, sort_keys=False, allow_unicode=True). Caveat: PyYAML does NOT preserve
YAML comments — any # comment in the original frontmatter will be lost on
the first set_frontmatter call.
A path whose final component is a symlink is refused, naming its target, so the frontmatter of an unnamed note is never rewritten; symlinked folders inside the vault work normally.
See get_vault_guide for vault frontmatter conventions.
expected_hash — binding a write to the bytes you read. Optional; omit
it and nothing changes. Pass the content_hash a read returned, verbatim
and canonical (sha256:<64 lowercase hex>), and the call is refused with
nothing written if the file changed in between. It is always the whole
file's hash, never a hash of the text you received. Two windows, both
live: expected_hash covers your read → this call's read, the server's own
pre-publication compare covers this call's read → its publication, and a
match on the first does not exempt the second. Every refusal ends with one
machine-readable MCP-REFUSAL {"code":…} line — stale_precondition (with
the file's current hash, ready to resend), concurrent_write,
no_incumbent, malformed_precondition, precondition_unavailable,
precondition_required — and each states what resolves it.
The comparison runs ahead of the malformed-block diagnosis and ahead of the
no-op check, so a stale base never yields "no changes" or a defect report
about bytes you have not seen. A write reports the content_hash of the
bytes this call published; a no-op publishes nothing and reports none.
Args:
path: Vault-relative path to the note.
updates: Mapping of keys to set. Use the empty dict (or omit) to skip.
remove: List of keys to delete from the frontmatter. Missing keys are
ignored (and, on their own, make the call a no-op rather than a
write).
expected_hash: The note's content_hash as you last read it. Refuses
the write, changing nothing, if the note has changed since.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | ||
| remove | No | ||
| updates | No | ||
| expected_hash | No |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| result | Yes |