Repository navigation
Safety and Ownership Model
This page states precisely what the system can and cannot do to the bookmarks in your browser, and names the mechanism that enforces each guarantee.
The README gives the rules in a sentence each. Here the question is how each one holds, what it protects against, and what happens at the edges. The two appliers are described in Architecture; the exit codes, log lines and recovery steps are in Operations and Troubleshooting. The verification behind the claims here, and the ones still marked untested, is in Research Status.
What the system can do to your browser bookmarks:
- add a bookmark it created, marked as its own, into a folder you built by hand;
- create a folder, marked as its own, at a path that did not exist;
- leave a bookmark it wanted to add out, because a node with the same URL already exists anywhere in the tree;
- via one explicit, user-invoked path, remove a managed folder that it owns end to end so that it can be rebuilt (see "The one removal path").
What it cannot do, by construction:
- move, rename, re-order or remove any node you created;
- move, rename or re-order any node it created, either (the shipped appliers only append);
- delete a browser bookmark because the corresponding item vanished from Raindrop;
- write the file at all if any preflight check fails;
- leave a torn or half-written file on disk;
- declare success without re-reading the file from disk and checking it.
The rest of the page is the mechanism behind each line.
Every node the file writer creates carries one extra field:
"meta_info": {"raindrop_sync": "v1"}The writer's test for ownership is exactly that field with exactly that value (marked() in apply_brave.py). It may add marked nodes. In the design it may remove marked nodes. It may do nothing at all to an unmarked node, and everything you made by hand is unmarked. Nodes are found by walking all three permanent roots (bookmark_bar, other, synced) and testing the marker on every node; the writer never remembers where a node was.
meta_info is a dictionary Chromium's own bookmark codec reads and writes, so the marker rides along inside the format rather than beside it. The load-bearing question was whether Brave preserves it through its own re-serialisation, since Brave rewrites the whole file from memory after every change. It does: seven of seven marked nodes survived Brave's round-trip on Brave 152. That single result is what makes the model viable, and it is a Chromium implementation detail with no compatibility promise. Re-verify it after every major Brave upgrade; it is the assumption most likely to be broken by an upstream change.
Most sync tools solve ownership by quarantine: one folder, everything the tool wrote goes in it, and the tool owns the folder. That is safe, and it makes the result nearly useless, because the whole value of a curated bar is that things are where you put them. Marker ownership is what allows this project to file a promotion into your Woodworking folder. The folder is yours, unmarked, untouchable. The bookmark inside it is the tool's, marked, and the only thing the tool will ever act on there.
The other tempting shortcut is positional: "our node is the third child of the second folder on the bar", remembered from the last run. It fails silently the first time you drag a folder. The remembered index now points at something else, the lookup misses, the writer concludes its node is absent and creates it again, and you have a duplicate. Then Chromium finishes the job: on encountering two nodes with the same GUID it re-randomises one of them, which severs the identity anchor for that node permanently. The writer can no longer tell which of the two it made.
So the design rule is a full-tree walk on the marker, never a position. Zero marker matches for a given identity means create. More than one means abort and let a human look, because a duplicate marked node is evidence something already went wrong. In the shipped writer that second half is enforced after the fact: the post-write verification fails, and restores the backup, if any GUID appears twice.
Marker says whose a node is. GUID says which one it is. Every node the file writer creates gets a deterministic GUID derived from its source: uuid5(NS, "url:" + raindrop_id) for a bookmark, uuid5(NS, "folder:" + path) for a folder, with NS a fixed namespace UUID in the script. The same staged item always maps to the same GUID, so a re-run recognises its own earlier work instead of churning. Chromium's numeric id is deliberately not used as identity: it is a per-file allocation detail, and on browser versions that still read the checksum a mismatch triggers a recovery pass that renumbers every node in the tree (a claim from Chromium source; it cannot be exercised on Brave 152, which no longer reads the checksum). GUIDs survive that; ids do not.
The extension applies through chrome.bookmarks, and that API has no metadata field. It cannot write meta_info, and it cannot read one that the file writer wrote. So a node the extension created looks, to a marker-only check, like a node the user made. The file writer would then "discover" the promotion missing and create it a second time.
The fix is that the file writer treats a promotion as already present if either of two things is true: a marked node with the expected GUID exists, or any node anywhere in the three roots has the same URL. The extension makes the same check from its side, searching the live model for the exact URL before creating. Each applier therefore skips whatever the other has already done, which is what makes it safe to run both at once.
The URL match has a consequence worth knowing about. If you bookmarked a page yourself, anywhere, and Phase A later promotes the same URL, neither applier will add a second copy. The promotion is treated as satisfied by your own bookmark, wherever it sits. Nothing is moved to make it "correct".
Because the marker cannot cross the API boundary, each applier keeps its own record of what it has applied:
| applier | how it knows a node is its own |
|---|---|
| file writer |
meta_info marker on the node, plus ~/.raindrop-sync/applied.json listing the GUIDs it last wrote |
| extension | a row in state.db, table extension_applied, keyed (client, raindrop_id), written by the native host |
The client half of that key matters if the extension runs in more than one browser: with a bare raindrop_id key, the first browser to sync would record the item and every other browser would be told nothing is pending. A worker running older code sends no client and is filed under unknown, which costs one redundant "already present" and nothing else. An item whose extension result was error is not recorded as done, so it is offered again on the next tick.
Before the file writer reads a single promotion into the tree it checks the profile, and any failure ends the run with exit code 2 and a log line naming the reason. None of these is a crash. Each is a deliberate refusal, and none of them is followed by an attempt to repair.
| check | log line | what it protects against |
|---|---|---|
Bookmarks does not exist |
ABORT: Bookmarks missing. Refusing to create one. |
A missing file is a symptom of something else, and a freshly created file is indistinguishable from the wipe the writer exists to prevent. The writer never creates a bookmarks file under any circumstances. |
AccountBookmarks, EncryptedBookmarks2 or EncryptedAccountBookmarks2 exists in the profile |
ABORT: <name> present |
Those files mean a different storage regime (account-bound or encrypted bookmarks) that this writer does not model. Editing Bookmarks alone in that situation would be editing half a picture. |
version is not 1
|
ABORT: unexpected version |
The format has changed, or the file is not what it claims to be. A version other than 1 is one of the exact conditions under which Chromium silently discards the file (see the catastrophic case). |
any of bookmark_bar, other, synced missing from roots
|
ABORT: missing root |
The other silent-discard condition. A file missing a permanent root is already broken; writing into it would only make the break ours. |
a top-level sync_metadata key exists |
ABORT: Brave Sync is enabled |
Injected nodes trip CorruptionReason::UNTRACKED_BOOKMARK in the sync layer. Brave discards its sync metadata, re-merges, and uploads the injected folders to every device on the chain. Nothing is damaged by the abort, and nothing applies until sync is turned off. |
| the recomputed checksum differs from the stored one | ABORT: stored checksum does not match |
Something else is editing this file. Brave 152 itself no longer reads the checksum, so the writer uses it purely as a tripwire: a file Brave wrote will always verify, and one that does not was touched by a third party. |
The checksum check is only possible because the walk is reproduced exactly: an MD5 over a pre-order walk of the tree, titles as UTF-16LE, everything else as UTF-8. Get that wrong and the tripwire fires on every healthy file. RESEARCH.md finding 1 has the implementation and the verification in both directions.
Two things are deliberately not preflight conditions:
- Brave running. The writer does not wait for Brave to close, for reasons covered under "Losing the race". A running Brave costs the writer its own additions at worst, and gating on it would leave the fallback path unable to make progress on a machine where the browser is always open.
- The checksum being wrong in a way the writer could fix. It could recompute and carry on. It refuses instead, because a wrong checksum is evidence of a concurrent editor, and the writer has no way to know what else that editor changed.
Before every write the file writer takes its own copies of both Bookmarks and Bookmarks.bak into ~/.raindrop-sync/backups/, each named with a UTC timestamp, and keeps the last ten. It then re-opens the copy of Bookmarks and parses it. A backup you have not read is a hope, not a backup; if the copy does not parse, the run fails before anything is written.
The reason for owning backups at all is that the file Chromium keeps for the purpose is not one. Bookmarks.bak is a copy of whatever was on disk at the first save of the current browser session, taken once per session behind a latch. It is not a rolling history and it is not checked for validity. The sequence that makes it useless as a safety net is short: something writes Bookmarks; Brave starts, reads it, and on its first save copies that file over Bookmarks.bak; the previous good copy is gone. One session is all it takes. And nothing in Chromium ever reads .bak back: there is no restore path in the product, automatic or manual, behind a flag or in the UI. The file exists for a person with a terminal, and only until the next session starts. That is finding 5, established from Chromium source and adopted as a constraint rather than reproduced, which Research Status records as such.
So: assume Bookmarks.bak holds your own most recent output, and build nothing on it.
The write itself is a fixed recipe, and every step of it exists to answer a specific failure:
mkstemp in the profile directory same volume, so the final rename is atomic
-> write the JSON
-> flush, then os.fsync the file the bytes are on disk, not in a buffer
-> chmod 0600
-> os.replace(tmp, Bookmarks) one rename: the old file or the new one, never a mixture
-> os.fsync the directory the rename itself is durable
-> unlink the temp file on any exception
A temp file in /tmp would be on a different volume, and a cross-volume "rename" is a copy, which can be interrupted halfway. Writing in place would leave a half-written file if the process died mid-write, and a half-written file is exactly the catastrophic case below. The rename is the whole guarantee: at every instant, Bookmarks is a complete file.
Then the writer re-reads the file from disk and verifies five things:
- it parses;
- the checksum recomputed over the tree equals the stored one;
- every
idis unique across all three roots; - every
guidis unique; - every promotion it set out to apply is present, by GUID or by URL.
If any check fails, it copies the backup it just took back over Bookmarks and exits 1. If they all pass it records the GUIDs in applied.json, logs OK, and, if a Brave process is running, adds the note that the write may be overwritten and will be re-applied on the next tick.
What the re-read does not prove is that the nodes will still be there tomorrow. It proves the file on disk is well-formed and contains them. Whether Brave keeps them depends on the race.
Both appliers are additive-only, and this is the design rather than an omission. The file writer as shipped only appends nodes. The extension only creates what is missing. Neither has a code path that removes a bookmark because it stopped being wanted, and CONTRIBUTING.md lists "anything that deletes" among changes that will be declined.
Deletion propagation from Raindrop is off for a reason that is not squeamishness. From the outside, three events look identical: an item hard-deleted in Raindrop, an item moved to Raindrop's trash, and an item that a paged read simply missed because a bookmark was saved mid-run and shifted the window. A propagating deleter would act on all three, and for two of them it would be destroying a browser bookmark that corresponds to a save the user still has. The tool cannot restore that. The design records what turning propagation on would require: absence across N consecutive full reads, a re-query of the id, and a check of the Trash collection (-99). None of it is built, and nothing that runs without confirmation is on the roadmap.
There is exactly one way this system removes anything from the bar, and it is worth being exact about it because the README's "never delete" is a statement about routine operation.
~/.raindrop-sync/reset_folders.json is a file Phase A can write to ask Phase B to tear down and rebuild a managed folder, for example after the folder layout has changed following a taxonomy version bump (see Taxonomy and Classification). The native host reads it and passes the folder names to the extension along with the pending items. The extension then removes each named folder with chrome.bookmarks.removeTree, on two conditions written into sw.js: the host explicitly named it, and it is a direct child of the bookmarks bar. The items the host reports as pending are then created into the rebuilt folder; the host offers only items not already recorded as created or already present for that client, and nothing resets that record for you. Before writing the reset file, clear the extension_applied rows for that folder's items by hand, or the torn-down folder comes back with only its pending items. Operations has the procedure.
What this path does not do is preserve unmarked children. removeTree takes the whole subtree, including a bookmark you dragged into that folder yourself. The extension cannot see markers, so it cannot harvest what is yours. That is why the host only ever names folders this tool owns end to end, and why the mechanism is triggered by a file rather than by any schedule. The file writer has no equivalent: its design rule for rebuilding a folder is to harvest and re-attach the folder's unmarked children first, and the shipped script does not rebuild folders at all.
If you keep your own bookmarks inside the managed mirror folder, know that a reset of that folder would take them. Keep hand-made bookmarks in hand-made folders.
Brave reads Bookmarks exactly once, at profile load, into an in-memory model that is authoritative from then on. There is no file watcher. Every later write to the file is Brave serialising its entire in-memory tree over the top: on a debounce of roughly 2.5 seconds after any bookmark change, and again at quit. (The debounce constant is a Chromium internal; do not build on the number.)
So a write made by the file writer while Brave is running is invisible to Brave, and the next time Brave serialises, it writes a tree that never contained the new nodes. The writer's additions are gone from disk.
Why this is a nuisance and not a data-loss risk:
-
Both writers are atomic. The file writer uses
os.replace; Chromium'sImportantFileWriterwrites a temp file and renames. A rename on the same volume is atomic. One version wins whole; the file is never torn. - Your bookmarks were never on disk in the sense that matters. While Brave runs they live in its memory, and that is what gets written out. The version that wins the race is the one containing your bookmarks, because the loser's content was never the source of them.
- The only casualty is the tool's pending addition, and the tool knows how to make it again.
The recovery is a retry, and it is a mechanism, not a workaround. Every fifteen minutes the launchd agent re-reads the file, checks whether every promotion is present, and if not, writes again. The write sticks the first time Brave has a quiet tick, and it is guaranteed to stick once Brave restarts, because that is the one moment Brave reads the file. The log line NOTE: Brave is running. It may overwrite this from memory; the next tick re-applies. is the writer telling you it knows.
Two things follow that people get wrong. Never kill Brave to make a write land. Quitting triggers the shutdown serialise, which is precisely the write that would discard yours, and it is hostile to the person using the browser besides. And do not add a Brave-is-running gate. The cost of losing the race is the tool's own work; the cost of the gate is a fallback that never runs on a machine where the browser is always open. If you want additions to land in a running browser immediately, that is what the extension is for: it writes through the model rather than around it, and there is no race to lose.
The genuine risk in writing this file is not a concurrent write. It is a malformed one.
On a parse failure, on version != 1, or on any of the three permanent roots being absent, Chromium's model loader records a metric and nothing else. Brave then constructs three empty permanent nodes and runs exactly like a fresh profile. No dialog, no error bar, no prompt, no restore path. Then the backup latch completes the damage: the first save of that session copies the broken file over Bookmarks.bak, destroying the last good copy Chromium held. A person who restarts the browser hoping it clears up has just closed the recovery window.
This is how a JSON bug becomes permanent data loss with no error message anywhere. It is established from Chromium's loader source and was deliberately not reproduced on a populated profile; the guard chain exists because the failure is silent, so the writer's own verification is the only error surface there is.
The guard is every mechanism on this page, in sequence:
-
Preflight refuses to touch a file that is already in a discard-eligible state (
version, roots) or that shows signs of another editor (checksum). - Backups of both files, parsed after copying, mean a good copy exists somewhere Chromium will not overwrite it.
- The atomic write means the file on disk is at every instant either the old complete file or the new complete file.
- The re-read means a file that does not parse, or whose checksum or identities are wrong, is replaced with the backup before the run exits, and the exit code is non-zero.
If the case ever does occur, the recovery order matters: quit Brave first, before it saves, then restore the newest backup that parses. Troubleshooting has the exact steps.
Every path the writer touches can be redirected. RAINDROP_SYNC_PROFILE points it at a directory holding a copied Bookmarks; RAINDROP_SYNC_ROOT points it at a directory holding a desired.json of your choosing. The same code then runs against a copy with nothing real in reach. This is not hygiene for its own sake: a dry run against a copy is how a real bug was caught before it reached a live profile. The folder resolver once treated a leading Bookmarks Bar path component as a folder to create rather than the root to resolve into, and would have built a duplicate nested tree beside the user's real folders. It reached a live profile in exactly zero runs, because the copy was tested first. Contributing and Roadmap asks for the same discipline from every change to the writer.
MIT licence | raindrop-brave-sync on GitHub | Roadmap board
raindrop-brave-sync
Start here
How it works
Running it
Research
Community