Plugin for Hermes Agent that reads, searches and analyzes your Firefox bookmarks — from the Firefox Sync cloud (Sync 1.5, end-to-end encrypted) or from a local profile (places.sqlite), with no Firefox installed on the machine. v1 is read-only: it never modifies bookmarks on the server.
flowchart TD
HA["Hermes Agent"] -->|tool / skill / commands| FB["firefox-bookmarks"]
FB -->|cache-first: every query is offline| CACHE[("SQLite + FTS5 cache<br/>(local)")]
CACHE -->|sync on-demand| SRC
SRC{{"Firefox Sync 1.5 (FxA + HAWK)<br/>or places.sqlite (local mode)"}}
- Sync: Firefox Account login (with 2FA/TOTP), HAWK, HKDF-SHA256, AES-256-CBC+HMAC on BSOs — implemented in pure Python (
ffsync/, zero Hermes dependencies) - Search: FTS5 over titles/URLs/tags/folder structure + domain, folder, tag and time-range filters; natural queries ("did I save something about systemd?")
- Deterministic analysis (no LLM): counts, per-domain distribution, duplicates, orphan links (URLs without a title)
- Opt-in link check: concurrent HEAD (4 threads), timeout, politeness; never automatic
- Local mode: reads
places.sqlitefrom a copy only (zero risk to the profile) - Skill
firefox-bookmarkswith operating rules (short queries, consent for link-check)
- A recent Hermes Agent with the plugin system (user dir:
~/.hermes/plugins/or$HERMES_HOME/plugins/) - Python 3.10+ in the Hermes venv (already included in standard installs)
- Runtime:
httpx+cryptography(already in the Hermes venv); dev:pytest
git clone <repo-url> ~/.hermes/plugins/firefox-bookmarks
# or: unzip into ~/.hermes/plugins/Activate the plugin (the gateway detects user plugins in plugins/):
hermes plugins list # verify that firefox-bookmarks appears
hermes plugins doctor firefox-bookmarksThe plugin requires 0 privileged capabilities and does not override built-in tools.
Running Hermes in Docker or headless? Skip this section — see Running inside Docker: email + password come from env vars.
Credentials never pass through Telegram/LLM (the password is used only in the FxA login; the refresh token stays on disk with 600 permissions).
Plan A — Firefox Sync (requires a Firefox Account with Sync active):
cd ~/.hermes/plugins/firefox-bookmarks
python -m ffsync.probe setup # password from the terminal (getpass, never shown)
python -m ffsync.probe sync # first sync + verification
python -m ffsync.probe status # credentials + cache statusPlan B — local profile (no account):
python -m ffsync.probe local ~/.mozilla/firefox/<profile>/ # or a path to places.sqliteHeadless deployment: sync mode only, no Firefox profile inside the container, no interactive setup. The Firefox Account email and password come from environment variables and are used exactly once — afterwards the saved refresh token on the volume is enough. If the account has 2FA enabled, Hermes asks you for the current code in chat during the first sync.
| Variable | Required | Purpose |
|---|---|---|
FFB_EMAIL |
yes | Firefox Account email |
FFB_PASSWORD |
yes | Account password — used for the one-time login, never stored |
HERMES_HOME (or FFB_HOME) still controls where plugin-data/ lives. Leave FFB_MODE unset — sync is the default.
services:
hermes:
image: your-hermes-image
env_file: .env
environment:
HERMES_HOME: /data/hermes
volumes:
- hermes-data:/data/hermes
- ./firefox-bookmarks:/data/hermes/plugins/firefox-bookmarks:ro
volumes:
hermes-data:.env (never commit it; chmod 600):
FFB_EMAIL=you@example.com
FFB_PASSWORD=your-password
The container needs egress to accounts.firefox.com and the Firefox Sync storage host.
- First
/bookmarks-sync(orfirefox_bookmarks_sync): nocreds.jsonyet → the plugin logs in withFFB_EMAIL/FFB_PASSWORD, savesrefreshToken+ key material to/data/hermes/plugin-data/firefox-bookmarks/creds.json(0600), then syncs. - With 2FA enabled the first sync returns
TOTP_REQUIRED— Hermes asks you in chat for the current OTP code and retries with it (totp_codeargument). The code expires in ~30 s and is never stored, and it is never asked for again. - Later syncs reuse
creds.json; the volume makes it survive restarts and re-creates. The TOTP is never asked for again. - Once logged in, drop
FFB_EMAIL/FFB_PASSWORDfrom.env— a savedcreds.jsonalways wins over the env vars. To force a re-login, deletecreds.jsonfrom the volume and set the env vars again.
The password is used only for the FxA login: never written to disk, never logged, never sent through chat/LLM.
docker compose exec hermes python -m ffsync.probe statusAfter the first sync (or with plan B), everything works from chat:
- "What did I save about systemd?" →
firefox_bookmarks_search - "How many bookmarks do I have, and where are most of them?" →
firefox_bookmarks_analyze - "Show me the folder structure" →
firefox_bookmarks_tree - "Check whether the links in a folder are dead" →
firefox_bookmarks_recheck(requires consent) - Commands:
/bookmarks-sync(immediate refresh),/bookmarks-report(overview + domains + duplicates)
The plugin has no fixed cadence: sync is on-demand (tool firefox_bookmarks_sync) or schedulable with a Hermes job.
Concrete v1 behavior:
firefox_bookmarks_search/analyze/treenever trigger a sync: they read the local cache. If the data is older than 7 days, the response includeshint: consider sync.- Sync is not incremental in v1 (full list of
bookmarks): it is very light (<1000 records), but it is still better not to run it on every query.
Recommended cadences:
| Need | Setup |
|---|---|
| "always up to date" | job every 30 minutes (see below) |
| daily use | job every night (e.g. 03:00) — the example from the development plan |
| occasional | on-demand only (/bookmarks-sync) |
Example job (natural language; Hermes creates the cron job):
every 30 minutes, if the bookmark cache is empty or older than 30 minutes,
run firefox_bookmarks_sync and do not reply if everything is fine
Note: every sync consumes FxA quota (Sync rate limits are per-collection; pagination handles 429s with backoff). A job every 30 minutes is reasonable; every 5 minutes is not.
cd firefox-bookmarks
python -m pytest tests -q # 13 offline tests (crypto, HAWK, parser, cache, handler)| Symptom | Cause / fix |
|---|---|
NOT_CONFIGURED |
Run python -m ffsync.probe setup |
SESSION_EXPIRED |
The refresh token expired: run probe setup again |
ACCOUNT_LOCKED |
Mozilla locked the account after too many attempts: wait and retry |
NEEDS_VERIFICATION |
The FxA account has not completed email verification: finish it on accounts.firefox.com |
STORAGE_ERROR 429 |
Sync rate limit: already handled with backoff; avoid back-to-back syncs |
| The cache looks stale | Check stats.last_sync (epoch) and run /bookmarks-sync |
- Read-only v1: no writes toward the Sync server.
- The password is never stored: only
refreshToken+keyFetchToken(on disk,600). - Credentials never transit through logs, tool responses, or scheduled jobs — the only exception is the one-time TOTP code (a tool argument, valid ~30 s, never persisted).
- Link-check: never automatic; requires an explicit request (see the skill).
- Data: bookmarks stay end-to-end encrypted on Firefox Sync; the local cache is in plaintext on disk — if you share the machine, protect the
plugin-data/folder.
MIT — see LICENSE.