Skip to content

Latest commit

 

History

14 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Commonplace

Instagram reels are my single largest source on harness engineering. Seventeen notes, more than YouTube, X, LinkedIn and the open web combined on that subject.

I only know that because everything I read now lands in one library, whatever channel it arrived through. Before that, those seventeen reels were seventeen reels I watched.

That is the problem this solves, and it is not the one I expected to be solving.

A commonplace book is what people called this before the internet: one book you copied everything worth keeping into, whatever you had been reading. It worked because there was only ever one book. We have since split our reading across a dozen apps and kept a separate head for each one.

The thinking gets trapped in the channel it arrived in

We consume across the whole multiverse now. A reel, a Substack essay, an X thread, a YouTube talk, a Reddit comment, a carousel of slides someone screenshotted from a deck. But almost nobody moves the thinking between them. Instagram thinking stays in the Instagram universe and gets applied back to Instagram. The essay you read on Tuesday never meets the reel you watched on Thursday, even when they are the same argument, because they live in different apps, different postures, and different parts of your head.

So you end up with a subject you have been circling for months from six directions, and no single channel has enough of it to show you the shape.

This collapses the channels into one library. Share anything from anywhere, and it comes back as a written assessment filed alongside everything else you have read, tagged by subject rather than by where it came from.

  Share Sheet, from any app; a hotkey, from Chrome
        |
        v
  one Apple Note  ->  a sweep, four times a day
        |
        v
  read it properly  ->  assess  ->  tag by subject  ->  file  ->  tell you if it mattered

Once the channels collapse, subjects start assembling themselves out of sources that would never otherwise have met.

What that looks like after 579 items

Real numbers from my own library, at the time of writing.

Subject Notes Channels it came from
context-governance 34 Instagram reels, LinkedIn, X, YouTube, carousels, newsletters, web
harness-engineering 31 Instagram reels (17 of them), YouTube, X, LinkedIn, carousels, web
ai-agents 32 web, Instagram reels, X, YouTube, carousels, newsletters
ai-finance 24 web and newsletters only

Twenty-eight of my subjects draw on three or more different channels. That last row is the useful contrast: ai-finance genuinely lives in one universe, so its spread is narrow, which is how you can tell the other numbers are measuring something real.

None of those subjects was declared in advance. See the theme engine below.

It tells you when it could not read something

Traversing channels only works if you can trust every entry equally, and that is the part where most tooling quietly breaks.

Every AI reading tool will hand you a summary of an article it could not open. It will be fluent, roughly the right shape, assembled from a headline and a preview, and indistinguishable from a summary of something genuinely read. Do that once a week for six months and your library is a mix of things you know and things you only think you know, with no marker between them.

Nothing here is ever assessed from content that was not actually read. Where a platform needs a logged-in session, the system either has one or says so. X Articles are read in headless Chromium with your own exported X session. Threads posts still have no headless path at all, so they get flagged, stay visible in the inbox, and get cleared by hand later. Nothing is ever summarised from a preview.

A stretch of the inbox note, showing read items alongside flagged ones

That is a real stretch of my own inbox, captured before the X Article reader existed, so two of the flagged lines in it would be read rather than flagged today. Most lines are [x], meaning read and filed. The [!] lines are items the system could not read: two X posts and a Claude artifact link. They stay visible until I clear them by hand, and none of them has an assessment sitting in my library pretending otherwise. Two account-linked tokens are redacted, for reasons the repo will make obvious.

examples/03-what-a-failure-looks-like.md shows exactly what that looks like, end to end. It is the example I would read first.

Which means it has to actually read hard things

The fetch ladder is where most of the work went, because a channel you cannot read is a channel that stays trapped.

Source How it gets read
Articles, blogs, newsletters Your agent's web fetch
YouTube Captions first, local Whisper transcription if there are none
Instagram reels and stories yt-dlp with your cookies, then Whisper
Silent reels, where the words are burned into the video Frame OCR, one segment per distinct on-screen text state
Instagram image carousels (the listicle slide decks) gallery-dl plus tesseract, one block per slide
X / Twitter posts Public syndication endpoint, no login needed
X Articles (their long-form format) Headless Chromium with your exported X session
PDFs Your agent's web fetch, same as any other document
Reddit Public JSON, thread plus top comments
LinkedIn Jina Reader
TikTok, Vimeo yt-dlp, then Whisper

The silent-reel path is the one that changed the numbers. A reel with no voiceover, where the substance is typed onto the screen, used to come back empty and get marked failed. On Instagram that is the common case rather than the exception, and fixing it is a large part of why seventeen of my harness-engineering notes came from there.

The theme engine

There is no taxonomy file. Nothing is pre-registered, and that is deliberate.

The categories you can name on day one are the ones you already know you care about. What a fixed taxonomy cannot do is notice that you have quietly read eleven things about a subject that was never on the list, which is exactly the subject worth surfacing. It is also the subject most likely to be spread across four channels, because if it were concentrated in one you would have noticed already.

So the assessor names each item's subject in its own words, and a ledger counts every tag it has ever seen. On the next run it hands that ledger back as a soft prior: these are the subjects this library already carries, reuse one if it fits, coin a new one freely if none does. Tags converge on their own, and a subject the library has never held can still arrive on any given Tuesday.

When one recurs often enough with nowhere of its own to live, the ledger says so:

$ ./scripts/themes.py suggest

These subjects have recurred at least 4 times and still file
into the library root. Give one a home if you want it kept separately:

  wine (6 items, 2026-05-02 to 2026-07-28)
      vinepair-sonoma-coast-pinot-2026-06-11.md
      punch-willamette-producers-2026-07-03.md
      themes.py home wine <dir>

You answer in one line, or you ignore it and everything keeps working:

$ ./scripts/themes.py home wine ~/reading/wine
OK: items tagged 'wine' now file into /Users/you/reading/wine

That is the only moment a human is in the loop, and the system reached the question by watching six months of actual reading rather than asking you to predict it on day one.

themes.py rename old new merges a tag that fragmented, which you will need. My own ledger carries gtm-marketing at 25 alongside marketing-gtm at 12. The ledger is plain JSON at <library>/.themes.json, so read it, edit it, or delete it and lose nothing but the counts.

What it produces

Three real notes from my library are in examples/, reformatted to the generic assessor and with my personal context stripped out.

Across those 579 notes the verdicts run roughly 253 "worth reading" against 200 "skim" or "skip". Something like 44% of what I shared to myself, having already passed my own interest filter at the moment I shared it, was not worth my time.

That ratio only holds because the assessor is willing to say "skip" about something you were excited enough to share ninety seconds earlier. One that finds every item valuable is one you will stop reading inside a fortnight.

Setup

You need macOS (Apple Notes and the Shortcuts CLI), Python 3.9 or later, and an agent that can run shell commands and fetch web pages. I run it with Claude Code; the run procedure is a plain markdown file, so anything that can follow instructions and use a terminal will do.

1. Clone it and make the config.

git clone https://github.com/<you>/commonplace.git
cd commonplace
python3 scripts/config.py init

Open config.json and set library to where you want your notes. That is the only required edit. Then:

python3 scripts/config.py check

which validates the file and creates the directories.

2. Make the Apple Note. In Apple Notes, create a note called exactly Commonplace, matching note_name in your config. Leave it empty. Call it something else if you like, as long as the two agree.

3. Install the Share Sheet shortcut. Open shortcut/ and either import the signed file or build it in the Shortcuts app in four steps. Both paths are in shortcut/README.md.

4. Install the Chrome extension, if you read in a desktop browser. Load chrome/ unpacked at chrome://extensions, run ./chrome/install.sh, and fully quit and reopen Chrome. A hotkey, the toolbar button, and a right-click menu then queue the current page (or a highlighted URL) into the same note. chrome/README.md has the detail, including why the native host lives outside the repo.

5. Install the readers you want. For articles and X posts, nothing extra is needed. For video and Instagram:

brew install yt-dlp ffmpeg tesseract
pipx install whisper-ctranslate2
./scripts/setup_env.sh          # gallery-dl + pytesseract, for image carousels

For X Articles, which need a real browser:

pipx install playwright && playwright install chromium

fetch_x_article.py re-execs itself under a Playwright-carrying interpreter when the one calling it has none, so an isolated install still works from the sweep.

6. Give Instagram your cookies, if you share from Instagram. It rejects logged-out requests, so export a Netscape-format cookies.txt from a logged-in browser session and save it at ~/.config/commonplace/cookies/instagram.txt. docs/COOKIES.md covers the export, what makes a file go stale, and how the system tells you it has.

7. Point your agent at the run procedure. In Claude Code, copy the slash command:

cp claude/commands/process-inbox.md ~/.claude/commands/

then edit the one path inside it to point at your clone. /process-inbox now runs a sweep. Any other agent: hand it prompt/TRIAGE.md and let it work.

8. Schedule it, once you trust it. I run four sweeps a day. Start by running it by hand for a week, because the first week is when you find out that your Instagram cookies are wrong or that your tags are too fine-grained.

Living with it

Almost all of it is silent by design. The run log holds the complete record of every sweep; notifications are capped (four per run by default) and only fire for something genuinely worth reading, or for a failure you might want to fix. A sweep that reads eleven things and notifies you eleven times has moved the queue from your inbox to your lock screen, which is not an improvement.

Expect the tags to be messy for the first month and then to settle. Expect to run themes.py rename a few times early on. Expect roughly one item in ten to fail the first time you set up cookies, and close to none once they are right.

The honest limitation: this reads what you already chose to share. It is not discovery, and it does not decide what you should care about. What it removes is the tax between deciding something looks interesting and knowing whether it was, and the wall between the channel it arrived in and everything else you know.

Repository map

config.example.json     Copy to config.json. The only file you must edit.
examples/               Three real notes, plus what a failure looks like.
prompt/
  TRIAGE.md             The run procedure. The agent reads this fresh each sweep.
  ASSESS.md             How to read one item, and the note format it produces.
scripts/
  config.py             Config loader. Every other script gets its paths here.
  inbox.py              The Apple Notes bridge: add, read, done, fail, recover,
                        unlock, plus dedupe/prune/sanitize for note health.
  themes.py             The theme engine: record, prompt, route, suggest, home.
  log.py                Run-log writer.
  notify.py             Capped notifications.
  cookies.py            Resolves and validates cookies for auth-walled hosts.
  fetch_*.py            The fetch ladder, one script per awkward platform.
                        fetch_x_article.py needs Playwright and x.com cookies.
  transcribe.sh         yt-dlp plus local Whisper.
  setup_env.sh          Builds the OCR virtualenv.
shortcut/
  src/add-to-inbox.plist  The Share Sheet shortcut as readable source.
  build.sh                Converts and signs it.
  README.md               Import it, or build it by hand in four steps.
chrome/
  manifest.json           The desktop capture end: a hotkey, the toolbar
  background.js           button, or a right-click sends the page to the note.
  native_host.py          The bridge Chrome spawns; it calls inbox.py add.
  install.sh              Copies the host out of the repo and registers it.
  README.md               Install in three steps, and what the badge means.
docs/                   Cookies, the fetch ladder, untrusted input, note repair, extending it.
  images/               Screenshots used in this README.
claude/commands/        The /process-inbox slash command for Claude Code.

Design notes worth knowing before you change anything

The sweep takes a lock. Scheduled sweeps fire together when a laptop reopens after being closed. Without the lock they each read the same unchecked items and each file a separate note for the same URL. The lock makes one win and the rest no-op, and it expires after 30 minutes so a crashed sweep never blocks the next one permanently.

Every write to the Apple Note goes through inbox.py. It edits only the checkbox on the lines you name and preserves everything else, and item ids are a hash of the line text, so marking is idempotent and race-free.

Commands are deliberately long and flat. Absolute paths everywhere, no cd, no loops, no $(...) substitution. Agent permission allowlists match on a command's leading path, so a shortened command is one that stalls an unattended 4am run waiting for an approval nobody is awake to give. log.py exists at all because printf "$(date ...)" cannot be statically verified.

A shared URL sometimes carries a live credential. A gist link with an embedded token, a signed S3 URL, a calendar invite, a password reset. The system never uses a credential it finds in a URL: the parameter is stripped before the URL goes anywhere, the fetch runs without it, and a failure flags the item. I found an unexpired third-party token in my own note eight months in, which is what prompted the rule. docs/REPAIR.md has the detail, including why Apple Notes' malformed &amp entity made the first fix fail silently.

Every fetched payload is scanned for text aimed at the agent before it is assessed. The sweep ingests untrusted foreign content headless, on a schedule, with nobody watching, which is exactly the setup prompt injection wants. So the scan is a mandatory step in the prompt rather than something the agent is trusted to remember: instructions addressed to an AI, claimed pre-authorization, requests to run commands or touch files, and hidden text surfaced by OCR all quarantine the item as flagged rather than assessed, and every item's log line carries a [scan: clean] or [scan: FLAGGED] marker so you can verify the scan actually ran. The pattern is borrowed from the steal skill in matteotitta/genesys-skills. docs/UNTRUSTED.md has the definitions, including the carve-out that keeps a library full of prompt tutorials from flagging itself.

The note can duplicate itself, and there are commands for it. Every mark rewrites the whole note body, and Apple Notes syncs that note across devices, so a sync conflict mid-write can be resolved by concatenation. My own note reached 747 lines holding 213 distinct URLs before I noticed. dedupe collapses it, prune keeps it small enough that it stops happening, and every whole-note write now reads back to check. Nothing is ever re-processed when this happens, because marks are idempotent, but it compounds quietly.

An item has three states rather than two. [ ] is unread, [x] is done, and [!] is flagged. That third state carries the difference between "this broke" and "this needs a logged-in browser", and keeping them apart is what stops a known blind spot from reading like a bug every single day.

License

MIT. See LICENSE.

About

Share a link from any app; get back a written assessment filed in one library. A commonplace book for the age of twelve feeds, that tells you when it could not read something.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages