Skip to content

Operations

Rajat edited this page Sep 9, 2026 · 2 revisions

Operations

This page is for day two: where the logs are and what healthy output looks like, what runs when, and how to do the handful of maintenance jobs (rebuild a folder, rotate the token, move machines, uninstall) without breaking the guarantees described in Safety and Ownership Model.

Everything below assumes a working install per SETUP.md. For what each file is, see Ledger and File Formats. For what to do when something is wrong, see Troubleshooting. Terms are defined in the Glossary.


Health check: ~/.raindrop-sync/bin/doctor.sh (1.1.0 and later). The profile the installer chose is in ~/.raindrop-sync/config.json; every snippet below reads it from there.

Cadence: what runs, when, and what it touches

Component Started by How often Touches
Phase A, the /raindrop-sync skill you, in an interactive Claude Code session on demand Raindrop API, state.db, taxonomy.json, collection-map.json, desired.json. Never the browser.
Extension service worker (sw.js) a chrome.alarms alarm, plus worker load, onInstalled, onStartup every 60 seconds while Brave is open (the alarms API clamps to a one-minute floor) the running browser through chrome.bookmarks. Never the file, never the network.
Native messaging host (native_host.py) spawned by the worker on each tick one short-lived process per tick, roughly 30 ms, then exits reads desired.json and reset_folders.json (and deletes the latter once the extension reports), reads and writes state.db, appends to native_host.log
Fallback file writer (apply_brave.py) launchd, label com.raindrop-sync.apply every 900 seconds (StartInterval), and once when the agent loads (RunAtLoad) Brave's Bookmarks file, backups/, applied.json
Brave itself any bookmark change, and quit re-serialises its in-memory tree over the file about 2.5 s after a change and at quit (a Chromium internal, do not depend on the number) the Bookmarks file and Bookmarks.bak

Two consequences of that table are worth holding in your head:

  • The extension is the fast path, the file writer is the certain path. A staged promotion lands in a running Brave within about a minute through the extension. If Brave is closed or the extension is off, the file writer rewrites the file and the change appears at the next Brave start.
  • Neither applier needs Phase A to be running. Phase B reads a file. You can hand-write a desired.json and both appliers will apply it. See Architecture for why the phases are split that way.

Logs and what healthy looks like

All logs live under ~/.raindrop-sync/log/. Nothing in the repo rotates them.

native_host.log (the extension path)

Written by the native host on every extension tick. Healthy steady state is three lines roughly once a minute while Brave is open:

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

How to read the middle line:

  • pending[brave], not pending[unknown]. The bracket is the client the worker reported. unknown means a worker running code that predates the per-client ledger, which is a sign the extension was never reloaded after an update (see the reload section below).
  • 0 of 42 staged means 42 items are in desired.json and every one of them is already recorded as applied for this client. That is the healthy resting state.
  • N of 42 staged with N > 0 means N items are pending for this browser. On the next line pair you should see the host record results:
... applied[brave]: recorded 3 results
... [brave] created folder: Woodworking

If N stays non-zero tick after tick, the extension is failing to create or to report. Look for [brave] extension error on <id>: ... lines, then go to Troubleshooting.

Because the host writes three lines a minute whenever Brave is open, the file grows by a few thousand lines a day. Truncate it whenever you like; the host reopens it in append mode on every write:

: > ~/.raindrop-sync/log/native_host.log

apply.out.log and apply.err.log (the fallback path)

launchd redirects the file writer's stdout and stderr here. Healthy is one line per 15-minute tick in apply.out.log, one of:

2026-01-01T12:00:00.000000+00:00 no desired.json; nothing to do
2026-01-01T12:00:00.000000+00:00 desired.json empty; nothing to do
2026-01-01T12:00:00.000000+00:00 all 42 promotions already present and marked; no write needed

and an empty apply.err.log. The word "marked" in that third line also covers nodes matched by URL, which is how the writer recognises bookmarks the extension created (the extension cannot write the file marker; see Safety and Ownership Model).

When there is work, a successful tick looks like this:

... 2 to apply (40 already present)
... backup: ~/.raindrop-sync/backups/Bookmarks.<stamp>
... created folder: Woodworking
... OK: 2 applied, 42 total marked nodes present
... NOTE: Brave is running. It may overwrite this from memory; the next tick re-applies.

The real backup: line shows the path expanded under your home directory, and <stamp> is a UTC timestamp of the form 20260101T120000Z; the writer also copies Bookmarks.bak to Bookmarks.bak.<stamp> beside it and keeps ten of each.

That NOTE is not an error. If Brave was running, the next tick may show 2 to apply again, then OK again, until Brave has a quiet tick or restarts. That oscillation is the retry loop working as designed; see RESEARCH.md finding 4. Once the extension is installed and Brave is open, the extension normally wins first and the file writer only ever logs "already present".

Lines that are not healthy:

  • ABORT: ... (exit 2): a preflight refusal. Each names its reason: Brave Sync is enabled. External nodes would corrupt sync metadata and upload to every device. Disable bookmark sync or remove this job., unexpected version, missing root, stored checksum does not match, Bookmarks missing, or one of the account/encrypted storage files being present. The writer refuses rather than guesses, and will keep refusing every tick until the condition clears.
  • WRITE FAILED: ... or VERIFY FAILED: ... (exit 1): the writer restored its own backup and exited non-zero. Treat as a bug report.
  • Anything at all in apply.err.log. A bare Operation not permitted there is a TCC denial, which under launchd produces no dialog.

Exit codes, for launchctl print: 0 ok or nothing to do, 1 hard failure with backup restored, 2 preflight abort.

The extension's own console

The worker logs one line when it actually creates something:

raindrop-sync[brave]: 2 created of 2 pending (alarm)

and stores the last run in chrome.storage.local under lastRun as {at, reason, applied, results}. On a normally loaded unpacked extension the card at brave://extensions usually offers an "Inspect views" link for the service worker; in a profile launched with command-line flags that console is effectively unreachable, which is why native-messaging diagnosis goes through Brave's own stderr log instead (RESEARCH.md finding 2).

Phase A's run summary

The skill reports in the Claude Code session and records a row in the run-history table of state.db (table names are in Ledger and File Formats). The one number to look at every run is the canary, classified_count. On a steady-state run it should be a handful. If it is in the dozens or hundreds on a library that barely changed, delta detection is broken; stop and read the reclassification entry in Troubleshooting before running again, because every extra run costs real LLM work and it will not settle on its own.

The summary should also say which new collections were created, if any. A new collection is the expected outcome when nothing in the pinned taxonomy fits (Taxonomy and Classification).


Who applied it: the extension or the fallback?

Both appliers are additive and both skip anything already present by URL, so the result on the bar is the same either way. When you need to know which path did the work, the evidence differs:

Evidence Extension Fallback file writer
Log line applied[brave]: recorded N results in native_host.log OK: N applied in apply.out.log
Ledger a row in state.db table extension_applied with status = 'created' and a bookmark_id (Chromium's node id) applied.json rewritten with a fresh at timestamp and the list of GUIDs
Backups none taken a new Bookmarks.<stamp> and Bookmarks.bak.<stamp> pair in backups/
The node in the Bookmarks file no meta_info marker (the API cannot write one) meta_info: {"raindrop_sync": "v1"}
GUID assigned by Chromium deterministic uuid5 from the Raindrop id

status = 'already_present' in extension_applied means the extension found the URL there already, created by the file writer or by you.

Quick checks:

# last few extension results, newest first
sqlite3 ~/.raindrop-sync/state.db \
  "SELECT client, raindrop_id, status, bookmark_id, applied_at
   FROM extension_applied ORDER BY applied_at DESC LIMIT 10;"

# last write by the fallback
python3 -c 'import json;d=json.load(open("'"$HOME"'/.raindrop-sync/applied.json"));print(d["at"], len(d["applied_guids"]), "guids")'

# every node in the live file that carries the marker (read-only)
python3 - <<'EOF'
import json, os
p = json.load(open(os.path.expanduser("~/.raindrop-sync/config.json")))["profile"] + "/Bookmarks"
def walk(n, path=""):
    path = f"{path}/{n['name']}" if path else n["name"]
    if (n.get("meta_info") or {}).get("raindrop_sync") == "v1":
        print(n["type"], path)
    for c in n.get("children", []): walk(c, path)
walk(json.load(open(p))["roots"]["bookmark_bar"])
EOF

Note that applied.json is written by the fallback but never read back by anything in the repo; it is a record for you, not state the system depends on.


A routine health check

Five commands, none of which print the token or touch the browser file:

launchctl list | grep raindrop                    # expect: -  0  com.raindrop-sync.apply
tail -3 ~/.raindrop-sync/log/apply.out.log        # one healthy line per 15 min
cat  ~/.raindrop-sync/log/apply.err.log           # expect empty
tail -3 ~/.raindrop-sync/log/native_host.log      # pending[brave]: 0 of N, within the last minute (Brave open)
~/.raindrop-sync/bin/get-token.sh | shasum -a 256 | cut -c1-8   # 8 hex chars, no error

Add the checksum check from SETUP.md, Verify your install after any Brave upgrade, and re-run the meta_info survival test on a throwaway profile after a major Brave upgrade. Marker survival is verified on Brave 152 only, and marker ownership is the assumption most likely to be broken upstream (Research Status).


When the extension needs a reload, and when it does not

Unpacked extensions do not hot-reload. The service worker that is running is the one loaded when you last pressed reload on the card, whatever the file on disk says now. This is the single most common reason a fix "does not work".

A reload is required

  • After any change to sw.js. Bump "version" in manifest.json first, then press reload on the card at brave://extensions. Both steps; the bump is what makes it visible on the card that new code is live.
  • After any change to manifest.json other than the key (permissions, version).
  • A changed key is not a reload, it is a new identity: the id is derived from the key. Remove the old card, update allowed_origins in both host manifests, Load unpacked again, and confirm the new id on the card. bootstrap.sh does the manifest work for you when key.pem changes.

Where to edit matters. bootstrap.sh copies sw.js from the checkout into ~/.raindrop-sync/extension/ and regenerates manifest.json there from the checkout's manifest plus your key. An edit made directly under ~/.raindrop-sync/extension/ is overwritten the next time bootstrap runs. Edit in the checkout, bump the version in the checkout's extension/manifest.json, re-run bootstrap, then reload the card.

How to tell the worker is stale: the version on the card differs from "version" in ~/.raindrop-sync/extension/manifest.json, or the host log shows pending[unknown] (a worker that does not send its client).

A reload is not required

  • desired.json, reset_folders.json, state.db. The host reads them fresh on every tick. A newly staged promotion is picked up within a minute with no action from you.
  • native_host.py. The host is spawned per tick and exits, so an edit is live on the next tick. Keep the #! line and the executable bit, and never print() to stdout: a stray byte corrupts the length-prefixed stream and Brave drops the connection without saying why.
  • apply_brave.py. launchd runs it fresh each tick. A change to the plist is different: launchctl bootout then bootstrap the agent (or re-run bootstrap.sh), which has nothing to do with the extension.
  • Token rotation, taxonomy changes, ledger repairs. Phase A only; Phase B never sees the token.
  • The host manifest. The browser consults it when it launches the host. If an allowed_origins or path edit does not seem to take, reloading the card costs nothing, but it is not normally needed.

Rebuilding a managed folder with reset_folders.json

Sometimes the layout under a managed folder needs to change (a category renamed in the taxonomy, a folder that was hand-tidied into a state the sync no longer recognises). The tool never deletes on its own, so there is an explicit, user-invoked request for a rebuild.

The mechanism, as implemented

  • You write ~/.raindrop-sync/reset_folders.json: a JSON array of folder titles, for example ["Raindrop"].
  • On the next extension tick, the host includes those names in its pending reply, but only if there are pending items for that client. With nothing pending, the file is ignored.
  • The worker calls chrome.bookmarks.removeTree on each named folder that is a direct child of the bookmarks bar, then creates the pending items, re-creating the folder path as it goes. Nested folders cannot be named; only top-level ones match.
  • The fallback file writer does not read this file at all. A reset is an extension-only operation and needs Brave open with the extension enabled.
  • The file is consumed once. The host reads it in op_pending and deletes it in op_applied, after the extension has reported its results, logging reset_folders.json cleared. You do not need to remove it yourself, and it cannot fire again on a later tick.

Two things the mechanism will not protect you from

  1. removeTree destroys everything inside the folder, including bookmarks you dropped in there yourself. The file-writer design in RESEARCH.md finding 6 harvests and re-attaches unmarked children; the extension's reset does not. Only ever name a folder this tool owns end to end (the top-level mirror folder, for example), never a curated folder, and move out anything of your own before you write the file.
  2. Only pending items are rebuilt. Items already recorded in extension_applied for this client are not pending, so after the tear-down they would not come back through the extension (the fallback would eventually re-add anything in desired.json by file write, on a quiet tick or the next Brave start). To rebuild the whole folder in one tick, make its items pending again first.

Procedure

cd ~/.raindrop-sync

# 1. Make every item under the managed folder pending again for the extension.
#    Example folder title: Raindrop. desired.json items are keyed raindrop_id.
python3 - <<'EOF'
import json, sqlite3, os
root, folder = os.path.expanduser("~/.raindrop-sync"), "Raindrop"
def top(path):
    parts = [p for p in path.split("/") if p and p != "Bookmarks Bar"]
    return parts[0] if parts else ""
ids = [d["raindrop_id"] for d in json.load(open(f"{root}/desired.json")) if top(d["folder_path"]) == folder]
con = sqlite3.connect(f"{root}/state.db")
con.executemany("DELETE FROM extension_applied WHERE raindrop_id=?", [(i,) for i in ids])
con.commit(); print("made pending:", len(ids))
EOF

# 2. Ask the extension to tear the folder down before it re-creates.
printf '["Raindrop"]\n' > reset_folders.json

# 3. With Brave open, wait about a minute, then confirm.
tail -6 log/native_host.log
#   pending[brave]: 12 of 42 staged, reset ['Raindrop']
#   reset_folders.json cleared
#   applied[brave]: recorded 12 results
#   pending[brave]: 0 of 42 staged

# 4. Nothing to clean up: the host deleted reset_folders.json when it recorded the results.
test ! -e reset_folders.json && echo "reset request consumed"

The nodes the extension re-creates carry no file marker (the API cannot write one), so the next fallback tick recognises them by URL and logs "already present". That is expected.

If you instead want to change the taxonomy itself, that is a different, heavier operation: an explicit re-derivation that bumps taxonomy_version and reclassifies everything. See Taxonomy and Classification.


Rotating the Raindrop token

Rotate when the token has reached any terminal, log, or agent transcript (an echo, a cat .env, a debugging session that printed it), when you retire a machine, or on whatever schedule you keep for long-lived credentials. The test token authenticates as you with no expiry and no scoping, so treat it like a password.

  1. In Raindrop: app.raindrop.io, Settings, Integrations, open your app. Delete the old test token and create a new one. If the old token was exposed, delete it first; it is dead the moment you do, and Phase A is on demand so there is no unattended job to disturb.

  2. Seed the new token without echoing it. read -rs keeps the paste out of the display and out of shell history:

    read -rs RAINDROP_TOKEN            # paste, press Return; nothing is displayed
    export RAINDROP_TOKEN
    ~/.raindrop-sync/bin/seed-token.sh --from-env   # the flag is required here: without it the script prompts and ignores the variable

    Alternatively run seed-token.sh with no flag and paste at its own hidden prompt; that route needs no exported variable at all.

  3. Update every store the resolver consults, not just the keychain. get-token.sh resolves in a fixed order and stops at the first hit: $RAINDROP_TOKEN, then ~/.raindrop-sync/.env, then the login keychain (service raindrop-api). Because .env is consulted before the keychain, a stale value there keeps serving the old token no matter what the keychain holds. If you keep .env, rewrite it with the shell's printf builtin (not echo, and not a subprocess that would put the value in argv):

    printf 'RAINDROP_TOKEN=%s\n' "$RAINDROP_TOKEN" > ~/.raindrop-sync/.env && chmod 600 ~/.raindrop-sync/.env
    unset RAINDROP_TOKEN

    If you also keep a copy in a password manager, update it there too, although the resolver does not read from one. If you rely on the keychain alone, delete .env and skip this step.

  4. Verify without printing. The fingerprint is a one-way SHA-256 prefix and safe to show; the HTTP check passes the token to curl on stdin so it never appears in ps or scrollback:

    ~/.raindrop-sync/bin/get-token.sh | shasum -a 256 | cut -c1-8
    printf 'header = "Authorization: Bearer %s"\n' "$(~/.raindrop-sync/bin/get-token.sh)" \
      | curl --config - -s -o /dev/null -w '%{http_code}\n' https://api.raindrop.io/rest/v1/user

    Expected: a fingerprint that differs from the old one, and 200. A 401 means an old value is still being resolved first; go back to step 3.

  5. Nothing else changes. The ledger, taxonomy and desired.json do not contain the token. No extension reload, no launchd action: Phase B never uses the token.

If the old value is in a Claude Code transcript, rotation is the only remedy; there is no way to un-print it. The skill is told to say so plainly when it happens (SKILL.md, Secrets).


Moving to a new machine

The runtime tree mixes three kinds of thing: identity (key.pem), knowledge (the ledger and taxonomy), and machine-specific plumbing (manifests and plists with absolute paths). Copy the first two, regenerate the third.

Copy, privately

From ~/.raindrop-sync/ on the old machine:

File Why
key.pem pins the extension id. Without it the new install gets a different id, and every host manifest must change to match.
state.db the ledger. Without it the first run on the new machine sees every bookmark as NEW and reclassifies the whole library, at full LLM cost.
taxonomy.json the pinned taxonomy. Without it the first run derives a new one, and your folders regroup under different names.
collection-map.json taxonomy path to Raindrop collection id. Regenerable in principle, cheap to carry.
desired.json the staged promotions, so the new browser receives them.
.env only if you rely on the cleartext fallback; otherwise re-seed the token instead.

Move them over a private channel (an encrypted disk, a direct copy). Not a shared drive, not a chat, and never a repository: .gitignore already excludes every one of these, which tells you how they are meant to be treated.

Regenerate

  • The host manifests (both directories) and the launchd plist contain absolute paths under your home directory, and launchd does not expand ~. bootstrap.sh rewrites all three from the current $HOME.
  • extension/manifest.json and .extension-identity.json: derived from key.pem by bootstrap. If key.pem is in place before bootstrap runs, it prints Reusing existing key.pem and the id matches the old machine.
  • The keychain item: per machine. Seed the token again.
  • The skill copy under ~/.claude/skills/raindrop-sync/: bootstrap installs it.
  • The extension card: Load unpacked is manual and per profile; there is no flag that installs into an already-running browser.

Do not copy

backups/ (snapshots of the old machine's Bookmarks file), log/, and applied.json (never read back). None of them mean anything on the new machine.

Order of operations

# on the new machine
# 1. Install Brave, run it once, and make sure Brave Sync is off. Run the Step 0 check from SETUP.md.
# 2. Put the identity and knowledge in place BEFORE bootstrap, so it reuses the key.
mkdir -p ~/.raindrop-sync && chmod 700 ~/.raindrop-sync
#    copy key.pem state.db taxonomy.json collection-map.json desired.json into ~/.raindrop-sync/
chmod 600 ~/.raindrop-sync/key.pem
# 3. Clone and bootstrap. Compare the printed id with the old machine's.
git clone <this repo> && cd raindrop-brave-sync && ./scripts/bootstrap.sh
# 4. Seed the token (keychain is per machine), then Load unpacked and confirm the id on the card.
# 5. The ledger says the old browser already applied everything. Make it pending again for the new one;
#    the extension re-checks by URL, so the worst outcome is one tick of already_present rows.
sqlite3 ~/.raindrop-sync/state.db "DELETE FROM extension_applied;"
# 6. Run the SETUP.md verification list, including the dry run against a copy of the new Bookmarks file.

Then run Phase A on one machine only. The ledger is a local file and is not designed to be shared between two classifiers writing back to the same Raindrop library. Retire the old machine's install with the uninstall steps below, or at least stop running the skill there.

Why key.pem must not be lost or published

The extension id is the first 16 bytes of the SHA-256 of the DER-encoded public key, rendered in a to p, and that public key already sits in the open in manifest.json as its key field. The private half is therefore not what stops someone from wearing your id when an unpacked extension loads; what key.pem buys you is reproducibility. It is the only thing that regenerates the same key field, and so the same pinned id, on a rebuild or on a new machine, and that id is what every copy of the host manifest's allowed_origins names. Losing the key means a new id and a manual update of every allowlist copy. Keep it mode 0600 inside the 0700 runtime tree, keep one private backup, never commit it, and do not publish it: it is a private key, and there is no upside to letting it out. The repository's .gitignore excludes *.pem and .extension-identity.json for this reason. Details on the identity derivation are in RESEARCH.md finding 3.


Uninstalling completely

The install touched six places. Nothing here deletes a bookmark: the tool is additive, and so is its removal.

# 1. Stop and remove the fallback agent
launchctl bootout gui/$(id -u)/com.raindrop-sync.apply
rm ~/Library/LaunchAgents/com.raindrop-sync.apply.plist

# 2. Remove the extension: brave://extensions, Remove on the Raindrop Sync card

# 3. Remove both copies of the host manifest
rm "$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.raindrop_sync.host.json"
rm "$HOME/Library/Application Support/BraveSoftware/Brave-Browser/NativeMessagingHosts/com.raindrop_sync.host.json"

# 4. Remove the token from the keychain, then revoke it in Raindrop (Settings, Integrations)
security delete-generic-password -s raindrop-api

# 5. Remove the skill
rm -r ~/.claude/skills/raindrop-sync

# 6. Remove the runtime tree. It holds key.pem, the ledger, and your Bookmarks backups.
#    Save key.pem and state.db somewhere private first if you might come back.
rm -r ~/.raindrop-sync

What stays, on purpose:

  • Every bookmark on the bar, including the ones this tool added. If you want the mirror folder gone, delete it by hand in Brave. Promotions inside your curated folders are the marked nodes; list them with the read-only snippet in "Who applied it" above before you remove the runtime tree, since that is also how you find them.
  • Raindrop collections and tags. Raindrop was the source of truth; the classification stays in your library.
  • Chrome's NativeMessagingHosts directory, if bootstrap created it. Remove it only if it is empty; other vendors install there too.
  • The repository checkout, which holds no state.

If you ever reinstall with a fresh key.pem, you get a new extension id. That is fine: nothing outside the host manifests refers to the old one.

Clone this wiki locally