<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Khalid Ibne Hasan]]></title><description><![CDATA[I'm Khalid Ibne Hasan — Director of Product & Technical Delivery at Keyin College, St. John's. Writing on EdTech, AI, and the operational glue that makes both useful. Lead product for platforms serving 5,000+ learners.]]></description><link>https://khalidibnehasan.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!MB-e!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F593a78f9-8415-4f37-8389-970a533864a3_200x200.jpeg</url><title>Khalid Ibne Hasan</title><link>https://khalidibnehasan.substack.com</link></image><generator>Substack</generator><lastBuildDate>Thu, 11 Jun 2026 20:44:47 GMT</lastBuildDate><atom:link href="https://khalidibnehasan.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Khalid Ibne Hasan]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[khalidibnehasan@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[khalidibnehasan@substack.com]]></itunes:email><itunes:name><![CDATA[Khalid Ibne Hasan]]></itunes:name></itunes:owner><itunes:author><![CDATA[Khalid Ibne Hasan]]></itunes:author><googleplay:owner><![CDATA[khalidibnehasan@substack.com]]></googleplay:owner><googleplay:email><![CDATA[khalidibnehasan@substack.com]]></googleplay:email><googleplay:author><![CDATA[Khalid Ibne Hasan]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Getting reMarkable notes into Obsidian automatically]]></title><description><![CDATA[How I wired my reMarkable to Obsidian so handwriting shows up on its own, ready for an assistant to read and file.]]></description><link>https://khalidibnehasan.substack.com/p/getting-remarkable-notes-into-obsidian</link><guid isPermaLink="false">https://khalidibnehasan.substack.com/p/getting-remarkable-notes-into-obsidian</guid><dc:creator><![CDATA[Khalid Ibne Hasan]]></dc:creator><pubDate>Mon, 01 Jun 2026 19:32:41 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!MB-e!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F593a78f9-8415-4f37-8389-970a533864a3_200x200.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I keep an Obsidian vault that runs as an AI-maintained wiki. Articles, highlights, and quick notes land in a <code>raw/</code> 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.</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;e63dc494-82a0-4cab-a312-e71f78dcf58f&quot;,&quot;caption&quot;:&quot;Two days ago I wanted one thing: ask Claude about my notes from my phone. By the end I had a working cross-platform setup &#8212; Claude on iOS, Android, web, and desktop, all reading and writing the same Obsidian vault, OAuth-gated by my GitHub identity. The path there was longer than it should have been. This is a writeup of what I tried, what didn&#8217;t work, &#8230;&quot;,&quot;cta&quot;:null,&quot;showBylines&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;How I got my Obsidian vault into Claude.ai on every device &#8212; what I tried, what didn't work, what finally did&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:114673091,&quot;name&quot;:&quot;Khalid Ibne Hasan&quot;,&quot;bio&quot;:&quot;I'm Khalid Ibne Hasan &#8212; Director of Product &amp; Technical Delivery at Keyin College, St. John's. Writing on EdTech, AI, and the operational glue that makes both useful. Lead product for platforms serving 5,000+ learners.&quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/593a78f9-8415-4f37-8389-970a533864a3_200x200.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2026-05-25T18:43:26.464Z&quot;,&quot;cover_image&quot;:null,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://khalidibnehasan.substack.com/p/how-i-got-my-obsidian-vault-into&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:199223612,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:0,&quot;comment_count&quot;:0,&quot;publication_id&quot;:9221363,&quot;publication_name&quot;:&quot;Khalid Ibne Hasan&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!MB-e!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F593a78f9-8415-4f37-8389-970a533864a3_200x200.jpeg&quot;,&quot;belowTheFold&quot;:false,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><p>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.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://khalidibnehasan.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>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.</p><h2><strong>The idea</strong></h2><p>The flow goes one direction, reMarkable to Obsidian.</p><pre><code><code>reMarkable "Wiki" folder
      |  (a job checks it on a schedule)
      v
render changed notebooks to page images
      |
      v
write raw/remarkable/&lt;note&gt;.md + page PNGs into the vault
      |  (normal Obsidian Sync)
      v
every device + the assistant, which transcribes the handwriting into a wiki page
</code></code></pre><p>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.</p><p>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.</p><p>The image is the honest record. Handwriting recognition makes mistakes, and my <code>raw/</code> 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.</p><div><hr></div><h2><strong>What you need</strong></h2><ul><li><p>A reMarkable with cloud sync (Connect or the free tier).</p></li><li><p>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.</p></li><li><p>Python 3.11 or newer on that machine.</p></li><li><p>Code that can read the reMarkable cloud and render <code>.rm</code> files to images. I built on the open-source <code>remarkable-mcp</code> project and the <code>rmscene</code> library, which already handle the cloud API and the ink-to-image rendering. Rendering ink needs <code>libcairo</code> on the box.</p></li></ul><h2><strong>The capture, step by step</strong></h2><h3><strong>1. Pick one folder as the inbox</strong></h3><p>I made a top-level folder called <code>Wiki</code> on the reMarkable. A notebook syncs to Obsidian only if it lives in that folder. Moving a note into <code>Wiki</code> is how I say &#8220;this one belongs in my knowledge base,&#8221; the same gesture as saving an article to a read-later app. Subfolders under it are included too.</p><p>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.</p><h3><strong>2. Register a dedicated device token</strong></h3><p>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 <code>my.remarkable.com/device/desktop/connect</code> and exchange it for a device token once. Store that token in a file with <code>0600</code> 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.</p><p>A separate device slot matters: re-pairing one client should not revoke the others.</p><h3><strong>3. Detect what changed</strong></h3><p>Each run lists the notebooks in the <code>Wiki</code> 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.</p><p>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.</p><h3><strong>4. Write the bundle</strong></h3><p>For each new or changed notebook the job renders every page to a PNG and writes two things into the vault:</p><ul><li><p><code>attachments/remarkable/&lt;slug&gt;/page-001.png</code>, one image per page.</p></li><li><p><code>raw/remarkable/&lt;slug&gt;.md</code>, a markdown file with frontmatter and one section per page.</p></li></ul><p>The markdown looks like this:</p><pre><code>---
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: &#8220;sha256:7256edcd...&#8221;
synced: &#8220;2026-06-01T18:53:37Z&#8221;
partial: false
---

<strong>## Page 1</strong>

![[remarkable/sprint-planning-116-june-01/page-001.png]]</code></pre><p>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 <code>partial: true</code> and the next run retries it, rather than writing a gap that looks finished.</p><h3><strong>5. Run it on a timer</strong></h3><p>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:</p><pre><code>#!/usr/bin/env bash
set -a
REMARKABLE_VAULT_SYNC_TOKEN=&#8221;$(cat /path/to/token.json)&#8221;
REMARKABLE_VAULT_ROOT=&#8221;/path/to/your/vault&#8221;
REMARKABLE_VAULT_SYNC_STATE=&#8221;/path/to/state.json&#8221;
REMARKABLE_SYNC_FOLDER=&#8221;Wiki&#8221;
set +a
exec /path/to/venv/bin/remarkable-vault-sync &#8220;$@&#8221;</code></pre><pre><code># remarkable-vault-sync.timer
[Timer]
OnCalendar=hourly
Persistent=true
RandomizedDelaySec=120

[Install]
WantedBy=timers.target</code></pre><p>Hourly 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.</p><div><hr></div><h2><strong>Teaching the assistant to read the bundles</strong></h2><p>The capture job stops at the image. Turning a page into a wiki entry is the assistant&#8217;s job, so I gave it one rule in the vault&#8217;s operating document (the file the assistant reads before it works). The rule, in plain terms:</p><blockquote><p>When a file appears in <code>raw/remarkable/</code>, read the page images, transcribe the handwriting, pull in any typed text, and create a source page in <code>wiki/</code>. Record the bundle&#8217;s <code>content_hash</code> on 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.</p></blockquote><p>That last line is the whole contract. The source folder holds images and stays immutable. The wiki folder holds the assistant&#8217;s transcriptions and is fully regenerable.</p><h2><strong>Using it day to day</strong></h2><ol><li><p>Write on the reMarkable.</p></li><li><p>When a notebook is worth keeping, move it into the <code>Wiki</code> folder (or start it there).</p></li><li><p>Within the hour it shows up in Obsidian as a page with the image embedded.</p></li><li><p>Ask the assistant to ingest new sources. It transcribes the handwriting and files a linked wiki page.</p></li></ol><p>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.</p><h2><strong>A few decisions worth copying</strong></h2><p>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.</p><p>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.</p><p>I keep state outside the vault. The <code>state.json</code> that tracks hashes is machine-local. It is not source material, so it does not belong in the synced folder.</p><h2><strong>Caveats</strong></h2><p>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.</p><p>If you mostly write typed notes or annotate PDFs, the text comes through directly and you may not need the assistant step at all.</p><div><hr></div><h2><strong>Build it with your own AI agent</strong></h2><p>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.</p><pre><code><code>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 -&gt; new; content_hash differs -&gt; changed; same hash but different
   title -&gt; renamed; else unchanged. State ids no longer in the folder -&gt; 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/&lt;slug&gt;/page-001.png, one per page
   - raw/remarkable/&lt;slug&gt;.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/&lt;slug&gt;/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.
</code></code></pre><p>If your agent asks where the reMarkable cloud API or the rendering lives, point it at <code>remarkable-mcp</code> and <code>rmscene</code>. Both are public. The rest of the logic above is the part you actually need to write.</p><div><hr></div><p>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.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://khalidibnehasan.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[How I built a Chrome extension that actually understands prompt engineering]]></title><description><![CDATA[Every prompt-improver in the Chrome store felt generic. Here's what it took to build one with real techniques, and the workflow that made it doable.]]></description><link>https://khalidibnehasan.substack.com/p/how-i-built-a-chrome-extension-that</link><guid isPermaLink="false">https://khalidibnehasan.substack.com/p/how-i-built-a-chrome-extension-that</guid><dc:creator><![CDATA[Khalid Ibne Hasan]]></dc:creator><pubDate>Thu, 28 May 2026 13:10:40 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!MB-e!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F593a78f9-8415-4f37-8389-970a533864a3_200x200.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I&#8217;m lazy at prompting. I know what good prompts look like. I just don&#8217;t want to write them every time I open ChatGPT. So I went looking for a Chrome extension that would do it for me, and the ones I found were not what I was hoping for.</p><p>You know the kind. You install it, you click the button, your prompt becomes 200 words longer but not actually better. Someone wrapped &#8220;make this better&#8221; around an API call and called it a product. I tried a few of them. They all felt like the same extension with different color schemes.</p><div><hr></div><p>At some point I just asked ChatGPT directly: is it even possible to build one of these that actually works? Even if it&#8217;s a wrapper, something that uses real prompt engineering techniques.</p><p>That conversation went deeper than I expected. We talked about step-back abstraction, where you have the model zoom out before drilling in. Self-refinement, where the model critiques its own output before handing it back. Chain-of-thought, which everyone has heard of and nobody implements properly. And the thing I cared about most: that the same prompt should be structured differently for ChatGPT, Claude, and Gemini, because they reward different things.</p><p>By the end of that chat I had a clear picture of what was missing in the existing extensions. I asked ChatGPT to write me a full PRD. Dropped the PRD into Replit. Replit built a working MVP in one shot.</p><p>The functionality was solid. The prompts coming out the other side were noticeably better than anything I&#8217;d tried. But the landing page looked like a generic Replit app, because it was a generic Replit app.</p><div><hr></div><p>I opened Claude Code next, gave it the codebase, told it to do the landing page properly. What it produced looked nothing like the Replit version. Better in a way that&#8217;s hard to articulate but obvious when you put them side by side. While Claude Code was in the codebase, it offered up two things I hadn&#8217;t thought to ask for: follow-up detection, so the extension can tell when you&#8217;re refining a prompt versus starting a new one, and per-platform optimization that adjusts the improvement strategy depending on which AI you&#8217;re prompting. Both of those ended up being core features. I would not have shipped them on my own.</p><p>My workflow ended up being Replit for hosting and the database, Claude Code for feature work and design, Git as the connective tissue. Push from one, pull from the other. I&#8217;m sure there&#8217;s a better setup. This one has been frictionless enough that I haven&#8217;t gone looking.</p><p>I used the extension myself for a few weeks before doing anything else with it. Every time the improvement came back feeling slightly off, I&#8217;d dig into the pipeline and figure out what was producing the bad output. Most of the fixes involved going back to Claude, researching a technique I&#8217;d half-implemented, and putting in the real version. The output now runs a 3-step pipeline: figure out what the user is actually trying to ask, generate a real improvement, then self-critique and refine. Three model calls, one button click.</p><p>At some point Claude suggested I should monetize it. Fair enough. Stripe integration, free tier where you bring your own API key, Pro tier where the platform key is included. Claude Code handled the whole setup. Webhooks, subscriptions, trial flow, feature gating. It also walked me through the Chrome Web Store submission, which is more bureaucratic than you&#8217;d think the first time through.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://khalidibnehasan.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://khalidibnehasan.substack.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><p>Here&#8217;s the part I keep getting stuck on. The distance from &#8220;I want this thing to exist&#8221; to &#8220;shipped product with Stripe payments&#8221; used to be a real distance. Months of work I didn&#8217;t want to do. Now most of what slowed me down was deciding what I actually wanted. The implementation showed up when I asked for it.</p><p>I don&#8217;t think that&#8217;s a hot take at this point. But it does change what&#8217;s worth sitting on. Ideas you&#8217;ve been mentally filing under &#8220;I would build that if I could&#8221; are probably closer than you think.</p><p>If you want to try it, the extension is at <a href="https://aipromptcopilot.com">aipromptcopilot.com</a>. It works on ChatGPT, Claude, Gemini, and Grok. Free with your own API key, seven-day trial on Pro if you want it with the included platform key. Source is on GitHub: <a href="https://github.com/khalid-hasan/ai-prompt-copilot-extension">github.com/khalid-hasan/ai-prompt-copilot-extension</a>.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://chromewebstore.google.com/detail/cphpcikojbgidakodbacnlidjjkboffn?authuser=0&amp;hl=en&quot;,&quot;text&quot;:&quot;Chrome Extension&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://chromewebstore.google.com/detail/cphpcikojbgidakodbacnlidjjkboffn?authuser=0&amp;hl=en"><span>Chrome Extension</span></a></p><p>If you&#8217;ve built something similar with a better workflow than mine, I want to hear about it.</p>]]></content:encoded></item><item><title><![CDATA[How I got my Obsidian vault into Claude.ai on every device — what I tried, what didn't work, what finally did]]></title><description><![CDATA[Built a cross-platform Claude + Obsidian setup with one synced vault, GitHub OAuth, and notes accessible everywhere.]]></description><link>https://khalidibnehasan.substack.com/p/how-i-got-my-obsidian-vault-into</link><guid isPermaLink="false">https://khalidibnehasan.substack.com/p/how-i-got-my-obsidian-vault-into</guid><dc:creator><![CDATA[Khalid Ibne Hasan]]></dc:creator><pubDate>Mon, 25 May 2026 18:43:26 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!MB-e!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F593a78f9-8415-4f37-8389-970a533864a3_200x200.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Two days ago I wanted one thing: ask Claude about my notes from my phone. By the end I had a working cross-platform setup &#8212; Claude on iOS, Android, web, and desktop, all reading and writing the same Obsidian vault, OAuth-gated by my GitHub identity. The path there was longer than it should have been. This is a writeup of what I tried, what didn&#8217;t work, and what finally did, in case anyone else is heading down the same path.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://khalidibnehasan.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2><strong>The goal</strong></h2><p>A personal AI wiki. Vault on my phone and desktop (Obsidian Sync), Claude can query and maintain it from any device. Specifically:</p><ul><li><p>The vault should be the same vault I already use &#8212; not a copy, not a fork</p></li><li><p>Mobile Claude must work, not just desktop</p></li><li><p>OAuth-gated; only I can authenticate</p></li><li><p>Cheap (under &#8364;10/month on top of what I already pay for Obsidian Sync)</p></li><li><p>Reliable enough that I&#8217;d trust it with two years of notes</p></li></ul><h2><strong>What I tried first</strong></h2><p>The most prominent existing project for this was <code>benpeter/obsidian-cloud-mcp</code> &#8212; a Terraform-driven deployment of an Obsidian MCP server on Hetzner with a Cloudflare Worker doing OAuth. It got me far enough to deploy a VPS, configure the Worker, and set up the GitHub OAuth flow, which gave me a solid mental model of the moving parts.</p><p>What I hit was an architecture-vs-dependency mismatch. The repo configures the underlying MCP server (<code>cyanheads/obsidian-mcp-server</code>) with <code>MCP_AUTH_MODE=introspection</code>, but the cyanheads config schema only accepts <code>jwt</code> or <code>oauth</code>. The result: the MCP container crashed on startup with:</p><pre><code><code>Error: Invalid environment configuration.
{"MCP_AUTH_MODE":["Invalid enum value. Expected 'jwt' | 'oauth', received 'introspection'"]}
</code></code></pre><p>So the token-introspection layer the design describes isn&#8217;t backed by code in any cyanheads commit I could find &#8212; looks like an architectural plan that didn&#8217;t get fully wired up end-to-end before the repo was published. Not malicious, just incomplete. The Terraform and Cloudflare Worker pieces of benpeter are genuinely useful as scaffolding (I ended up forking and using them), but the OAuth-to-introspection path needed a different approach.</p><p>A bunch of &#8220;let me just patch this one thing&#8221; attempts later, I stepped back to rethink the design.</p><h2><strong>The pivot</strong></h2><p>I ran a bunch of research in parallel &#8212; what&#8217;s the MCP server landscape actually look like in May 2026, what&#8217;s the canonical way to put OAuth in front of a remote MCP server, what are people who&#8217;ve successfully deployed this using.</p><p>Two findings unlocked everything:</p><ol><li><p><strong>Cloudflare shipped &#8220;Managed OAuth for Access&#8221; in 2025.</strong> It turns Cloudflare Access itself into an RFC-compliant OAuth 2.1 authorization server for any HTTP service behind it. This didn&#8217;t exist when most existing writeups were drafted, and it eliminates most of the custom OAuth proxy code I&#8217;d been trying to debug. Free up to 50 users.</p></li><li><p><strong>There&#8217;s a purpose-built headless Obsidian MCP server I&#8217;d missed:</strong> <code>nweii/obsidian-remote-mcp</code>. Six weeks old, 2 stars at the time, but ~150 lines of audit-able OAuth code, standards-correct (PKCE, RFC 8414, RFC 9728), and built to run <em>without</em> the Obsidian desktop app. Just reads markdown off disk. Claude&#8217;s callback URI is the default redirect.</p></li></ol><p>Combined with <code>obsidian-headless</code> &#8212; Obsidian&#8217;s official Node CLI for headless Sync, shipped in Feb 2026 &#8212; the stack basically writes itself.</p><h2><strong>What I ended up building</strong></h2><pre><code><code>Claude.ai (desktop / web / mobile)
   &#9474;  OAuth (Cloudflare Access Managed OAuth, GitHub IdP, email allowlist)
   &#9660;
Cloudflare Access
   &#9474;
   &#9660;  Cloudflare Tunnel (no public IP on the VPS)
   &#9660;
Hetzner cx23 VPS (&#8364;4/mo)
   &#9500;&#9472;&#9472; nweii/obsidian-remote-mcp (Docker, :3456 localhost-only)
   &#9500;&#9472;&#9472; obsidian-headless (systemd service: ob sync --continuous)
   &#9492;&#9472;&#9472; 5-min cron: git mirror to a private backup repo
</code></code></pre><p>Total new spend: <strong>~&#8364;4/month</strong> on top of my existing Obsidian Sync. The Cloudflare side is free tier.</p><h2><strong>The unexpected trick</strong></h2><p>Most &#8220;Cloudflare Access + MCP&#8221; writeups protect the <strong>whole hostname</strong> with Access. That&#8217;s the obvious move. It breaks.</p><p>Why: Claude.ai&#8217;s MCP connector OAuth flow involves multiple endpoints with different requirements:</p><ul><li><p><code>/.well-known/oauth-protected-resource</code> &#8212; must be <strong>publicly readable</strong> (MCP spec)</p></li><li><p><code>/authorize</code> &#8212; needs a <strong>browser</strong> to complete user auth</p></li><li><p><code>/oauth/token</code> &#8212; called from Claude.ai&#8217;s <strong>backend</strong>, no browser context, no Access cookies</p></li><li><p><code>/mcp</code> &#8212; carries the <strong>MCP-issued bearer</strong>, not an Access JWT</p></li></ul><p>Putting Access on the whole hostname blocks the public discovery endpoint AND blocks Claude.ai&#8217;s backend from token exchange. The OAuth flow stalls.</p><p>So I protected <strong>only </strong><code>/authorize</code> with Cloudflare Access. Everything else stays open at the Cloudflare edge, but <code>/mcp</code> is still gated by nweii&#8217;s own opaque-token auth. The split:</p><p><strong>PathGated byWhy</strong><code>/.well-known/*</code>nothingMCP spec requires public discovery<code>/authorize</code><strong>Cloudflare Access</strong>Where user identity is established (GitHub + email allowlist)<code>/oauth/token</code>nothingBackend-to-backend code exchange; nweii validates the code single-use<code>/mcp</code>nweii&#8217;s opaque bearerToken issued only after the Access-gated <code>/authorize</code> succeeds</p><p>Two layers, zero custom code, clean separation: <strong>Access = who are you, nweii = here&#8217;s your session.</strong></p><p>I haven&#8217;t seen this written up anywhere else. Most guides do all-or-nothing.</p><h2><strong>What&#8217;s novel vs. what&#8217;s not</strong></h2><p>Honest take:</p><ul><li><p><strong>The overall pattern</strong> (Tunnel + Access + MCP server for Obsidian) &#8212; not novel. Several people have done variants. <code>jimprosser/obsidian-web-mcp</code> is the closest comparable. JonHollander has an all-Cloudflare variant using Containers + R2. Cloudflare&#8217;s own docs publish this as a reference architecture now.</p></li><li><p><strong>The specific component combination</strong> (<code>nweii</code> + <code>obsidian-headless</code> + CF Access Managed OAuth + path-specific protection) &#8212; almost certainly a one-of-one. The pieces are all from the last 12 months; nweii has tens of users at most.</p></li><li><p><strong>The path-specific Access protection trick</strong> &#8212; appears to be the actually novel contribution.</p></li><li><p><strong>The git-mirror safety net</strong> for <code>obsidian-headless</code> &#8212; borrowed from rolle.design (2024). Not novel but worth knowing about because <code>obsidian-headless</code> has several open data-loss bugs at time of writing (#19, #21, #25, #27) and you genuinely want a backup ready.</p></li></ul><h2><strong>What I wrote up</strong></h2><p>Full deployment guide at: <strong><a href="https://github.com/khalid-hasan/obsidian-mcp-cloudflare">https://github.com/khalid-hasan/obsidian-mcp-cloudflare</a></strong></p><p>It&#8217;s not a tool; it&#8217;s a deployment guide. Terraform for the Hetzner VPS, cloud-init for the base image, docker-compose for nweii, systemd unit for <code>ob sync --continuous</code>, a script that wires up the Cloudflare Tunnel + Access via API. MIT licensed. Honest about the tradeoffs (which underlying components have open bugs, when this is the wrong choice vs alternatives, what the threat model does and doesn&#8217;t cover).</p><p>Use it if you&#8217;re already paying for Obsidian Sync and want vault changes from your phone to propagate to a server in real time. Use <code>jimprosser/obsidian-web-mcp</code> if you want a more polished single-package solution. Use Cloudflare&#8217;s <a href="https://lobehub.com/mcp/jonhollander-obsidian-mcp-cloudflare">Containers + R2 reference</a> if you&#8217;d rather not run a VPS at all.</p><h2><strong>Takeaways for anyone going down this path</strong></h2><ol><li><p><strong>Check that your dependencies actually implement the features your design depends on.</strong> A four-hour debugging session would have been a five-minute <code>grep</code> of the underlying server&#8217;s source if I&#8217;d been more skeptical of the README&#8217;s claims.</p></li><li><p><strong>Cloudflare Access Managed OAuth changes the math</strong> for anyone putting auth in front of a remote MCP server. If your design was sketched in 2024, revisit it.</p></li><li><p><strong>Path-specific Access protection</strong> is the missing trick most writeups don&#8217;t mention. Whole-hostname Access doesn&#8217;t compose with MCP&#8217;s OAuth flow.</p></li><li><p><code>obsidian-headless</code><strong> is new and has open data-loss bugs.</strong> Pair it with a git mirror. Five minutes of cron config saves you from a vault-wide deletion event.</p></li><li><p><strong>Audit auth code in young repos before deploying.</strong> <code>nweii/obsidian-remote-mcp</code> was six weeks old when I deployed it. Reading 290 lines of TypeScript took 15 minutes and gave me real confidence (or would have caught real bugs).</p></li></ol><p>Built this with <a href="https://claude.com/claude-code">Claude Code</a> doing most of the typing.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://khalidibnehasan.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>