Skip to main content
Glama
marioallaa
by marioallaa
README.md
<div align="center">

<img src="assets/logo.png" width="104" alt="">

# rmprint

**Your reMarkable, from the terminal and from Claude.**

A virtual printer, a CLI, and an MCP server. One required dependency.
Windows, macOS and Linux.

</div>

---

```bash
git clone https://github.com/marioallaa/rmprint
cd rmprint
python install.py          # Windows: double-click install.bat
```

That's it. A window opens and walks you through four steps: connect your
account, add the printer, keep it running, hook it up to Claude.

<div align="center"><br>

**Then just press Ctrl+P and choose `reMarkable`.**

</div>

And with Claude:

> **You:** what did I write in my notes about the supplier options?
> **Claude:** *reads the handwritten pages* — You compared three: Northwind…

## What it does

**Sends.** PDFs and EPUBs, into any folder. Or typeset text straight from
stdin — no browser, no LaTeX, no fonts to install.

**Prints.** `rmprint printer install` adds a real system printer. Ctrl+P from
any application, pick **reMarkable**, and it lands on the tablet with its
proper document name.

**Reads.** Lists your library, pulls text out of PDFs and EPUBs, and renders
handwritten pages as images — including annotations drawn over the original.

**Talks to Claude.** Seven MCP tools, plus two Claude Skills that teach it how
to write for a tablet and how to read handwriting properly.

What it deliberately cannot do: rename, move, delete, or create folders. It
never writes to the sync tree, so the worst a bug in here can do is fail.

## Install

Needs Python 3.10 or newer. Nothing else, and no administrator rights.

| | |
|---|---|
| **Windows** | Double-click `install.bat` |
| **macOS / Linux** | `./install.sh` |
| **Any** | `python install.py` |

The installer adds rmprint to your applications menu, sets it to start when
you sign in, and opens the window. Open it again any time from the menu, or
with `rmprint`.

<div align="center">
<img src="assets/screenshot.png" width="440" alt="The rmprint window">
</div>

To undo everything: `python install.py --uninstall`.

**Prefer the terminal?** `pip install ".[notes]"` and use the commands below —
`pipx` and `uv tool install` work too. Drop `[notes]` if you only want to
*send* documents; that extra is what enables *reading* them back.

### Connect your account

The window's step 1 does this, but by hand it's:

1. Open <https://my.remarkable.com/device/desktop/connect> and sign in.
2. Copy the eight-character code. It expires in a few minutes.
3. `rmprint pair <code>`

Already use [rmapi](https://github.com/ddvk/rmapi)? rmprint reads its token
from `~/.rmapi`, so you can skip pairing entirely. On Windows it will also use
the reMarkable desktop app's login if that's signed in.

## The printer

The window's step 2 adds it. By hand:

```bash
rmprint printer install --folder Reading
```

Then print to **reMarkable** from any application.

An agent runs in the background to upload what you print. It starts when you
sign in, restarts itself if anything goes wrong, and logs to
`agent.log` next to your config. You can start it by hand with
`rmprint agent`, and see whether it's up in the window or with
`rmprint printer status`.

**On Windows** no driver is installed. Windows already ships *Microsoft Print
To PDF*, and the Local Port monitor accepts a *file path as the port name* —
point the driver at one and it renders straight to that file with no Save As
dialog. A watcher then recovers the document title from the print spooler and
uploads. No administrator rights needed.

**On macOS and Linux** CUPS is already a PDF pipeline, so the printer is a
queue plus a small backend that pipes each job to `rmprint send`. Installing a
backend writes into `/usr/lib/cups/backend`, so:

```bash
sudo rmprint printer install --folder Reading
```

No watcher is needed there — CUPS hands the job over directly.

> The Windows path is tested on real hardware. The CUPS backend follows the
> documented contract but has not been tested by the author. Reports welcome.

## Commands

```
rmprint                        open the window (same as `rmprint gui`)

rmprint pair <code>            connect to a reMarkable account
rmprint status                 check the connection
rmprint folders                list folders

rmprint send FILE              send a PDF or EPUB
rmprint text [TEXT]            typeset text (or stdin) and send it

rmprint ls [FOLDER]            list documents
rmprint read DOC               print a document's text
rmprint page DOC               render a page as PNG, handwriting included
rmprint pull DOC               download the original PDF or EPUB

rmprint printer install|remove|status|watch
rmprint agent                  the always-on uploader (starts at login)
rmprint mcp                    run the MCP server over stdio
rmprint install-claude         register the MCP server with Claude Desktop
```

`DOC` can be an id, an exact title, or any distinctive fragment of one.

```bash
rmprint send contract.pdf -f Clients -t "Acme — MSA v3"
cat notes.md | rmprint text --title "Meeting notes" -f Work
rmprint text --title "Preview" -o out.pdf < notes.md    # local, don't send
rmprint read "board pack" | head -40
rmprint page "board pack" --page 3 --width 1800
```

## Using it with Claude

```bash
rmprint install-claude          # --dry-run to see the change first
```

Restart Claude Desktop. Any existing config is backed up alongside the
original before it's touched.

By hand, or for another MCP client:

```json
{
  "mcpServers": {
    "rmprint": { "command": "python", "args": ["-m", "rmprint", "mcp"] }
  }
}
```

| | |
|---|---|
| Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Linux | `~/.config/Claude/claude_desktop_config.json` |

### The tools

| Tool | Does |
|---|---|
| `status` | Is this connected, and to which account. |
| `list_folders` | Where a document could go. |
| `send_text` | Typeset text and send it. |
| `send_document` | Send a PDF or EPUB from disk. |
| `list_documents` | What's on the tablet, with page counts. |
| `read_document` | The text layer of a PDF, EPUB, or typed notebook text. |
| `get_page_image` | A page as an image — handwriting and annotations. |

Handwriting is never transcribed by rmprint. It renders the page and lets
Claude read it, which works far better than any OCR that could ship in here.

### The Skills

Two Claude Skills ship in [`skills/`](skills):

- **[remarkable-notes](skills/remarkable-notes/SKILL.md)** — how to write for a
  device that gets read away from a screen and annotated by hand, and how to
  file things. It also tells Claude to *remember how you like it done*: which
  folder work reading goes in, how you title client documents, whether you want
  a summary before the bullets.
- **[remarkable-reading](skills/remarkable-reading/SKILL.md)** — how to read
  handwriting properly: page by page, transcribe before summarising, say what
  you can't make out, and preserve the structure that boxes and arrows carry.

**Install the skills** in the window (step 4) copies both into
`~/.claude/skills/`. Or copy the directories there yourself.

## How it works

There is no public reMarkable API. This was worked out by watching the official
desktop app.

**Pairing.** A one-time code is traded for a permanent device token:

```
POST webapp.cloud.remarkable.com/token/json/2/device/new
{"code": "...", "deviceDesc": "desktop-windows", "deviceID": "<uuid>"}
```

**Sessions.** The device token buys a user token good for about three hours.
Its `tectonic` claim is your storage region (`eu`, `us`, …), which picks the
sync host `<region>.tectonic.remarkable.com`.

**Uploading.** The document goes up whole, with title and destination in a
header; the cloud unpacks it into the sync tree itself:

```
POST internal.cloud.remarkable.com/doc/v2/files
Content-Type: application/pdf
rm-meta: base64({"file_name": "...", "parent": "<folder uuid>"})
```

**Reading** is a read-only walk of `/sync/v3/`. Two details there cost the most
time, so to save you the trouble:

- Every blob request needs an **`rm-filename` header whose extension the server
  validates** — `root.docSchema` for the root index, `<uuid>.docSchema` for a
  document index, `<uuid>.metadata` for metadata. Anything else returns
  `400 unexpected 'rm-filename' http header`, which is *also* what you get when
  the header is missing entirely, so the message misleads.
- That header is matched **case-sensitively**. Python's `urllib` title-cases
  header names and sends `Rm-Filename`, which is rejected — hence `http.client`
  for those calls.

Index blobs are plain text: a schema line `3`, then
`<sha256>:<type>:<name>:<subfiles>:<size>` per entry, where type `80000000`
marks a document index and `0` a plain file.

**Handwriting** is `.rm` v6, parsed by [rmscene](https://github.com/ricklupton/rmscene)
and rendered to SVG by [rmc](https://github.com/ricklupton/rmc), then rasterised
with PyMuPDF and composited over the original page.

## Where things are kept

One file, holding one device token:

| | |
|---|---|
| Windows | `%APPDATA%\rmprint\config.json` |
| macOS | `~/Library/Application Support/rmprint/config.json` |
| Linux | `~/.config/rmprint/config.json` |

`chmod 600` on POSIX. Set `RMPRINT_HOME` to move it. Delete it to disconnect;
there is nothing else to clean up.

## Development

```bash
pip install -e ".[dev,notes]"
pytest
```

The suite covers the PDF writer — including that the xref offsets actually
point at their objects, which is the classic way to emit a file readers reject
— and the pure helpers. Nothing in it touches the network.

## Notes

Undocumented endpoints, so reMarkable can break this whenever they like. Your
documents go to reMarkable's cloud and nowhere else: no server in the middle,
no telemetry.

Related work worth knowing: [rmapi](https://github.com/ddvk/rmapi) (a
full-featured Go CLI), [rmfakecloud](https://github.com/ddvk/rmfakecloud) (host
the cloud yourself), and [awesome-reMarkable](https://github.com/reHackable/awesome-reMarkable).

Not affiliated with reMarkable AS.

## Licence

MIT — see [LICENSE](LICENSE).