Repository navigation
Ledger and File Formats
This page is the reference for every table and file the system keeps under ~/.raindrop-sync/, for the content hash that drives change detection, and for the exact node shape the file writer emits into Brave's Bookmarks file, including the checksum.
Everything here is per-install. Every id, key, collection name and hash shown is an invented example of the right shape; none of them will appear in your install. The README lists what lives where; this page says what each field means, who writes it, who reads it, and what breaks if you change it. For how the pieces fit together at run time see Architecture.
bootstrap.sh creates ~/.raindrop-sync/ with mode 0700. The checkout is code; this directory is state, and every state and secret file in it is listed in the repo's .gitignore; bin/ and extension/ are not.
| Path | Written by | Read by | Holds |
|---|---|---|---|
state.db |
Phase A skill (bookmarks, meta, runs); native host (extension_applied) |
Phase A skill; native host | The SQLite ledger |
taxonomy.json |
The first classification pass, then only an explicit re-derivation | Phase A skill | The pinned taxonomy |
collection-map.json |
Phase A skill, when a collection is created | Phase A skill | Taxonomy path to Raindrop collection id |
desired.json |
Phase A skill (append) | Native host, file writer | Staged browser promotions: the only Phase A to Phase B hand-off |
reset_folders.json |
Phase A skill, or you | Native host, which deletes it once the extension reports the applied results | Managed folders the extension should tear down and rebuild |
applied.json |
File writer, after a verified write | Nobody in the current code | The GUIDs the writer last confirmed present |
backups/ |
File writer, before every write | You, during a restore | Timestamped copies of Bookmarks and Bookmarks.bak
|
log/ |
Native host (native_host.log); launchd (apply.out.log, apply.err.log) |
You | Diagnostics |
key.pem |
bootstrap.sh, once |
bootstrap.sh |
The extension signing key that pins the extension id; mode 0600
|
.extension-identity.json |
bootstrap.sh |
bootstrap.sh |
{"id": "<32 letters a-p>"}, the derived extension id |
.env |
seed-token.sh, or you |
get-token.sh |
Optional cleartext token fallback; mode 0600
|
extension/ |
bootstrap.sh |
Brave, loaded unpacked |
sw.js plus a manifest.json with your key pinned in |
bin/ |
bootstrap.sh |
launchd, Brave, you |
apply_brave.py, native_host.py, get-token.sh, seed-token.sh
|
A plain SQLite database, opened with Python's stdlib sqlite3 and the sqlite3 CLI. Four tables. The three Phase A tables are created by the skill on the first run; extension_applied is created by the native host with CREATE TABLE IF NOT EXISTS the first time it opens the database, so a fresh install with the extension loaded and no classification run yet has a state.db containing only that table.
The DDL for extension_applied is in native_host.py. The DDL for the other three is not declared in any public source. The shapes below are as created by the reference classification pass; treat them as illustrative, the skill owns this schema.
As created by the reference classification pass; treat as illustrative, the skill owns this schema.
CREATE TABLE bookmarks(
raindrop_id INTEGER PRIMARY KEY,
title TEXT, link TEXT,
content_hash TEXT, -- sha256(link|title|excerpt|note|type)
collection TEXT, tags TEXT,
promoted INTEGER DEFAULT 0,
taxonomy_version INTEGER,
classified_at TEXT
);
CREATE INDEX idx_hash ON bookmarks(content_hash);| Column | Meaning | Notes |
|---|---|---|
raindrop_id |
Raindrop's numeric id for the item | The identity anchor for everything downstream: the ledger key, the extension_applied key, and the input to the browser GUID |
title |
The curated display title | Emoji stripped, over-long blurbs shortened, HTML entities decoded. This is what goes into desired.json as name and onto the bar. It is not what was hashed |
link |
The curated URL | Tracking parameters removed. Same rule: display value, not hash input |
content_hash |
64-character lowercase hex SHA-256 over the raw values | Delta detection only. See the next section |
collection |
The taxonomy path the item was classified into, as text |
/-separated. See the separator note below |
tags |
The tags applied, as a comma-separated string | Drawn from the taxonomy's controlled vocabulary plus provenance tags such as paid-ad and short-link
|
promoted |
1 if the item was staged into desired.json, else 0
|
A tiny fraction of rows; the gate rejects by default |
taxonomy_version |
The value of meta.taxonomy_version when the row was last classified |
A row whose value is behind meta is due for reclassification |
classified_at |
ISO 8601 timestamp with timezone | When the row was last classified |
The separator note. collection is a path-keyed column, and two runs of the same code wrote different forms into it: Woodworking / Hand Tools and Woodworking/Hand Tools. Nothing currently breaks, because classification goes path to id through collection-map.json and both appliers key on desired.json.folder_path, never on this column. But an exact map[row["collection"]] lookup added later would silently miss most of the library. Any reader must normalise first:
def norm(s): return " / ".join(p.strip() for p in s.split("/"))Rewriting the column is a data migration and belongs to an explicit, user-invoked pass, never a silent fixup inside a routine run.
The hash is the only change detector. Its format is five |-separated slots:
content_hash = sha256("<raw_link>|<raw_title>|<excerpt>|<note>|<type>")
The last three slots are present in the format but always empty, because the connector surface the classifier reads through does not return excerpt, note or type. So in practice every hash is:
import hashlib
content_hash = hashlib.sha256(f"{raw_link}|{raw_title}|||".encode("utf-8")).hexdigest()The public sources state the format as a string and the ledger stores a 64-character hex digest; UTF-8 is the natural encoding for a title that can contain anything, and the expression above is the one to reproduce.
raw_link and raw_title are the values exactly as Raindrop returned them: the & still encoded, the emoji still present, the utm_source still on the URL. That is the invariant, and it means two columns in the same row intentionally disagree:
| Column | Holds | Used for |
|---|---|---|
content_hash |
sha256(raw_link|raw_title|||), verbatim Raindrop values |
Delta detection only |
title, link
|
The curated display values | Naming in the browser, desired.json, human reading |
Worked example, generic. Raindrop returns:
link: https://example.com/dovetail-joints?utm_source=newsletter
title: Dovetail Joints & Layout
The ledger row stores link = https://example.com/dovetail-joints, title = Dovetail Joints & Layout, and
content_hash = sha256("https://example.com/dovetail-joints?utm_source=newsletter|Dovetail Joints & Layout|||")
= 55c2191829cfb951054472b00e0ce6df410f30c23ab2050c4f8369dc3bbcd1ad
On the next run, Raindrop returns the same raw pair, the same hash comes out, and the row is untouched. Had the hash been computed from the curated columns, it could never be reproduced from the source, and the row would re-flag as EDITED on every run forever. That is not hypothetical: the first run of this system did exactly that and re-flagged about 28 percent of the library on the following run. See Research Status, finding 7a.
Three rules follow:
-
Never "repair" the columns into agreement. A future maintainer looking at a row whose
titledoes not match the string that produced its hash will be tempted to recompute the hash from the stored columns. That single act reintroduces the bug. -
Write the hash last. The skill records
content_hashonly after the item has been committed todesired.json. A crash between the two costs one redundant reclassification; the other order produces a silent skip that nothing ever flags again. -
Widening the hash is a breaking change. Filling the
excerpt,noteortypeslots invalidates every stored hash at once and reclassifies the whole library. That is ataxonomy_version-class operation: explicit and user-invoked, never a silent upgrade.
A missing API token does not prevent computing the hash. Link and title are both in the connector's response; the empty slots are empty by design. Rows were once left with a NULL hash on that mistaken reasoning, and edit detection was blind for them until they were rebased.
As created by the reference classification pass; treat as illustrative, the skill owns this schema.
CREATE TABLE meta(k TEXT PRIMARY KEY, v TEXT);All values are text. The one key the public sources name is taxonomy_version:
| Key | Value | Meaning |
|---|---|---|
taxonomy_version |
Integer as text, 1 after the first pass |
Bumped only by an explicit re-derivation. Every bookmarks row records the version it was classified under; a mismatch selects the row for reclassification |
The skill inserts a row into runs at the end of every pass and records classified_count, how many items the delta selected. This is the canary. Steady state is a handful; above about 50 on a library that barely changed means delta detection is broken (a curated-value hash, a widened hash, a paging race), not that the library changed. The rest of the table's shape is owned by the skill and is not declared in any public source.
CREATE TABLE extension_applied(
client TEXT NOT NULL, raindrop_id INTEGER NOT NULL,
bookmark_id TEXT, url TEXT, status TEXT, applied_at TEXT,
PRIMARY KEY (client, raindrop_id));| Column | Meaning | Notes |
|---|---|---|
client |
Which browser applied it: brave, chrome, edge, vivaldi, opera, or unknown
|
Reported by the service worker, normalised by the host. A worker running older code sends nothing and is stored as unknown
|
raindrop_id |
The desired.json entry this row is about |
Same id as bookmarks.raindrop_id
|
bookmark_id |
The chrome.bookmarks node id the extension created, as a string |
Empty for already_present. Chromium node ids are a per-profile allocation detail; do not key anything durable on them |
url |
The entry's URL | Copied from desired.json by the host at record time, not sent by the worker |
status |
created, already_present, or error
|
The host stores whatever the worker sent, defaulting to unknown
|
applied_at |
ISO 8601 timestamp | Set by the host |
Why the key is (client, raindrop_id) rather than raindrop_id: desired.json is one shared list, and every browser running the extension must apply it independently. The host answers a pending request with every entry that has no created or already_present row for that client. With a bare id key, the first browser to sync would record the item and every other browser would be told nothing is pending, silently and forever. Architecture covers this in more depth.
Two behaviours worth knowing:
- Writes are
INSERT OR REPLACE, so a later result for the same(client, raindrop_id)overwrites the earlier one. - Only
createdandalready_presentcount as done. Anerrorrow leaves the item pending, so it is retried on the next tick.
The extension cannot mark its nodes in the file (see Safety and Ownership Model), so this table is the only record that the extension did the work. Deleting rows from it makes the extension re-check those items; since it checks by URL before creating, that costs one already_present per row and nothing else.
-- what is still pending for a client
SELECT raindrop_id FROM extension_applied WHERE client='brave' AND status='error';
-- the canary over time
SELECT classified_count FROM runs ORDER BY rowid DESC LIMIT 10;
-- rows due for reclassification after a taxonomy bump
SELECT count(*) FROM bookmarks WHERE taxonomy_version < (SELECT CAST(v AS INTEGER) FROM meta WHERE k='taxonomy_version');
-- rows with no hash: edit detection is blind for these
SELECT count(*) FROM bookmarks WHERE content_hash IS NULL;A JSON array. Each element is one promotion to the browser bar:
[
{"raindrop_id": 12345, "name": "Dovetail Joints and Layout", "url": "https://example.com/dovetail-joints", "folder_path": "Raindrop/Woodworking"},
{"raindrop_id": 12346, "name": "Sourdough Starter, Day by Day", "url": "https://example.com/sourdough", "folder_path": "Raindrop/Recipes"}
]| Field | Type | Meaning |
|---|---|---|
raindrop_id |
integer | The Raindrop id. The file writer derives the node GUID from it; the host keys extension_applied on it |
name |
string | The curated title, used verbatim as the bookmark's title |
url |
string | The URL to create. Both appliers use it, as an exact string, to decide whether the item is already present |
folder_path |
string |
/-separated path relative to the bookmarks bar |
Semantics that matter:
- It is the whole desired state, not a queue. Phase A appends; nothing removes. Both appliers read the entire array on every tick and compute what is missing from the browser, so re-reading it a thousand times creates nothing twice.
-
folder_pathis relative to the bar.Raindrop/Woodworkingmeans a folderRaindropon the bar containing a folderWoodworking. A leadingBookmarks Barsegment (or the bar's own localised name, or an empty segment) is tolerated and stripped by both appliers; without that rule an earlier writer built a duplicate tree beside the user's real folders. - Keep it flat. The skill stages one level under a managed top-level folder. Hover menus make every extra level a tax on the reader.
- Ordering is irrelevant. Nodes are appended to their folder in whatever order the applier encounters them.
The file writer's already-present check is (GUID present and marked) OR (URL present anywhere in the tree); the extension's is chrome.bookmarks.search({ url }) with an exact match. Editing url in an existing entry therefore makes the item look new to both appliers, and neither will remove the node created under the old URL.
A JSON array of folder names, each a direct child of the bookmarks bar:
["Raindrop"]The native host reads it and includes it as reset_folders in the pending reply, but only when that reply also carries pending items. The service worker then calls chrome.bookmarks.removeTree on each named folder before applying, so that a changed folder layout can be rebuilt from desired.json rather than accreting beside the old one.
This is the one destructive operation in the system, and its scope is exactly the named top-level folders. Everything inside them goes, including bookmarks a person dropped in by hand, which is why the worker's own comment says the host should only ever name folders this tool owns end to end.
Two consequences of how the host is written:
-
The file is consumed once, then cleared. The host deletes
reset_folders.jsonwhen the extension reports the applied results for that tick, so a reset runs against exactly one pending batch and cannot tear the folder down again on the next tick. If noappliedmessage ever arrives, the file stays until one does; remove it yourself if you abandon the rebuild. -
A reset only rebuilds what is still pending. The worker removes the whole folder, then creates only the items that have no
createdoralready_presentrow for that client; items already recorded as done are not re-sent. If you need a full rebuild, clear the matchingextension_appliedrows in the same operation that writes the reset file.
The file writer ignores reset_folders.json entirely.
A flat JSON object mapping a taxonomy path to the numeric Raindrop collection id that holds it:
{
"Recipes": 40000001,
"Recipes / Baking": 40000002,
"Woodworking": 40000003,
"Woodworking / Hand Tools": 40000004,
"Research Papers": 40000005,
"Travel": 40000006
}Keys use the spaced separator, Parent / Child. Values are whatever id Raindrop assigned when the collection was created, so they are per-install and meaningless anywhere else. The skill classifies each item into exactly one key of this map and uses the value for the write-back; when it creates a new collection under the no-bin rule, it adds the new path here. The appliers never read this file.
The pinned taxonomy, generated from your own library on the first pass and never re-derived by a routine run. The shape below is as created by the reference classification pass; treat it as illustrative, the skill owns this schema:
{
"collections": [
{
"title": "Woodworking",
"description": "Joinery, tools, finishing and project logs",
"children": [
{"title": "Hand Tools", "description": "Planes, chisels, saws and their upkeep"},
{"title": "Joinery", "description": "Dovetails, mortise and tenon, box joints"}
]
},
{
"title": "Recipes",
"description": "Things to cook, with a bias toward what was actually made",
"children": [
{"title": "Baking", "description": "Bread, pastry and anything with a starter"}
]
}
],
"tags": [
{"tag": "reference", "meaning": "A page returned to repeatedly, not read once"},
{"tag": "paid-ad", "meaning": "Arrived through an advertisement; provenance only, never placement"}
],
"rationale": "Free text explaining the shape of the tree and what was deliberately left out."
}| Field | Meaning |
|---|---|
collections[].title |
A top-level collection; one key of collection-map.json
|
collections[].description |
What belongs there, written for the classifier |
collections[].children[] |
Second-level collections, same shape minus children
|
tags[].tag |
A member of the controlled vocabulary |
tags[].meaning |
What the tag asserts; the classifier picks one to four per item from this list and nothing outside it |
rationale |
Why the tree looks the way it does |
The version number is not in this file. It lives in meta.taxonomy_version. Extending the tree with a new collection edits this file and the map without touching the version; restructuring it is the explicit re-derivation that bumps the version and reclassifies everything. The classification rules are in Taxonomy and Classification.
After a verified write the file writer records what it confirmed:
{
"applied_guids": ["ed5da9de-facc-5594-a8f6-a0ee47fb7aee", "..."],
"at": "2026-01-01T12:00:00.000000+00:00"
}applied_guids is the sorted list of GUIDs for every desired.json entry, not only the ones written this run. Nothing in the current code reads it back; the writer decides what to do by re-reading the Bookmarks file on every tick, which is the design that makes a clobbered write self-healing.
bootstrap.sh writes .extension-identity.json as {"id": "abcdefghijklmnopabcdefghijklmnop"}: the first 16 bytes of the SHA-256 of your DER-encoded public key, rendered as 32 letters a through p. It is derived from key.pem, so yours differs from every other install's.
Before every write, and only when there is something to write, the file writer copies both Chromium files with one UTC stamp:
backups/Bookmarks.20260101T120000Z
backups/Bookmarks.bak.20260101T120000Z
It then re-opens the copy of Bookmarks and parses it, so a backup that does not parse fails the run before anything is written. Chromium's own Bookmarks.bak is not a safety net: it is overwritten by whatever is on disk at the first save of the next session, and nothing in Chromium ever reads it back. These copies are the only restore path that exists; the procedure is in Troubleshooting.
Retention is per prefix: the writer keeps the 10 newest Bookmarks.<stamp> copies and the 10 newest Bookmarks.bak.<stamp> copies, pruned separately, so a primary copy is never evicted to make room for a .bak copy. An earlier version pruned the two together by name order, which kept .bak copies over primaries; fixed. If you want longer retention, keep your own.
log/native_host.log, one line per event, UTC:
2026-01-01T12:00:00.000000+00:00 host start (pid 12345)
2026-01-01T12:00:00.002000+00:00 pending[brave]: 0 of 42 staged
2026-01-01T12:00:00.002300+00:00 stream closed
pending[brave] rather than pending[unknown] confirms the worker is reporting its client. applied[brave]: recorded N results follows a tick that created something, along with one created folder: line per folder and one extension error on <id>: line per failure.
log/apply.out.log, one line per launchd tick, ends in one of: no desired.json; nothing to do, desired.json empty; nothing to do, all N promotions already present and marked; no write needed, or a write sequence (N to apply, backup: ..., created folder: ..., OK: N applied, M total marked nodes present, optionally NOTE: Brave is running ...). Preflight refusals start with ABORT: and exit 2; WRITE FAILED and VERIFY FAILED restore the backup and exit 1. apply.err.log should stay empty; a bare Operation not permitted there is a TCC denial under launchd.
The writer edits Chromium's bookmark store in place. The document is:
{
"checksum": "d41d8cd98f00b204e9800998ecf8427e",
"roots": {
"bookmark_bar": {"children": [ ... ], "date_added": "...", "date_modified": "...", "guid": "...", "id": "1", "name": "Bookmarks Bar", "type": "folder"},
"other": { ... },
"synced": { ... }
},
"version": 1
}Preflight refuses any file where version is not 1, any of the three roots is missing, a top-level sync_metadata key is present, or the stored checksum does not recompute. It never adds checksum_sha256.
{
"date_added": "13411699200000000",
"date_last_used": "0",
"guid": "ed5da9de-facc-5594-a8f6-a0ee47fb7aee",
"id": "142",
"meta_info": {"raindrop_sync": "v1"},
"name": "Dovetail Joints and Layout",
"type": "url",
"url": "https://example.com/dovetail-joints"
}{
"children": [],
"date_added": "13411699200000000",
"date_modified": "13411699200000000",
"guid": "0724a35e-cd73-5b02-88a3-b9d213e1a9b1",
"id": "141",
"meta_info": {"raindrop_sync": "v1"},
"name": "Woodworking",
"type": "folder"
}| Field | Rule | Why |
|---|---|---|
id |
A decimal string, unique across all three roots. New nodes get max(all ids) + 1, allocated upward in tree order after all nodes are placed |
Chromium stores ids as strings. Ids are a per-file allocation detail; pre-M152 checksum recovery renumbers every one of them (from source only, cannot be triggered on M152, which does not read the checksum), so nothing durable is keyed on id
|
date_added, date_modified, date_last_used
|
Strings holding microseconds since 1601-01-01 UTC (the Windows FILETIME epoch). date_last_used is "0" on new nodes |
int(time.time() * 1_000_000) + 11_644_473_600_000_000. For 2026-01-01T00:00:00Z that is 13411699200000000. To read one back: (int(d) - 11644473600000000) / 1e6 is Unix seconds |
guid |
Lowercase UUID string, unique across the file. Deterministic: uuid5(NAMESPACE_DNS, "url:<raindrop_id>") for a bookmark, uuid5(NAMESPACE_DNS, "folder:<path so far>") for a folder, where the path is the stripped folder_path segments joined with / up to that folder |
GUIDs survive Chromium's recovery renumbering (from source only; see the id row), so they are the identity anchor. Deriving them from the source id makes re-runs produce the same node rather than a churned one. NAMESPACE_DNS is 6ba7b810-9dad-11d1-80b4-00c04fd430c8, the RFC 4122 constant, spelled out in the code |
meta_info |
{"raindrop_sync": "v1"} on every node the writer creates, folders included |
The ownership marker. The writer may add or remove marked nodes and nothing else; unmarked nodes are untouchable by construction. Verified to survive Brave's own re-serialisation, 7 of 7 nodes on Brave 152; re-verify after major upgrades |
name |
The desired.json name, verbatim |
Hashed as UTF-16LE in the checksum (below) |
type |
"url" or "folder"
|
Hashed as a literal |
url |
The desired.json url, verbatim |
URL nodes only |
children |
Present on folders, in document order | New nodes are appended at the end; the writer never reorders |
Worked GUID examples, so you can check an implementation: uuid5(NAMESPACE_DNS, "url:12345") is ed5da9de-facc-5594-a8f6-a0ee47fb7aee; uuid5(NAMESPACE_DNS, "folder:Raindrop") is 7c2e7d9a-e77e-5018-adbd-dd34ae0b76a7; uuid5(NAMESPACE_DNS, "folder:Raindrop/Woodworking") is 0724a35e-cd73-5b02-88a3-b9d213e1a9b1.
The writer emits compact JSON with ensure_ascii=False, so non-ASCII titles are written as raw UTF-8 rather than \u escapes. Layout is irrelevant to Brave, which rewrites the whole file in its own layout the next time it serialises. Emoji, astral-plane, CJK and Devanagari titles have been round-tripped through both writers intact.
The writer finds its own nodes by walking the whole tree and matching on GUID plus marker, and finds everyone's nodes by walking the whole tree and collecting URLs. It never locates anything by position. Positional lookup silently duplicates the first time a user drags a folder, after which Chromium re-randomises one of the duplicate GUIDs and severs the identity anchor for good.
The top-level checksum is an MD5, not over the file, but over a pre-order walk of the node tree: no separators, no lengths, no field names, just concatenated field bytes in a fixed order. One encoding detail is the whole puzzle: the title is hashed as UTF-16LE, everything else as UTF-8. The reason is visible in Chromium's BookmarkCodec, where the title is a string16 whose native buffer is fed to the digest while ids and URLs are std::string.
This is the implementation in apply_brave.py, verified in both directions: it reproduces the digest Brave itself wrote, and Brave accepts a file carrying its output with no recovery pass.
import hashlib
def checksum(doc):
"""Reproduce Chromium/Brave's Bookmarks `checksum` field."""
m = hashlib.md5()
u8 = lambda s: m.update(s.encode("utf-8"))
# surrogatepass: Chromium hashes raw UTF-16 code units, so an unpaired
# surrogate that arrived as a \uD800 escape in the JSON must pass through
# rather than raise. The default codec raises.
u16 = lambda s: m.update(s.encode("utf-16-le", "surrogatepass"))
def node(n):
u8(n["id"])
u16(n.get("name", ""))
u8(n["type"])
if n["type"] == "url":
u8(n.get("url", ""))
else:
for c in n.get("children", []):
node(c)
for root in ("bookmark_bar", "other", "synced"):
node(doc["roots"][root])
return m.hexdigest()Per node, in this order: id as UTF-8, name as UTF-16LE, the type literal url or folder as UTF-8, then for a URL node the url as UTF-8, and for a folder the children in stored order. The three roots go in the order bookmark_bar, other, synced.
Three traps inside those lines:
-
surrogatepass. Titles can contain unpaired surrogates. Python's plainutf-16-lecodec raises on them; Chromium does not care. Without the error handler the job crashes on the one bookmark you cannot reproduce on demand. - Order is document order. Sorting children, even for tidiness, changes the digest.
- The type literal is hashed. Omitting it produces a digest that is stable and wrong.
How the writer uses it: preflight recomputes the checksum and refuses to run on a mismatch, which is a cheap detector for "something else is editing this file"; the write sets doc["checksum"] after ids are assigned; postflight re-reads the file from disk and asserts the stored value recomputes. Brave 152 itself no longer reads the field (the read side was removed upstream, in a change noted as easy to revert), so a wrong value is currently harmless to Brave, but the writer produces a correct one anyway: the read side is one revert away, other tools read the file, and on browsers that do read it a mismatch triggers a recovery pass that renumbers every node (from source only, cannot be triggered on M152).
To check any Bookmarks file:
python3 - <<'EOF'
import json, os
p = json.load(open(os.path.expanduser("~/.raindrop-sync/config.json")))["profile"] + "/Bookmarks"
doc = json.load(open(p))
print("match:", checksum(doc) == doc["checksum"]) # checksum() from above
EOFExpected: match: True. See Research Status for the verification record, and Operations for the environment overrides (RAINDROP_SYNC_ROOT, RAINDROP_SYNC_PROFILE) that let you point every path on this page at a throwaway copy.
MIT licence | raindrop-brave-sync on GitHub | Roadmap board
raindrop-brave-sync
Start here
How it works
Running it
Research
Community