Skip to content

Troubleshooting

Rajat edited this page Sep 9, 2026 · 3 revisions

Troubleshooting

This page is a symptom-first guide: find what you are seeing in the table, jump to the section, and each section gives the cause, one diagnosis command, and the fix.

Two rules before you start. First, never test a fix against your live profile; every path in the applier is overridable with RAINDROP_SYNC_PROFILE and RAINDROP_SYNC_ROOT, and the throwaway-profile method is in RESEARCH.md section 12. Second, never print the token while debugging; a token that reached a terminal must be rotated (Operations). Healthy log output, for comparison, is also on the Operations page.


Since 1.1.0, run the doctor first: ~/.raindrop-sync/bin/doctor.sh prints one ok, warn or FAIL line per check (interpreter, tree, key, manifests, host ping through the wrapper, profile, Brave Sync, launchd, token presence, ledger tables, extension contact) and never prints the token. Most entries below start from a FAIL line it gives you.

Symptom table

What you see Where Most likely cause Section
Specified native messaging host not found worker console, or a native_host.log with no host start lines host manifest in Brave's directory instead of Chrome's, or a broken manifest field Host not found
Card greyed out, toggle will not stay on brave://extensions the extension id was poisoned by a CRX install Extension greyed out
Bookmarks appear, then vanish the bar; apply.out.log shows OK then to apply again the file writer lost a race with a running Brave Appear then vanish
Nothing appears at all, no errors everywhere nothing staged, stale worker, agent not loaded, preflight abort, or the ledger says it is done Nothing appears
classified_count in the dozens or hundreds every run Phase A summary hash over curated values, a timestamp watermark, a widened hash, or newest-first paging Reclassifies every run
Two identical bookmarks, same second the bar the MV3 serialisation lock or the URL match was removed Duplicates
Brave starts with an empty bar, like a fresh profile Brave a malformed Bookmarks file Empty bar after a write
launchctl list shows nothing, or logs never advance apply.out.log stale agent never bootstrapped, bad absolute paths, moved interpreter, TCC denial Agent never runs
Skill reports the Raindrop tools are missing a Claude Code session headless or scheduled session has no connector Headless session
pending[unknown] in the host log native_host.log a service worker running old code Shorter ones
ABORT: ... every tick apply.out.log a deliberate preflight refusal Shorter ones
No Raindrop token found Phase A, or the verify step token never seeded, or seeded in a store the resolver does not consult Shorter ones

Host not found

Symptom. The card looks healthy, nothing is ever applied, and native_host.log has no host start line in the last minute with Brave open. If you can reach the worker console it says Specified native messaging host not found.

Cause. Brave does not override Chromium's stock product path for native-messaging lookup on macOS, so a manifest under BraveSoftware/Brave-Browser/NativeMessagingHosts/ is silently ignored. It must be in Chrome's directory, ~/Library/Application Support/Google/Chrome/NativeMessagingHosts/, whether or not Chrome is installed. The same generic error also covers four manifest mistakes, each silent: the filename does not equal the name field; path is not absolute or not executable; allowed_origins lacks the trailing slash; or the id in allowed_origins is not the id on the card (a regenerated key.pem, or a manifest without a pinned key after the folder moved).

Diagnose.

# is the manifest where Brave looks, and does it name the id on the card?
python3 - <<'EOF'
import json, os
p = os.path.expanduser("~/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.raindrop_sync.host.json")
m = json.load(open(p)); print("name:", m["name"]); print("path:", m["path"], "executable:", os.access(m["path"], os.X_OK))
print("allowed_origins:", m["allowed_origins"])
EOF
cat ~/.raindrop-sync/.extension-identity.json      # the id bootstrap derived from key.pem

Then prove the host itself works, bypassing the browser:

python3 -c 'import struct,sys; m=b"{\"op\":\"ping\"}"; sys.stdout.buffer.write(struct.pack("@I",len(m))+m)' \
  | ~/.raindrop-sync/bin/native_host.py | tail -c +5
# expect: {"ok": true, "version": "<VERSION from src/native_host.py>"}

If that answers and the extension still does nothing, the problem is the manifest, not the host. The decisive evidence is Brave's own stderr from a throwaway profile:

"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser" \
  --user-data-dir=/tmp/lab --load-extension="$HOME/.raindrop-sync/extension" \
  --enable-logging=stderr --v=0 about:blank 2>&1 | grep -iE "native|raindrop"
# decisive line: launch_context.cc:148 Can't find manifest for native messaging host

Fix. Re-run bootstrap.sh. It is idempotent, reuses your key.pem, and rewrites both manifest copies with the current id and an absolute path. Then press reload on the card. If you prefer to hand-fix, follow SETUP.md step 4 and its four rules.

Read more. RESEARCH.md finding 2, verified by a single-variable experiment.


Extension greyed out with the toggle disabled

Symptom. The card shows the extension disabled. Clicking the toggle does nothing or it flips back. brave://extensions gives no reason.

Cause. At some point a locally signed CRX with this id was installed. Chromium's install verifier flagged the id DISABLE_NOT_VERIFIED (disable_reasons: [256]) against the signed allowlist in Preferences, and every attempt to re-enable is reverted. The flag follows the id, not the installation: loading the same folder unpacked inherits it. No UI, flag or profile edit clears it.

Diagnose. Read Preferences directly; the UI hides the field you need.

python3 - <<'EOF'
import json, os
p = json.load(open(json.load(open(os.path.expanduser("~/.raindrop-sync/config.json")))["profile"] + "/Preferences"))
for ext_id, s in p["extensions"]["settings"].items():
    if s.get("disable_reasons"):
        print(ext_id, "location:", s.get("location"), "disable_reasons:", s["disable_reasons"])
EOF

256 next to your id means the identity is gone, not the code.

Fix. Mint a new id. bootstrap.sh treats a missing key.pem as a request for one:

rm ~/.raindrop-sync/key.pem
cd <your checkout> && ./scripts/bootstrap.sh        # generates a key, rewrites manifest.json "key" and both host manifests, prints the new id

Then in brave://extensions Remove the dead card (do not just reload it), Load unpacked ~/.raindrop-sync/extension again, and confirm the card shows the id bootstrap printed. Never pack a CRX again, not even to test.

Read more. RESEARCH.md finding 3, verified in Preferences and escaped only by a new id.


Bookmarks appear, then vanish

Symptom. apply.out.log says OK: N applied, the nodes are in the file, and a few seconds later, or after quitting Brave, they are gone. The next tick logs N to apply again.

Cause. The file writer wrote while Brave was running. Brave reads Bookmarks exactly once at startup and thereafter its in-memory model is authoritative; about 2.5 s after any bookmark change, and again at quit, it serialises that model over the file, which never contained your nodes. The writer already told you:

NOTE: Brave is running. It may overwrite this from memory; the next tick re-applies.

This is not a data-loss risk to anything you made. Both writers are atomic, so the file is never torn; the only casualty is the pending addition, and the ledger still wants it.

Diagnose.

grep -E "to apply|OK:|NOTE:|already present" ~/.raindrop-sync/log/apply.out.log | tail -8

A pattern of to apply, OK, NOTE, then to apply again is the race, not a bug. It ends with all N promotions already present and marked on the first quiet tick or after the next Brave start.

Fix. Nothing. If you want it to land now rather than on the next quiet tick, that is what the extension is for: it writes through the model instead of around it, and the file writer then logs "already present". Two things not to do: do not kill Brave (the quit-time save is exactly what discards the write, and it is hostile besides), and do not add a Brave-is-running gate to the writer; the retry is the mechanism.

Read more. RESEARCH.md finding 4, verified with a probe extension. The debounce constant is a Chromium internal; do not depend on the number.


Nothing appears at all

Symptom. No errors anywhere, no new bookmarks, both appliers apparently idle.

Causes and diagnosis, in the order worth checking.

  1. Nothing is staged. The promotion gate is strict; classifying many and promoting none is normal.

    python3 -c 'import json,os;print(len(json.load(open(os.path.expanduser("~/.raindrop-sync/desired.json")))), "staged")'
  2. The host is never invoked. With Brave open, tail -3 ~/.raindrop-sync/log/native_host.log. No host start within the last minute means the extension is not reaching the host: go to Host not found.

  3. The worker is running old code. Unpacked extensions do not hot-reload. If you edited sw.js and did not bump manifest.json's version and press reload on the card, the old worker is still live. Compare the version on the card with ~/.raindrop-sync/extension/manifest.json. This is the most common cause after a code change.

  4. The ledger says it is already done. The host only sends items not recorded for this client. After a profile reset, a new machine, or a folder you deleted by hand, extension_applied can say created for something the browser no longer has.

    sqlite3 ~/.raindrop-sync/state.db "SELECT client, status, COUNT(*) FROM extension_applied GROUP BY 1,2;"

    Fix: delete the rows for the affected items (or all of them); the extension re-checks by URL, so the worst outcome is one tick of already_present.

  5. The launchd agent never fired. launchctl list | grep raindrop. A missing line means it was never bootstrapped; a non-zero exit column means it ran and failed. See Agent never runs.

  6. Preflight aborted. grep ABORT ~/.raindrop-sync/log/apply.out.log | tail -3. Each abort names its reason and is a deliberate refusal, repeated every tick until you clear the condition. See Shorter ones.

If the extension is off or Brave is closed, remember the fallback only becomes visible at the next Brave start; a file write made while Brave is running may take a few ticks to stick (Appear then vanish).


Every item reclassifies on every run

Symptom. The classified_count canary is huge on a library that barely changed: dozens or hundreds per run. The LLM cost is real and it never settles.

Cause. Something is re-selecting the library. In order of likelihood:

  • The ledger hashes curated values. This bug shipped once: the first run stored the cleaned title and link (emoji stripped, entities decoded, tracking parameters removed) and computed content_hash from those. A hash of an edited record can never be reproduced from the raw source, so about a quarter of the library re-flagged as EDITED on the next run and would have forever.
  • A timestamp watermark. Raindrop bumps lastUpdate on every write including your own write-back, so any lastUpdate-based selection returns everything you just wrote, every run.
  • A widened hash. Adding a field to the hash input invalidates every stored hash at once. That is a taxonomy_version-class change and must be explicit, never a silent upgrade.
  • Newest-first paging. A bookmark saved mid-run shifts the window and items fall between pages; that produces phantom NEW and phantom missing items rather than a stable delta.
  • NULL hashes. Rows with no content_hash are blind to edits and may be selected every run. A missing REST token is not a reason for a NULL: link and title come from the classifier's data source, and the empty excerpt|note|type slots are empty by design.

Diagnose.

sqlite3 ~/.raindrop-sync/state.db "SELECT COUNT(*) AS null_hashes FROM bookmarks WHERE content_hash IS NULL;"

Then pick one row that re-flagged, take its raw link and title from Raindrop (not from the ledger's display columns), and recompute. The documented format is sha256(raw_link|raw_title|||), three trailing pipes for the always-empty slots; confirm your reconstruction against a known-good row before trusting it:

python3 -c 'import hashlib,sys; print(hashlib.sha256(f"{sys.argv[1]}|{sys.argv[2]}|||".encode()).hexdigest())' '<raw link>' '<raw title>'

A mismatch on a row whose Raindrop record did not change means the stored hash was computed from something other than the raw values.

Fix. Restore the invariant: two columns that 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 browser naming, desired.json, reading

Recompute the affected hashes from raw source values, validate against known-good rows, and only then run again. Never "repair" the two columns into agreement by recomputing the hash from the curated columns; that is precisely how the bug is reintroduced. Page ascending (sort=created). If you genuinely need a wider hash, do it as an explicit re-derivation that bumps taxonomy_version. Details of the ledger are in Ledger and File Formats; the classification rules are in Taxonomy and Classification.

Read more. RESEARCH.md finding 7. The curated-hash failure is marked verified the hard way; the changedBookmarksDate gate is marked untested, which is why Phase A does a full read every run.


Duplicate bookmarks

Symptom. Two identical bookmarks, same title and URL, created within the same second.

Cause. There are two routes, and they leave different fingerprints.

  • The MV3 lifecycle race. Worker load and onInstalled can fire in the same millisecond; each ran the existence check, each saw the URL absent, each called chrome.bookmarks.create(). chrome.bookmarks has no uniqueness constraint, no upsert and no transaction. sw.js funnels every entry point through a single promise chain, serialize(); if that lock was removed, or a second entry point was added that bypasses it, this race is back.
  • The two appliers duplicating each other. The extension cannot write the file's meta_info marker, so a marker-only or GUID-only ownership check in the file writer does not see extension-created nodes and re-creates them. apply_brave.py therefore also matches on URL; if that match was removed, every extension-applied item gets a marked twin on the next quiet tick.

Diagnose.

# route 1: the host saw the work twice in the same tick window
grep -E "applied\[|created folder" ~/.raindrop-sync/log/native_host.log | tail -6
grep -n "serialize" ~/.raindrop-sync/extension/sw.js            # the lock must wrap every entry point

# route 2: the file writer created a node whose URL already existed
grep -E "to apply|OK:" ~/.raindrop-sync/log/apply.out.log | tail -4
grep -n "urls" ~/.raindrop-sync/bin/apply_brave.py             # the URL index and the "or d[\"url\"] in urls" match

One of the twins will carry meta_info and a deterministic uuid5 GUID (the file writer's), the other neither (the extension's). The read-only marker listing on the Operations page shows which is which.

Fix. Restore whichever guard is missing, reload the extension (bump the version), and delete the extra copy by hand in Brave; nothing in this tool deletes, including its own duplicates. Do not "fix" the race by adding a delay: that moves it, it does not close it.

Read more. RESEARCH.md finding 8 and finding 9, both verified. The ownership split is explained in Safety and Ownership Model.


The bar shows nothing after a write

Symptom. Brave starts with an empty bookmarks bar and behaves like a fresh profile. No dialog, no error, no warning.

Cause. A malformed Bookmarks file. On a parse failure, version != 1, or a missing permanent root, Chromium records a metric and loads three empty permanent nodes. That is the entire user-visible feedback. Worse, the first save of that session copies the broken file over Bookmarks.bak, destroying the last good copy; Chromium never reads .bak back and has no restore path anywhere.

The file writer's postflight (re-read from disk, verify parse, checksum, id and GUID uniqueness, restore the backup on any failure) exists so this cannot come from the writer. If you see it, something else wrote the file, or the writer was modified. Either way, the recovery is the same.

Diagnose, quickly, because the clock is running.

# 1. Quit Brave completely (Cmd-Q) BEFORE it saves. Confirm:
pgrep -x "Brave Browser"              # expect no output

# 2. Confirm the live file is the problem
python3 -c 'import json,os,sys
p=json.load(open(os.path.expanduser("~/.raindrop-sync/config.json")))["profile"] + "/Bookmarks"
d=json.load(open(p)); print("version",d.get("version"),"roots",sorted(d.get("roots",{})))' \
  || echo "does not parse"

# 3. Find the newest backup the writer took
ls -t ~/.raindrop-sync/backups/ | head

Fix. Restore from the writer's own timestamped backups, after proving the backup parses:

B=~/.raindrop-sync/backups/Bookmarks.<TIMESTAMP>
python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print("version",d["version"],"roots",sorted(d["roots"]))' "$B"
cp "$B" "$(python3 -c 'import json,os;print(json.load(open(os.path.expanduser("~/.raindrop-sync/config.json")))["profile"])')/Bookmarks"
# start Brave and confirm the bar is back

If you are modifying the writer, keep every step of backup, atomic replace, re-read and verify. They are the only error surface this failure has.

Read more. RESEARCH.md finding 6 and finding 5. Both are source-derived and designed against, deliberately not reproduced on a populated profile; a reproduction on a throwaway profile is a welcome contribution (Contributing and Roadmap).


The launchd agent never runs

Symptom. launchctl list | grep raindrop prints nothing, or prints a non-zero exit status, or apply.out.log has not gained a line in more than 15 minutes.

Causes.

  • Never bootstrapped. The plist is on disk but was never loaded, or the session was rebooted after a bootout.
  • Paths in the plist are not absolute. launchd does not expand ~ or $HOME. The template uses placeholders that bootstrap.sh fills in; a hand-written plist with ~/.raindrop-sync/... silently never starts.
  • The interpreter moved. /opt/homebrew/bin/python3 is a symlink that moves on brew upgrade. The recommended interpreter is /usr/bin/python3; the scripts are pure stdlib.
  • TCC denial. A runtime tree under ~/Documents or ~/Downloads can be denied to launchd with a bare Operation not permitted and no dialog. ~/.raindrop-sync and the Brave profile directory are not protected paths.
  • The grep pattern. The label is com.raindrop-sync.apply, with a hyphen. A pattern like raindropsync matches nothing even when the agent is fine.

Diagnose.

launchctl list | grep raindrop
launchctl print gui/$(id -u)/com.raindrop-sync.apply | grep -E "state|last exit|program|arguments"
plutil -lint ~/Library/LaunchAgents/com.raindrop-sync.apply.plist
grep -A1 -E "<string>/" ~/Library/LaunchAgents/com.raindrop-sync.apply.plist | head    # every path absolute?
ls -l /usr/bin/python3
cat ~/.raindrop-sync/log/apply.err.log

Fix. Re-run bootstrap.sh (it rewrites the plist from the template with absolute paths and reloads it), or load it by hand and force one run:

launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.raindrop-sync.apply.plist
launchctl kickstart -p gui/$(id -u)/com.raindrop-sync.apply
tail -2 ~/.raindrop-sync/log/apply.out.log

Expected launchctl list line: - 0 com.raindrop-sync.apply (not running right now, last exit 0). If apply.err.log shows Operation not permitted, move the runtime tree back to ~/.raindrop-sync rather than granting anything. If you must use Homebrew's Python, name the versioned path, never the bin symlink.

Read more. SETUP.md step 6 and the TCC note in SETUP.md step 1. The plist template is launchd/com.raindrop-sync.apply.plist.template.


The skill cannot see the Raindrop tools in a headless session

Symptom. /raindrop-sync runs from a scheduled, background or otherwise non-interactive Claude Code session and reports that find_bookmarks or update_bookmarks are unavailable, or it stalls at the fetch step.

Cause. Phase A reads and writes Raindrop through a Claude Code connector, and that connector is attached to an interactive session. A headless run has no connector, so the tools the skill's procedure names do not exist in that session. This is why classification is on demand rather than scheduled: the design describes a daily fetch agent, but as built it is not installed, because it would have nothing to read through.

Diagnose. In the session that failed, check whether the Raindrop tools are listed at all. If the session was started by a scheduler or with no interactive terminal, that is the cause; there is nothing to debug in the skill.

Fix. Run /raindrop-sync from an interactive Claude Code session with the Raindrop connector attached. Do not work around it by pasting the REST token into a prompt, and do not try to reimplement the fetch with curl inside a scheduled agent: the skill is told never to echo the token, and a token that reaches a transcript must be rotated (Operations). The REST token's job is reconciliation checks through get-token.sh piped into curl, never a substitute for the connector.

Phase B is unaffected: the extension and the launchd agent run unattended on whatever desired.json the last interactive run staged.

Read more. The limitation is stated in the README, Status and limitations and in SETUP.md step 7. Making Phase A schedulable is a roadmap item; see Contributing and Roadmap.


Shorter ones

pending[unknown] in the host log

The worker did not send its client, which means it is running code from before the per-client ledger. It degrades safely (the host keys those rows under the sentinel unknown, costing at most one redundant already_present), but it also means any other fix you shipped to sw.js is not live either. Bump manifest.json's version and press reload on the card.

ABORT: ... every tick, exit 2

Each is a deliberate refusal, not a crash, and it repeats until the condition clears:

Line Meaning What to do
ABORT: Brave Sync is enabled top-level sync_metadata in the file; injected nodes would corrupt sync metadata and upload to every device turn Brave Sync off. Nothing was damaged.
ABORT: unexpected version version != 1 stop; the format changed. Report it with your Brave version.
ABORT: missing root ... one of bookmark_bar, other, synced absent as above
ABORT: stored checksum does not match something else is editing the file, or a Brave upgrade changed the digest run the checksum check in SETUP.md, Verify your install. If it fails on a file Brave itself just wrote, report it.
ABORT: Bookmarks missing the file is absent while the profile exists do not create one. A missing file is a symptom. Restore from backups/ only after finding out why.
ABORT: AccountBookmarks present (or the encrypted variants) a storage regime the writer does not model stop and report

The preflight rules and why each exists are in Safety and Ownership Model.

No Raindrop token found

The resolver found nothing in the environment, .env, or the keychain. Seed it per SETUP.md step 2, then verify with the fingerprint, never with echo. If you seeded a password manager only, note that the unattended resolver deliberately does not depend on one: two scheduled runs once died waiting for a Touch ID prompt with nobody at the machine (RESEARCH.md section 11).

The dry run built a duplicate folder tree beside my real folders

The folder_path in desired.json is relative to the bar. A leading Bookmarks Bar component is tolerated and stripped by both appliers; a path that names the bar under a different title is not, and is created as a folder. This is the exact bug the dry run against a copy was built to catch, so keep running it before trusting a changed writer (SETUP.md, Verify your install, step 7).


Still stuck

Open an issue with the finding number if one applies, what you observed instead, the exact command, your Brave and macOS versions, and whether Brave was running. Include raw output; a launch_context.cc line can be acted on, "it didn't work" cannot. Never include real bookmarks, tokens, collection ids, extension ids, or absolute paths containing a username. Issues: https://github.com/rajatrv-fullstack/raindrop-brave-sync/issues. Questions: https://github.com/rajatrv-fullstack/raindrop-brave-sync/discussions.

Clone this wiki locally