hwangso59515 downloadsLocal-first bridge between reMarkable tablet and Obsidian. Extract PDF highlights as markdown without cloud dependency.
Local-first bridge between your reMarkable tablet and Obsidian. Extract PDF highlights as linked markdown -- no cloud, no subscription, no data leaving your network.
Works with reMarkable 1 (512MB RAM) and reMarkable 2. Supports firmware 3.0+ (v6 .rm format) and legacy firmware (v3/v5 format).
Most Obsidian plugins are pure JavaScript. This one isn't — and the answer to "why?" is short: the reMarkable's v6 .rm binary format has exactly one mature parser, rmscene, and it's Python. There's no JavaScript port. Similarly, PyMuPDF is the only library that reliably extracts PDF text together with its bounding-box coordinates — which is what makes correlating a highlight rectangle back to the underlying text actually work. pdf.js exposes text but not coordinates cleanly.
The plugin spawns Python only when you trigger sync or extraction; if Python isn't installed, the plugin still loads and surfaces a clear error in the setup wizard rather than silently breaking.
| Requirement | Version | Why |
|---|---|---|
| Obsidian | 1.5.0+ | Plugin host |
| Python | 3.8+ | Highlight extraction and page rendering (see above) |
| rmscene | latest | Parses v6 .rm annotation files |
| PyMuPDF | latest | Extracts text from PDF pages, renders page images |
| Syncthing | any | Syncs files between tablet and computer (local network) |
pip install rmscene PyMuPDF
Windows ships with python.exe and python3.exe aliases that open the Microsoft Store instead of running Python. The plugin will silently fail to extract highlights if these are active.
Fix: Settings > Apps > Advanced app settings > App execution aliases -- turn off python.exe and python3.exe.
git clone <repo-url>
cd remarkable-obsidian
npm install
npm run build
Then install the plugin into your vault using the install script:
# Interactive (prompts for vault path)
npm run install-plugin
# With vault path argument
npm run install-plugin -- /path/to/your/vault
# Or via environment variable
OBSIDIAN_VAULT=/path/to/your/vault npm run install-plugin
The script copies main.js, manifest.json, styles.css, extraction/, and templates/ into <vault>/.obsidian/plugins/eink-sync/. Re-run it after each npm run build to update.
Alternative: symlink for live development
If you prefer changes to appear immediately without re-running the install script:
# Mac/Linux
ln -s "$(pwd)" "<vault>/.obsidian/plugins/eink-sync"
# Windows (PowerShell, run as Admin)
New-Item -ItemType Junction -Path "<vault>\.obsidian\plugins\eink-sync" -Target "$(Get-Location)"
| Folder | Default | Purpose |
|---|---|---|
| Sync | reMarkable/Sync |
Raw xochitl files synced from the tablet |
| Highlights | reMarkable/Highlights |
Extracted markdown notes + drawing PNGs |
| Archive | reMarkable/Archive |
Documents archived off the tablet |
Click the reMarkable icon in the sidebar to open the library view, then click the refresh button (top-right). This:
You can also use the command palette: E-Ink Sync: Extract highlights.
| Annotation type | Extracted as |
|---|---|
| Text-selection highlights (long-press to select text) | Blockquoted text with PDF page link |
| Pen strokes on PDFs (drawing/writing over pages) | PNG image of the annotated page |
| Notebook pages (Quick sheets, etc.) | PNG image of each page with strokes |
Note: Text-selection highlights produce the best results -- you get the actual text as searchable markdown. Pen strokes (including the pen-style highlighter) are rendered as images only. The extractor does not currently OCR pen-stroke annotations to recover text.
Each document produces a markdown file like:
---
title: "Paper Title"
source_pdf: "[[uuid.pdf]]"
source_type: pdf
highlight_count: 3
remarkable_uuid: abc-123
---
<!-- eink-sync:start -->
### Page 5
> The highlighted text passage
> -- [[uuid.pdf#page=5|Page 5]]
![[Paper Title_p5.png|500]]
<!-- eink-sync:end -->
Everything between the <!-- eink-sync:start/end --> markers is managed by the plugin. Content outside the markers (your own notes) is preserved across re-extractions. Notes written by the pre-rename plugin (which used <!-- remarkable-bridge:start/end -->) are still recognised and migrated to the new markers on the next extraction.
reMarkable tablet
|
| Syncthing (local WiFi / USB)
v
Vault/reMarkable/Sync/ <-- raw xochitl files (UUIDs)
|
| Python extraction pipeline
v
Vault/reMarkable/Highlights/ <-- readable markdown + PNGs
All data stays on your local network. The plugin never contacts reMarkable Cloud or any external server.
By default, the plugin only processes documents modified since the last extraction (based on the lastModified timestamp in each document's .metadata file). To force a full re-extraction of all documents, use the Sync button in the library view or the command palette.
When you change any folder path in settings, a migration dialog appears:
The file watcher and Syncthing configuration are updated automatically.
The archive folder must not be inside the sync folder. If it is, Syncthing will sync archived documents back to the tablet, defeating the purpose of archiving.
The plugin supports multiple sync sources -- each with its own sync folder, Syncthing folder ID, and independent extraction timestamp.
To add a second tablet:
source field in frontmatter so you can tell which tablet produced themExisting single-source setups are migrated automatically to a "Default" source on upgrade -- no reconfiguration needed.
Each source can optionally specify its own highlights subfolder to keep notes organized per tablet.
If you sync your Obsidian vault between computers via Syncthing, the plugin is designed to avoid conflicts:
data.jsondate_highlighted uses the document's modification date from the tablet (same on both machines), not the extraction date.obsidian/plugins/ (not synced by default), not in the sync/highlights foldersIf you already have sync-conflict files from before this fix, you can safely delete them:
find /path/to/vault -name "*sync-conflict*" -delete
All folder paths are resolved relative to the vault root. The plugin detects when multiple vault instances use overlapping folders:
When you disable and re-enable the plugin (e.g., in a subvault), it re-reads settings from that vault's data.json, re-creates folders if needed, and restarts watchers. Each vault's state is fully independent.
http://127.0.0.1:8384 and verify the folder is "Up to Date"python --version (or python3 --version) in a terminal. If it opens the Microsoft Store, disable the aliases (see Prerequisites above)lastModified timestamp hasn't changed. Use the Sync button in the library or the command palette to force a full run..metadata and .content file in the sync folderpageCount: 0 and empty page lists -- they appear in the library but cannot produce highlights until opened on the tabletWhen you delete highlights on the tablet and re-sync, the extraction finds 0 highlights for that document. If the document also has no pen stroke drawings, the pipeline skips updating the file (it treats "nothing to show" as "nothing to do"). The old highlight note remains unchanged.
Workaround: manually delete the stale highlight note, or enable overwriteExisting in settings to force a fresh write every time.
npm install # install dependencies
npm run dev # watch mode (rebuilds on file change)
npm run build # production build
npm test # run test suite
npm run test:watch # run tests in watch mode
npm run lint # lint TypeScript source
src/
plugin/ # Obsidian plugin lifecycle, settings, library view
pipeline/ # Extraction pipeline, document discovery, markdown rendering
ssh/ # SSH client for tablet communication
device/ # Firmware detection, device management
sync/ # Syncthing configuration and sync orchestration
utils/ # Logger, shared utilities
extraction/ # Python scripts (called via child_process)
extract.py # Main entry point for highlight extraction
metadata_parser.py # xochitl .metadata/.content file parsing
highlight_extractor.py # GlyphRange and legacy highlight parsing
render_pages.py # PNG rendering of annotated pages
legacy_rm_parser.py # v3/v5 .rm file format parser
templates/ # Handlebars template for highlight notes
The plugin has three layers:
ssh/, device/) -- SSH connection, firmware detection, Syncthing installation on the tabletsync/) -- Syncthing configuration, file sync orchestrationpipeline/, extraction/) -- Document discovery, Python-based highlight extraction, markdown renderingThe pipeline delegates to Python for two tasks: (1) parsing .rm files via rmscene and extracting highlight text via PyMuPDF, and (2) rendering annotated pages as PNGs. Communication is one-directional: TypeScript spawns Python with CLI arguments, Python returns JSON on stdout.
This plugin requests broader access than a typical Obsidian plugin because it bridges your tablet (an external device on your local network) to your vault. Here's exactly what it does and why:
| Capability | Why it's needed |
|---|---|
| Filesystem access outside the vault | Reads .rm/.metadata/.content files from the Syncthing sync folder (which lives outside your vault, since Syncthing manages it). Writes rendered PNGs and extracted markdown into your vault. |
Shell execution (child_process) |
Spawns Python for highlight extraction (rmscene + PyMuPDF) and SSH for one-time tablet setup. Both are essential -- there's no JavaScript equivalent for parsing reMarkable's v6 .rm binary format. |
Hostname read (os.hostname) |
Scopes the "last extracted at" timestamp per machine, so if you sync your vault between two computers via Syncthing they don't fight over data.json and produce sync-conflict files. |
| Vault file enumeration | Used by the "Send document to reMarkable" command to find PDFs/EPUBs in your vault. |
The plugin never makes external network requests. All sync happens over your local network via Syncthing or SSH to your tablet. No data goes to reMarkable Cloud, no telemetry, no analytics, no third-party servers.
lastModified field in .metadata may not update when annotations change, causing incremental mode to miss updates. Use full re-extraction if highlights seem stuck.MIT