Getting reMarkable notes into Obsidian automatically
How I wired my reMarkable to Obsidian so handwriting shows up on its own, ready for an assistant to read and file.
I keep an Obsidian vault that runs as an AI-maintained wiki. Articles, highlights, and quick notes land in a raw/ folder, and an assistant turns them into linked wiki pages I can query later. For a while the one thing missing was my own handwriting. I think on a reMarkable, and those pages stayed stuck on the tablet.
This is how I wired the reMarkable into the vault so anything I write in one folder shows up automatically, ready for the assistant to read. No exporting, no copying files around.
If you would rather not build this by hand, skip to the build brief at the end. It is a single block you can paste into your own AI coding agent, and it has enough detail for the agent to build the whole thing without my code.
The idea
The flow goes one direction, reMarkable to Obsidian.
reMarkable "Wiki" folder
| (a job checks it on a schedule)
v
render changed notebooks to page images
|
v
write raw/remarkable/<note>.md + page PNGs into the vault
| (normal Obsidian Sync)
v
every device + the assistant, which transcribes the handwriting into a wiki page
A small job runs on a timer. It looks at one folder on my reMarkable, renders any new or changed notebooks to page images, and drops them into the vault as a markdown file plus PNGs. Obsidian Sync carries them everywhere, and the assistant reads the images when it builds wiki pages.
The design choice that took the most thought: the sync job does not transcribe handwriting. It only captures the image. Transcription happens later, when the assistant reads the page. There are two reasons for that split.
The image is the honest record. Handwriting recognition makes mistakes, and my raw/ folder is immutable by contract, so a bad transcription written there would be permanent. Keeping the rendered image as the source of truth means the text is always regenerable: if a transcription reads wrong, I re-run the assistant against the picture, I never edit the source. The second reason is cost. My vault is already maintained by an assistant that can read images, so transcription adds nothing. The picture is the truth, the text is a view.
What you need
A reMarkable with cloud sync (Connect or the free tier).
An Obsidian vault that syncs to a machine that stays on. I run mine on a small VPS with the vault synced onto it, but a home server or an always-on desktop is fine.
Python 3.11 or newer on that machine.
Code that can read the reMarkable cloud and render
.rmfiles to images. I built on the open-sourceremarkable-mcpproject and thermscenelibrary, which already handle the cloud API and the ink-to-image rendering. Rendering ink needslibcairoon the box.
The capture, step by step
1. Pick one folder as the inbox
I made a top-level folder called Wiki on the reMarkable. A notebook syncs to Obsidian only if it lives in that folder. Moving a note into Wiki is how I say “this one belongs in my knowledge base,” the same gesture as saving an article to a read-later app. Subfolders under it are included too.
This is deliberate. The alternative, syncing every notebook, would fill an immutable source folder with grocery lists and idle sketches. One folder keeps it intentional.
2. Register a dedicated device token
The job authenticates to the reMarkable cloud as its own device, separate from your tablet and any other integration. Get a one-time code from my.remarkable.com/device/desktop/connect and exchange it for a device token once. Store that token in a file with 0600 permissions on the machine. The job reads it at runtime, so the secret never appears in a command line, a unit file, or a log.
A separate device slot matters: re-pairing one client should not revoke the others.
3. Detect what changed
Each run lists the notebooks in the Wiki folder and compares them against a small state file kept on the machine, outside the vault. The state maps each notebook to a content hash built from its page-blob hashes. From that comparison the job sorts every notebook into one of: new, changed, unchanged, renamed, or dropped from the folder.
Only new and changed notebooks get re-rendered. Unchanged ones are skipped, so a run with nothing new does no work and costs nothing. A rename moves the existing files instead of re-rendering. A notebook you pull out of the folder is left in place in the vault, never deleted automatically, because the source folder is append-only by design.
4. Write the bundle
For each new or changed notebook the job renders every page to a PNG and writes two things into the vault:
attachments/remarkable/<slug>/page-001.png, one image per page.raw/remarkable/<slug>.md, a markdown file with frontmatter and one section per page.
The markdown looks like this:
---
source_type: remarkable
remarkable_id: a6cb520e-2611-41ae-...
title: Sprint Planning - 116 - June 01
remarkable_path: /Wiki/Sprint Planning - 116 - June 01
page_count: 1
content_hash: “sha256:7256edcd...”
synced: “2026-06-01T18:53:37Z”
partial: false
---
## Page 1
![[remarkable/sprint-planning-116-june-01/page-001.png]]The writes are atomic: images first, then the markdown via a temp file and a rename, so Obsidian Sync never sees a half-written note. If a page fails to render, the bundle is marked partial: true and the next run retries it, rather than writing a gap that looks finished.
5. Run it on a timer
On Linux I use a systemd timer. The service runs a small wrapper that loads the token and config, then calls the sync once. The wrapper keeps the token out of the unit file:
#!/usr/bin/env bash
set -a
REMARKABLE_VAULT_SYNC_TOKEN=”$(cat /path/to/token.json)”
REMARKABLE_VAULT_ROOT=”/path/to/your/vault”
REMARKABLE_VAULT_SYNC_STATE=”/path/to/state.json”
REMARKABLE_SYNC_FOLDER=”Wiki”
set +a
exec /path/to/venv/bin/remarkable-vault-sync “$@”# remarkable-vault-sync.timer
[Timer]
OnCalendar=hourly
Persistent=true
RandomizedDelaySec=120
[Install]
WantedBy=timers.targetHourly is plenty for notes. Run it once by hand with a dry-run flag first to confirm it sees the folder before you trust the schedule.
Teaching the assistant to read the bundles
The capture job stops at the image. Turning a page into a wiki entry is the assistant’s job, so I gave it one rule in the vault’s operating document (the file the assistant reads before it works). The rule, in plain terms:
When a file appears in
raw/remarkable/, read the page images, transcribe the handwriting, pull in any typed text, and create a source page inwiki/. Record the bundle’scontent_hashon that page. If the hash later changes, re-transcribe. The image is the ground truth: fix a bad transcription by re-reading the image, never by editing the source.
That last line is the whole contract. The source folder holds images and stays immutable. The wiki folder holds the assistant’s transcriptions and is fully regenerable.
Using it day to day
Write on the reMarkable.
When a notebook is worth keeping, move it into the
Wikifolder (or start it there).Within the hour it shows up in Obsidian as a page with the image embedded.
Ask the assistant to ingest new sources. It transcribes the handwriting and files a linked wiki page.
Edits work too. Add pages to a notebook that already synced, and the next run notices the content changed and refreshes the bundle, which flags the wiki page for re-transcription.
A few decisions worth copying
I store the rendered image, not just text. Diagrams, arrows, and crossed-out ideas survive, and I can always re-transcribe from a higher-fidelity source.
I write straight into the synced vault folder rather than pushing through git or an API. The machine that runs the job already has the vault on disk via Obsidian Sync, so writing a file and letting Sync propagate it is the simplest path. One writer, one propagation mechanism, no merge conflicts.
I keep state outside the vault. The state.json that tracks hashes is machine-local. It is not source material, so it does not belong in the synced folder.
Caveats
Current reMarkable firmware writes a notebook format that open-source readers do not fully parse yet. In practice rendering still produces a usable page image, with an occasional warning. Pure-text extraction from ink is unreliable, which is exactly why I lean on a vision-capable assistant for transcription instead of OCR.
If you mostly write typed notes or annotate PDFs, the text comes through directly and you may not need the assistant step at all.
Build it with your own AI agent
If you have an AI coding agent (Claude Code, Cursor, and the like), paste the block below into it. It is self-contained: the agent can build the tool from this alone, without my code. It will ask you for your vault path and how you want to schedule it, then build it test-first.
Build me a small tool that mirrors handwritten reMarkable notes into my Obsidian
vault. You do not have an existing codebase; build it from this spec. Ask me for
my vault path and anything else you need before you start.
Goal: a capture-only command-line job that, on a schedule, copies notebooks from
ONE reMarkable cloud folder into my Obsidian vault as page images plus a markdown
file. It must NOT transcribe handwriting. A separate AI step reads the images
later. The rendered image is the source of truth; the transcription is a
regenerable view.
Stack and building blocks:
- Python 3.11+. No web framework, no database. State is one small JSON file.
- Render reMarkable .rm pages to PNG with the open-source `rmscene` library
(https://github.com/ricklupton/rmscene ; needs libcairo on the machine).
- List documents and download a notebook's blob bundle with the reMarkable sync
v3 cloud API. The open-source `remarkable-mcp` project
(https://github.com/SamMorrowDrums/remarkable-mcp) has a working client you
can reuse or copy: device registration from a one-time code, a cloud client
with get_meta_items() and download(doc), and page renderers.
Configuration via env vars: a reMarkable device token (registered once via
my.remarkable.com/device/desktop/connect), the Obsidian vault root path, a state
file path that lives OUTSIDE the vault, and the folder name to watch (default
"Wiki").
Behavior:
1. List documents under the named top-level reMarkable folder, recursively
(include subfolders). Folders are matched by name; error if the name is
missing or ambiguous at the top level.
2. content_hash for a document = "sha256:" + sha256 over the sorted list of
"fileid=blobhash" pairs from its blob entries. Order independent.
3. State JSON shape: { remarkable_id: {content_hash, filename, title, synced} }.
Classify each in-scope doc against state:
not in state -> new; content_hash differs -> changed; same hash but different
title -> renamed; else unchanged. State ids no longer in the folder -> dropped
(leave their vault files in place, never auto-delete).
4. Filenames: a kebab-case slug of the title; on collision append a short suffix
from the id. Docs already in state keep their recorded filename so a same-title
collision can never overwrite a sibling.
5. For new and changed docs: download the notebook, render each page to a PNG,
extract any typed or PDF text. Write atomically, images first then the markdown
via temp-file-plus-rename so a half-written note is never visible:
- attachments/remarkable/<slug>/page-001.png, one per page
- raw/remarkable/<slug>.md with YAML frontmatter (source_type: remarkable,
remarkable_id, title, remarkable_path, page_count, content_hash, synced,
partial) and a body of "## Page N" sections, each embedding
![[remarkable/<slug>/page-NNN.png]] plus any extracted text.
If a page fails to render, set partial: true, record the failed page numbers,
and store a sentinel content_hash (the real hash plus ":partial") so the next
run retries it instead of treating the gap as finished.
6. Renamed docs: move the existing .md and attachments folder to the new slug and
rewrite the embed paths inside the .md. Do not re-render.
7. Save state. Print a one-line summary: new/changed/unchanged/renamed/partial/
dropped/errors. Exit non-zero if any doc errored. Handle each doc in its own
try/except so one failure does not stop the run.
8. Add a --dry-run flag that classifies and logs planned actions but writes
nothing and downloads nothing (classification needs only metadata).
9. Never put the token in logs, command-line args, or the markdown. Read it from
a 0600 file at runtime.
Deployment: a systemd timer or cron entry that runs it hourly. It must run on a
machine that has the Obsidian vault on disk via Obsidian Sync, so writing files
and letting Sync propagate them is the only write path. No git, no REST API.
Tests (write them first, mock the reMarkable client so no network or libcairo is
needed): slug and collision suffix; the classify state machine across all five
outcomes; atomic write leaves no partial markdown if it fails mid-write; --dry-run
writes nothing; a second run skips unchanged docs, overwrites a changed one, and
moves a renamed one without duplicating.
When the tool works, give me one rule to paste into my vault assistant's
instructions: "When a file appears in raw/remarkable/, read the page images,
transcribe the handwriting, include any typed text, and create a source page in
wiki/. Store the bundle's content_hash on that page; re-transcribe when it
changes. The image is the ground truth: never edit raw/."
Confirm the stack, ask me for my vault path and schedule preference, then build it.
If your agent asks where the reMarkable cloud API or the rendering lives, point it at remarkable-mcp and rmscene. Both are public. The rest of the logic above is the part you actually need to write.
The result is quiet, which is the point. I write on the reMarkable, drop the notebook in one folder, and it becomes part of a searchable, linked knowledge base without me thinking about it.
