Write the spec like a regular doc. Free to start, no credit card.
the specgit blog
Your Obsidian vault, edited together, exactly as you wrote it
· the specgit team
Picture a team vault in GitHub. Someone opens a meeting note to fix a typo, and by the time the change reaches review it touches forty lines. The task boxes marked [/] for "in progress" have become plain unchecked boxes, a callout has lost its fold marker, the list someone indents with asterisks now uses dashes, and the note no longer ends the way it did. None of that was anyone's intention. The editor made those changes on its own, and they now sit in the vault's history under the name of a person who only meant to fix a typo.
That is the problem we set out to remove. specgit is a browser editor for docs that live in GitHub, and a lot of the people who use it keep an Obsidian vault there. For them, an editor has one job that comes before any other, which is to leave their notes alone: it should change what you changed and keep everything else exactly as it was, down to the byte.
This post is about how we hold ourselves to that, and what it means for a team sharing a vault.
Why editors rewrite your notes
Obsidian notes are written in Markdown, the plain-text format where **bold** means bold and - [ ] is a task. A visual editor doesn't edit that text directly. Instead, it reads the note into its own model of a document, lets you work on that model, and writes Markdown back out when you save.
The trouble is that one document can be written many ways: a bullet can start with -, * or +, and emphasis can use asterisks or underscores. An editor that rebuilds the whole file from its model writes every one of those choices its own way, every time it saves. GitLab's rich text editor team described the same problem in public: change one word in a 2,500-line file, and nearly every list gets rewritten.
For an Obsidian vault it's worse, because a vault is full of syntax a general Markdown tool has never heard of: callouts, wikilinks, block references, custom task statuses, Dataview fields, Templater tags, Canvas boards. A generic editor tends to flatten or escape all of it.
How specgit saves instead
When you save in specgit, every block you didn't touch is written back with its original bytes, where a block means a paragraph, a heading, a list, a quote or a table. Only the block you changed goes through the editor's writer (and inside a list, only the item you changed), while the rest of the file is copied from what was already there.
Say someone opens the launch plan and changes a single word, turning "weekly" into "daily," and touches nothing else.
Here is what a lot of editors out there, and early versions of specgit, would hand back. It is the same note after that one-word change, run through a standard Markdown writer of the kind editors use to rebuild a file every time you save:
cssclasses: - wide-page ---+ # Launch plan -> [!summary] TL;DR-> Ship the editor to **three pilot teams** by ==Nov 15==. See [[Launch/Risks#Top risks|top risks]] and the [[Launch/Timeline]].+> \[!summary] TL;DR+> Ship the editor to **three pilot teams** by ==Nov 15==. See \[\[Launch/Risks#Top risks|top risks]] and the \[\[Launch/Timeline]]. ## Goals ^goals-1. Three pilot teams editing weekly++1. Three pilot teams editing daily 2. Zero data-loss reports- - Measured by support tickets- - And by [[Launch/Metrics#^retention|retention]]+ * Measured by support tickets+ * And by \[\[Launch/Metrics#^retention|retention]] %% Internal: pricing still open, do not publish %% -- [ ] Confirm pilot list 📅 2026-10-30 ⏫-- [x] Draft announcement ✅ 2026-10-20-- [/] Legal review in progress+* [ ] Confirm pilot list 📅 2026-10-30 ⏫+* [x] Draft announcement ✅ 2026-10-20+* \[/] Legal review in progress -See also: [[Team/Roles|who owns what]] and ![[Launch/Messaging#Positioning]].+See also: \[\[Team/Roles|who owns what]] and !\[\[Launch/Messaging#Positioning]].For a single changed word, Git shows eleven lines added and nine removed. The callout has become plain text, and every wikilink is escaped, so Obsidian no longer sees a link in any of them. The tab-indented list has turned into spaces and asterisks, the "in progress" task has stopped being a task, and all of it now sits in the vault's history under the name of the person who changed one word.
Here is the same change as specgit makes it today:
> Ship the editor to **three pilot teams** by ==Nov 15==. See [[Launch/Risks#Top risks|top risks]] and the [[Launch/Timeline]]. ## Goals ^goals-1. Three pilot teams editing weekly+1. Three pilot teams editing daily 2. Zero data-loss reports - Measured by support tickets - And by [[Launch/Metrics#^retention|retention]]The whole diff is a single line, because specgit makes sure everything you didn't mean to change stays exactly as it was: the callout, the block ID, the tab-indented list and every wikilink.
Before anything is saved, specgit reads its own output back and checks that it means exactly what the editor shows. If an edit would change how a neighboring block reads, specgit rewrites that neighbor too, so it never saves a file whose meaning differs from the document on your screen.
Two smaller decisions also do a lot of work behind the scenes.
- Anything the editor can't represent stays as you wrote it. If a note holds a construct the editor has no shape for, such as bold nested inside bold, specgit keeps that span as raw text instead of guessing.
- Line endings, a byte order mark, and how the file ends are part of the file. If your note uses Windows line endings or has no final newline, it keeps them.
What this means when a team shares a vault
To see what that looks like in practice, follow an ordinary Tuesday in a vault that one product team, two technical writers and a researcher share in GitHub.
Maya, a product manager, opens the roadmap note in specgit to change one date. The note is full of [/] and [-] tasks, a folded [!warning] callout and a Dataview field or two, but the date is all she needs to touch, so she edits it and saves. When her tech lead opens the review, there is exactly one changed line to look at. In GitHub, each save is a commit, a saved change with a name and a date attached, and the change she submits for approval is a pull request, GitHub's name for a set of changes that goes to review together. Both of them show that one line and nothing else.
Sam, a technical writer, works in Obsidian on his laptop with the Tasks and Linter plugins, so Linter tidies his list markers whenever he saves and Tasks stamps a done date when he finishes something. He pushes those changes to GitHub while Maya still has the note open. specgit notices and brings Sam's changes into the open note block by block: the blocks Sam changed update, Maya's edits stay where they are, and nobody sees a dialog, because nothing conflicted. specgit doesn't put back the list markers Linter changed, and Sam's next pull from GitHub brings him exactly what Maya wrote, with his own formatting intact. Only when Maya and Sam have changed the same paragraph does specgit ask anyone anything, and even then it shows just that paragraph.
Later that day the team opens their launch board, an Obsidian Canvas file: a big open canvas where each card is a note or a sticky, with arrows showing what depends on what. A board is where a single session quietly reaches into many files at once. Maya drags the café notes card a little down and to the right, changes "weekly" to "daily" on the launch plan card, and, while she is there, bumps a footnote on the risks card from three support threads to four. It feels like three small touches to one board, but they land in three separate files: the board itself and the two notes behind those cards.
For that session, a lot of editors out there, and early versions of specgit, would hand back this:
Launch map.canvas | 301 +++++++++++++++++++++++++++++++++++++----- Launch/Launch plan.md | 20 +-- Launch/Risks.md | 13 +- 3 files changed, 285 insertions(+), 49 deletions(-)The board goes through a generic JSON writer, which deletes every line of the file except the opening and closing braces and then writes the whole thing back out as 269 lines, so one moved card costs 301 changed lines. The diff opens like this:
@@ -1,36 +1,269 @@ {- "nodes":[- {"id":"g-launch","type":"group","label":"Launch","background":"attachments/diagram.png","backgroundStyle":"cover","x":-40,"y":-80,"width":1240,"height":700,"color":"4"},- {"id":"g-team","type":"group","label":"Team","color":"#2f9e44","x":1280,"y":-80,"width":480,"height":700},- {"id":"n-plan","type":"file","file":"Launch/Launch plan.md","x":0,"y":0,"width":360,"height":240,"color":"1"},- {"id":"n-time","type":"file","file":"Launch/Timeline.md","x":400,"y":0,"width":360,"height":240},- {"id":"n-risk","type":"file","file":"Launch/Risks.md","x":800,"y":0,"width":360,"height":240,"subpath":"#Top risks"},Further down the same diff is the card Maya actually moved, which used to be a single line on the board and now takes up nine:
+ {+ "id": "n-cafe",+ "type": "file",+ "file": "Café notes – ünïcode & (draft).md",+ "x": 460,+ "y": 700,+ "width": 360,+ "height": 240+ },The risks note comes back with every callout escaped into plain text, its wikilink broken and its inline footnote undone:
@@ -1,15 +1,16 @@ # Risks ## Top risks-> [!warning]- Data loss on concurrent edits-> Two editors in the same paragraph. Mitigation in [[Launch/Launch plan#^goals]].++> \[!warning]- Data loss on concurrent edits+> Two editors in the same paragraph. Mitigation in \[\[Launch/Launch plan#^goals]]. >-> > [!note] Nested+> > \[!note] Nested > > Track in #risk/data -> [!danger] Pricing confusion+> \[!danger] Pricing confusion > Customers misread the Free tier.[^1] -[^1]: Seen in three support threads.+[^1]: Seen in four support threads. -Inline footnote here.^[Added by Sam during review.]+Inline footnote here.^\[Added by Sam during review.]The launch plan, meanwhile, gets the same eleven-in, nine-out rewrite you saw earlier.
Three small edits have become 334 changed lines across three files, and every card on the board, along with every callout and wikilink in both notes, now sits in the vault's history under Maya's name. A reviewer has to dig through hundreds of lines of braces to find the one card that moved, and any teammate with their own change to the board now has a merge conflict waiting for them.
Here is the same session in specgit today:
Launch map.canvas | 2 +- Launch/Launch plan.md | 2 +- Launch/Risks.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-)@@ -11,7 +11,7 @@ {"id":"n-roles","type":"file","file":"Team/Roles.md","x":1320,"y":0,"width":360,"height":240}, {"id":"n-maya","type":"file","file":"Team/Maya Chen.md","x":1320,"y":300,"width":200,"height":160}, {"id":"n-sam","type":"file","file":"Team/Sam Ortiz.md","x":1540,"y":300,"width":200,"height":160},- {"id":"n-cafe","type":"file","file":"Café notes – ünïcode & (draft).md","x":400,"y":680,"width":360,"height":240},+ {"id":"n-cafe","type":"file","file":"Café notes – ünïcode & (draft).md","x":460,"y":700,"width":360,"height":240}, {"id":"n-img","type":"file","file":"attachments/diagram.png","x":800,"y":680,"width":300,"height":200}, {"id":"t-q","type":"text","text":"**Open question:** do we gate GA on ==zero== data-loss tickets?\n\n- [ ] decide by Oct 30, see [[Risks#Top risks]]","x":0,"y":680,"width":360,"height":180,"color":"6","x-plugin":{"foo":1}}, {"id":"l-docs","type":"link","url":"https://jsoncanvas.org/spec/1.0/","x":1320,"y":680,"width":360,"height":200},@@ -16,7 +16,7 @@ cssclasses: > Ship the editor to **three pilot teams** by ==Nov 15==. See [[Launch/Risks#Top risks|top risks]] and the [[Launch/Timeline]]. ## Goals ^goals-1. Three pilot teams editing weekly+1. Three pilot teams editing daily 2. Zero data-loss reports - Measured by support tickets - And by [[Launch/Metrics#^retention|retention]]@@ -10,6 +10,6 @@ > [!danger] Pricing confusion > Customers misread the Free tier.[^1] -[^1]: Seen in three support threads.+[^1]: Seen in four support threads. Inline footnote here.^[Added by Sam during review.]Each file gets one changed line, and it is exactly the line Maya changed, while the card's accented file name, every other card, the callouts, the wikilinks and the inline footnote stay byte for byte as they were. specgit changes exactly what you changed, in every file the session touched, and it writes the board the way Obsidian writes it, so when Sam later opens it in Obsidian there is nothing left for Obsidian to redo.
None of this is dramatic, and that is rather the point. Nobody's history fills with formatting churn, nobody's plugin work gets undone, and the people who'd rather work in Obsidian and the people who'd rather work in a browser can edit the same vault without fighting over it.
How we test it
Promises like "we don't change your notes" are cheap, so here is what we actually run.
Real vaults. Our test set has 140 files: 63 copied byte for byte from 12 public, openly licensed Obsidian vaults and plugin projects, plus 77 we wrote to isolate one Obsidian feature each. Beside it sits a larger test vault, built from about 20 public vaults, that maps 1,324 variations of Obsidian content across 21 categories (callout types, task statuses, wikilink forms, properties, code fences, canvas nodes and edges, file types) to a file that exercises each one.
Six checks on every test file.
- Open and save with no edits, and the file must come back byte for byte identical.
- Type one word into one block, list item or nested item, and only that block's lines may change.
- Let another tool change the file first (a Tasks completion, a new property, a renamed link, a Linter rewrite), and specgit must keep that change.
- Alternate edits between specgit and an Obsidian-style edit for five rounds, and the file must end up with only the intended edits, no formatting going back and forth.
- Make one edit in specgit and another in the raw file, and Git's own merge must combine them cleanly.
- Make an edit in a live specgit session while someone else's change arrives from GitHub, and both must survive with no prompt.
Each check runs three ways: in one browser tab, after a second person joins the same live document, and after a reload, because that is where collaborative editors tend to break.
Every CommonMark and GitHub example. The two published specifications for Markdown include 1,322 worked examples between them. We run each one through specgit, with no edit and again with a word added, and compare what it renders to.
Obsidian itself. For the final word on how Obsidian reads a file, we ask Obsidian. Our test harness opens the real Obsidian app with the test vault, records how it reads every note (its links, tags, tasks, properties, headings, and what its reading view shows), then reads specgit's saved version and compares. Across our 140 test files and the 103 files of the larger vault, an unedited specgit save changed nothing Obsidian reads. A word typed into a paragraph or a list item changed Obsidian's reading inside that paragraph or item, and nowhere else. That's how we learned, for example, exactly when Obsidian rewrites a Canvas file and in what order it writes each card's details, so specgit can write boards the way Obsidian does.
The checks and the spec examples run on every proposed change to specgit's code and again before every release, and a change that breaks them can't ship. The Obsidian comparison, over the test set and the larger vault, runs whenever we change how specgit saves a file.
Why it matters
We believe a team's docs belong where the work lives, in a repo the team owns, with GitHub as the source of truth. An editor earns its place there the way a good guest does, by doing what you asked and leaving the rest of the house as it found it. If you keep your vault in GitHub, try specgit on a note you know well: change one word, and then look at what it saves.