hwangso595148 downloadsSync reMarkable documents, notebooks, and highlights over SFTP or Syncthing. No reMarkable Cloud required.
Sync reMarkable documents, notebooks, and highlights over SFTP or Syncthing. No reMarkable Cloud required.
Works with reMarkable 1 (512MB RAM) and reMarkable 2. Supports firmware 3.0+ (v6 .rm format) and legacy firmware (v3/v5 format).
reMarkable firmware 3.x stores annotations in v6 .rm files. E-Ink Sync uses rmscene to parse those files and PyMuPDF to match highlight coordinates with PDF text. Both are Python libraries, so extraction runs in a local Python process.
Python runs only during setup, sync, or extraction. The setup wizard creates a private environment and installs the tested package versions automatically. If Python is unavailable, setup stops with an actionable installation message instead of completing with broken extraction.
| Requirement | Version | Why |
|---|---|---|
| Obsidian | 1.7.2+ | Plugin host |
| Python | 3.10+ | Host runtime for highlight extraction and page rendering |
| rmscene | managed automatically | Parses v6 .rm annotation files |
| PyMuPDF | managed automatically | Extracts text from PDF pages and renders page images |
| reMarkable SSH access | enabled | Required for direct SFTP sync and tablet management |
| Syncthing (optional) | any | Provides continuous background sync as an alternative to SFTP |
| Tesseract (optional) | 5.x | Local handwriting OCR; only needed for Search handwriting (OCR) |
Install Python 3.10 or newer from python.org, or install uv. When uv is available, it can provision the tested Python version itself. During the final wizard step, E-Ink Sync creates a private environment outside your vault, installs the tested rmscene and PyMuPDF versions, and verifies both imports before setup can finish. An internet connection is required for this one-time package installation.
If you disable Managed Python environment, install the packages yourself:
pip install rmscene==0.8.0 PyMuPDF==1.28.0
The Search handwriting (OCR) setting needs Tesseract plus two Python packages. Skip this unless you want handwritten pages to be searchable.
pip install pytesseract Pillow
Then install the Tesseract binary: winget install UB-Mannheim.TesseractOCR (Windows), brew install tesseract (macOS), or apt install tesseract-ocr (Linux). The plugin finds it on PATH or at the standard install location; set the TESSERACT_CMD environment variable to override.
Windows ships with python.exe and python3.exe aliases that may open the Microsoft Store instead of running Python. The plugin also checks the Windows py launcher and standard Python installation directories, but disabling the Store aliases prevents confusing manual-command behavior.
Fix: Settings > Apps > Advanced app settings > App execution aliases -- turn off python.exe and python3.exe.
GitHub Actions builds and attests every downloadable release asset. After downloading an asset, verify that it was built from this repository:
gh attestation verify main.js --repo hwangso595/eink-sync
gh attestation verify manifest.json --repo hwangso595/eink-sync
gh attestation verify styles.css --repo hwangso595/eink-sync
git clone https://github.com/hwangso595/eink-sync.git
cd eink-sync
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.
Use the document actions in the library view to remove documents from the tablet:
Archive must be outside every Sync folder so archived files are not copied back to the tablet.
| 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.
Notebook and quick-sheet pages are a full 1404×1872 canvas even when you only jotted a few lines at the top. With Trim blank page space on (the default), a page whose content sits within the top half is cropped to just below your writing, so a short quick sheet embeds a short image instead of a tall mostly-blank one. Pages that use more of the sheet, and all PDF-backed pages, are left untouched. Toggle it in Settings → Extraction.
Turn on Search handwriting (OCR) to run local OCR over each notebook page. The recognized text is added under the page image as a callout that is collapsed by default, so it stays out of the way but is still found by Obsidian search:
### Page 3
![[My Notebook_p3.png|500]]
> [!note]- Handwriting (OCR)
> Meeting notes - Project Aurora
> 1. Ship the OCR search feature
> Remember: buy milk and coffee
OCR is off by default and requires Tesseract (see Prerequisites). Recognition quality depends on how neatly the page is written; printed/neat handwriting works well, dense cursive less so. All OCR runs on your machine; no image ever leaves your network. Set the language pack(s) with the OCR language field (e.g. eng, eng+deu).
Notebook pages that use a reMarkable template (ruled lines, grid, planner, …) normally render on plain white, because the template art lives on the tablet at /usr/share/remarkable/templates/ and isn't part of the synced document data. With Render page templates on (the default), the plugin fetches that art from the tablet over SFTP during sync and draws each page's template behind its strokes. Blank pages are unaffected, and until the art has been fetched (or for Syncthing-only setups) pages simply render on white as before. Toggle it in Settings → Extraction.
reMarkable tablet
|
| SFTP (direct transfer) or Syncthing (background sync)
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 is updated automatically. In Syncthing mode, the shared folder configuration is updated too.
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 a highlight note already exists for it, the plugin now clears the managed highlights section (replacing it with "No highlights or annotations found in this document.") while preserving any notes you added outside the managed markers. Brand-new documents with nothing to extract still don't create empty notes.
If you don't see the note update, re-run Extract new highlights (the update happens on the next extraction after the deletion syncs across).
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/) -- Direct SFTP transfer, optional Syncthing configuration, and 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.
E-Ink Sync needs broader access than a typical Obsidian plugin because it reads reMarkable files and communicates with the tablet. These capabilities are limited to the features listed below.
| Capability | Why it's needed |
|---|---|
| Direct filesystem access | Reads raw .rm/.metadata/.content files from the configured sync folder, which may be inside or outside the vault. It also manages the plugin's extraction runtime and writes rendered images/notes to configured vault folders. |
Shell execution (child_process) |
Spawns Python without a shell for highlight extraction (rmscene + PyMuPDF). SSH/SFTP access is limited to the configured reMarkable tablet for setup, sync, archive, delete, and restart operations. Both are essential because there is no JavaScript equivalent for parsing reMarkable's v6 .rm format. |
Dynamic feature check (new Function) |
Comes from the bundled ssh2 dependency, which uses a tiny generated function only to test whether the JavaScript runtime supports BigInt exponentiation. The plugin itself does not evaluate user-provided or downloaded code. |
| Local browser-profile storage | Stores one random installation ID in the local Obsidian browser profile so vault sync does not copy the same device identity to another computer. It does not read the hostname, username, environment variables, or network interfaces, and the ID never leaves the local profile. |
| Vault file enumeration | Used by the "Send document to reMarkable" command to find PDFs/EPUBs in your vault. |
Normal sync and extraction contact only your tablet and the Syncthing service on your own computer. During optional Syncthing setup, the tablet downloads Entware and Syncthing from their upstream repositories. No document data goes to reMarkable Cloud or any third party, and the plugin contains no telemetry or analytics.
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