---
summary: obsidian_write_note refuses to clobber existing notes by default — opt in with overwrite:true; obsidian_list_commands moves behind OBSIDIAN_ENABLE_COMMANDS alongside obsidian_execute_command.
breaking: false
---

# 3.1.0 — 2026-04-29

A safety pass on whole-file writes plus a tighter command-palette gate. Service-layer error classification picks up the framework's `httpErrorFromResponse` for the 4xx/5xx fallback so the canonical status → code mapping (and `Retry-After` capture) replaces the hand-rolled ladder.

## Added

- **`obsidian_write_note` `overwrite` flag (default `false`).** Whole-file writes against an existing note now fail with `file_exists` (`Conflict`) and an actionable message pointing at `obsidian_patch_note` / `obsidian_append_to_note` / `obsidian_replace_in_note` for in-place edits, or `overwrite: true` for a deliberate full replacement. Section-targeted writes are unaffected — the flag is ignored when `section` is set.
- **`obsidian_write_note` `created` field in the output.** `true` when the call brought a new file into existence; `false` when it replaced an existing one or targeted a section. Surfaces in both `structuredContent` and the rendered `content[]` twin.
- **Typed error contract on `obsidian_write_note`.** Declares `file_exists` so clients can switch on `error.data.reason` instead of parsing message text.
- **`ObsidianService.noteExists()`** — HEAD probe that returns `true` on 2xx, `false` on 404, and surfaces other statuses through the normal error classifier so a 401 doesn't masquerade as a missing file. Bypasses `withRetry` — a HEAD probe shouldn't retry on 404.

## Changed

- **`obsidian_list_commands` is now opt-in alongside `obsidian_execute_command`.** Both register only when `OBSIDIAN_ENABLE_COMMANDS=true`; both are hidden otherwise. Previously `obsidian_list_commands` was always-on and only execution was gated, which advertised a discovery surface for capabilities the operator had explicitly disabled. The pair now travels together as `commandToolDefinitions`.
- **Service-layer 4xx/5xx fallback routes through `httpErrorFromResponse`.** The default branch in `ObsidianService.#throwForStatus` delegates to the framework helper for unhandled statuses so the canonical mapping (`500/501 → InternalError`, `502/503 → ServiceUnavailable`, `504 → Timeout`) and `Retry-After` capture replace the local `serviceUnavailable` ladder. The body is consumed before the helper runs, so the call passes `captureBody: false` and forwards a truncated copy via `data.body`. Wire-visible: 500s now land as `InternalError` (`-32603`) instead of `ServiceUnavailable` (`-32004`).
- **Dependency bump:** `@cyanheads/mcp-ts-core` 0.8.1 → 0.8.2.
- **`skills/maintenance/SKILL.md` v1.9 → v2.0.** Step 6's framework-adoption rule moved from "default adopt" to "auto-adopt every applicable site, in this pass." Documents an explicit asymmetry against third-party adoptions, a hard rule against scope/effort/marginal-benefit deferrals, and a valid-vs-invalid deferral table. Step 8's "Open decisions" section is reframed accordingly — empty is now the expected outcome of a clean framework upgrade.

## Fixed

- **`obsidian_list_commands` description** no longer claims it is always available — calls out the `OBSIDIAN_ENABLE_COMMANDS` gate.
