shelf-save
Turn raw URLs into a searchable tool library of markdown notes.
Tools are named by what they do, with the Claude Code name in brackets. Use
whatever your harness calls the equivalent.
Step 0 — find the library (always do this first)
Read ~/.tool-library-path (in the user's home directory). It holds one line:
the library folder. Call it <LIB> below.
If it exists, use that path.
If it's missing, this is a first run:
- Ask: "Where should your tool library live? Give me a folder path — an
Obsidian vault subfolder works, or any normal folder."
- Write their answer as a single line to
~/.tool-library-path.
Use forward slashes even on Windows.
- Create this structure under it, then tell the user the full path you
created and what goes where — they'll need to find it without you.
<LIB>/
├── inbox.md # user dumps URLs and pasted text here
├── upload_documents/ # user drops PDFs, docs, screenshots here
├── _processed.md # append-only archive; dedupe source of truth
├── _index.md # auto-generated, grouped by category
└── notes/ # one file per tool
_processed.md starts with a | date | source | table header. inbox.md
starts with this comment, verbatim:
<!--
Dump here. No formatting, no thinking.
URLs one per line
Articles paste the text, as messy as you like
Documents drop the file in the upload_documents/ folder next to this file
Then say "process my inbox".
Paste the text whenever a link won't load — paywalls, logins, newsletter
tracking redirects. It makes a better note than fetching would.
Everything that works becomes a note and leaves this file. Anything that
fails stays behind under a "Failed" heading.
-->
Never create or touch anything outside <LIB>.
Inputs — a URL is only the common case
Anything that carries the content works. Same note schema, same folder, every
time. Never turn one of these away or ask the user to find a link instead.
| Input |
What to do |
| URL |
Fetch it [WebFetch]. The default path. |
| Pasted text — an article, an email, a newsletter body, release notes |
Read it as given. Do not fetch anything. Set source: paste. |
| A file — PDF, docx, markdown, txt, a saved page |
Read the file directly. Set source: file and record the filename in the Notes section. Read the whole document, not the first page. |
| A screenshot or image |
Read the image. Set source: screenshot. If text in it is unreadable, say which parts you couldn't make out rather than guessing. |
Each arrives one of two ways — in chat ("save this: …", or handing over a
file path), or dropped in the library for later, which is the point of the
inbox and needs no session open:
- Text → pasted straight into
inbox.md, no syntax required.
- Files → dropped into
<LIB>/upload_documents/. Any format. Filename doesn't matter.
confidence: high for all of these — you saw the real content, which is a
stronger source than a fetched page, not a weaker one.
Paste beats a dead link. Many URLs can't be fetched: newsletter tracking
redirects, paywalls, login walls, JS-only pages, PDFs behind a CDN. When a link
fails, tell the user the fix is to open it themselves and paste the text or drop
the file in. That's a first-class path, not a workaround.
A document may hold several things worth saving. A conference PDF or a
newsletter issue can cover five tools. Write one note per thing, not one note
per file. If it's really a single work — a paper, a spec, a long essay — that's
one note, category: reading or research.
Longer documents get more room. The schema is a floor, not a ceiling. For a
paper or a spec, keep the frontmatter exactly as specified, then write as much
in the body as the thing deserves — arguments, findings, numbers, caveats. A
## Key points section is a good addition here. The problem_solved rule never
relaxes: still one sentence, still the user's problem.
Run
If the user handed you content directly (paste, file, or image), skip to
step 5 — build the note from what they gave you. Steps 1–4 are for the inbox.
Still do steps 6–9 so it's archived and indexed like anything else. For a paste
or file with no URL, put unknown in the url: field.
Read <LIB>/inbox.md and collect two kinds of item, ignoring the comment
header and anything under the ## Failed heading:
- URLs — any line starting with
http, on its own line.
- Pasted text — everything else. Use judgment about where one item
ends and the next begins. Do not split on blank lines — a real pasted
article is full of them, and splitting on them would shred one article into
dozens of fake notes. Read the content: one coherent piece of writing is
one item, however long. Two obviously unrelated pieces are two items. A
--- on its own line, if the user bothers to add one, is an explicit
separator you should honour.
- URLs inside a pasted article are part of that article, not separate
items. Only treat a URL as its own item when it stands alone.
List <LIB>/upload_documents/ if it exists. Every file there is an item.
Read <LIB>/_processed.md. Drop anything already listed — by URL, by
(pasted) <title>, or by filename. Count the drops.
Compare URLs with tracking parameters stripped, not as literal strings.
Newsletter links arrive carrying utm_*, ref, dub_id, mid, cid and
similar, and the same article forwarded twice will have different ones —
a literal match sees two URLs and writes two notes for one thing. Strip the
tracking params, drop any trailing #fragment, ignore a trailing slash, and
compare what's left. Store the clean URL in _processed.md and in the
note's url: field, so the archive stays comparable over time.
Announce what you found, broken down by kind: "Found 3 links, 2 pasted
items, 1 file (4 already processed). Processing now." The breakdown matters —
it's the user's chance to catch a stray note-to-self that got read as an
item, before any notes are written. Then go. Do not ask for per-item
approval.
Process in batches of 5. For each item:
- URL → fetch the page [
WebFetch]. Pasted text → read it as given.
File → read the file.
- Extract into the schema below.
- List
<LIB>/notes/*.md [Glob] and check for a near-duplicate by title.
If one exists, update it instead of creating a second file, and say so
in the summary.
- Write
<LIB>/notes/<kebab-case-name>.md.
Append each success to the _processed.md table as | YYYY-MM-DD | x |,
where x is the URL, (pasted) <title>, or (file) <filename>.
Clear inbox.md — carefully. This is the one destructive step in the run.
Re-read inbox.md in full immediately before writing it. The user may
have pasted more into it while you were processing — runs take minutes, and
dumping into the inbox is a thing they do without thinking. Then write back:
the header, anything in the re-read that you did not just process, and
the failures under ## Failed — retry or add manually. A clean run with
nothing newly added leaves it empty apart from the header.
If a write to inbox.md fails with a "file has been modified" error, that
is not a stale handle — it means the user just edited the file. Re-read the
whole file, never a truncated view, and diff it against what you
collected in step 1 before writing anything. Content you never saw is content
you are about to destroy.
Processed files in <LIB>/upload_documents/ stay where they are — never delete a
source file. They're skipped on the next run because _processed.md has
their filenames. Tell the user they can clear the folder themselves whenever
they like.
Regenerate _index.md — read every note's frontmatter, group by
category, one line each: - [[note-name]] — <problem_solved>.
Summarize: saved / updated / skipped-duplicate / failed. Flag every
note that landed in category: other — those need a human decision.
Note schema
Exactly this, at <LIB>/notes/<kebab-case-name>.md:
---
title: Resend
url: https://resend.com
source: inbox
date_saved: 2026-07-22
category: dev-tool
problem_solved: Sending transactional email from an app without deliverability headaches
pricing: Free tier 3k emails/mo; paid from $20/mo
maturity: stable
confidence: high
tags: [email, api, backend, saas]
---
## What it is
Two to three sentences, plain language. What it actually does.
## Use this when
- Concrete situation 1
- Concrete situation 2
## Don't use this when
- Concrete situation where it's the wrong choice
## Notes
Anything else worth remembering — alternatives, gotchas, integrations.
problem_solved — get this right, nothing else matters as much
This is the field shelf-find matches on. A bad one makes the note invisible
forever.
Write the user's problem, not the product's feature list. Phrase it as the
situation they'd be in, in their words, not the vendor's marketing.
- Good:
Sending transactional email without fighting deliverability
- Bad:
A modern email API for developers — that answers "what is this".
Retrieval asks "what do I need right now".
One sentence. No product name in it.
Other fields
| field |
rule |
category |
exactly one of dev-tool ai-agent marketing design data fundraising ops research reading. No fit → other, and flag it in the summary. This list is the user's to change — it's a starting set, not a fixed taxonomy. If they want a category added or removed, edit it here and in shelf-find; recall goes blind to any category only one file knows about. |
source |
inbox | paste | file | screenshot — where the content came from. |
url |
the source URL. unknown for a paste or file with no link. |
maturity |
experimental | stable | enterprise |
confidence |
high if fetched and parsed clean. low if it came from fallback search, paywall, or JS-blocked page. |
tags |
3–6 lowercase keywords. Reuse existing tags over inventing near-synonyms — auth/authentication/authn as three tags means three partial matches instead of one good one. Check first by searching the notes for existing tags: lines. |
pricing |
write unknown if not findable. Do not guess. |
Never invent facts. Any field you can't determine: unknown.
Failures
- Paywall / 403 / JS-only: fall back to a web search [
WebSearch] on the
page title or domain. Build the note from that, set confidence: low, and put
this at the top of the note body:
> [!warning] Fetched indirectly — verify before relying on this.
- Total failure (search turns up nothing usable): leave the URL in
inbox.md under the failed heading. Never write a hallucinated note.
When reporting failures, tell the user they can open the link themselves and
paste the text, or drop the file in — that always works and produces a better
note than a fetch would have. Newsletter tracking redirects and CDN-hosted
PDFs fail this way constantly; they are not worth retrying.
- Never delete a note. Never clear
_processed.md.
- Note already exists and you're updating it: preserve the original
date_saved, overwrite the rest.
1---2name: shelf-save3description: Save links, articles, documents, or screenshots into the user's tool library as searchable notes. USE THIS whenever the user says "process my inbox", "save this", "save these links", "ingest my newsletter links", "add to my tool library", "/shelf-save", "file these", "keep this for later" — or pastes a batch of URLs, pastes article or newsletter text, or hands over a PDF, doc, or screenshot with any intent to keep it. A URL is only the common case; pasted text and uploaded files are first-class inputs, and are the right answer whenever a link is paywalled, login-walled, or a tracking redirect. Also use when the user mentions their inbox.md, their tool library, or newsletter links piling up. Writes one note per thing, dedupes, rebuilds the index.4license: MIT5---67# shelf-save89Turn raw URLs into a searchable tool library of markdown notes.1011Tools are named by what they do, with the Claude Code name in brackets. Use12whatever your harness calls the equivalent.1314## Step 0 — find the library (always do this first)1516Read `~/.tool-library-path` (in the user's home directory). It holds one line:17the library folder. Call it `<LIB>` below.1819**If it exists**, use that path.2021**If it's missing**, this is a first run:22231. Ask: *"Where should your tool library live? Give me a folder path — an24 Obsidian vault subfolder works, or any normal folder."*252. Write their answer as a single line to `~/.tool-library-path`.26 Use forward slashes even on Windows.273. Create this structure under it, then **tell the user the full path you28 created and what goes where** — they'll need to find it without you.2930```31<LIB>/32├── inbox.md # user dumps URLs and pasted text here33├── upload_documents/ # user drops PDFs, docs, screenshots here34├── _processed.md # append-only archive; dedupe source of truth35├── _index.md # auto-generated, grouped by category36└── notes/ # one file per tool37```3839`_processed.md` starts with a `| date | source |` table header. `inbox.md`40starts with this comment, verbatim:4142```markdown43<!--44Dump here. No formatting, no thinking.4546 URLs one per line47 Articles paste the text, as messy as you like48 Documents drop the file in the upload_documents/ folder next to this file4950Then say "process my inbox".5152Paste the text whenever a link won't load — paywalls, logins, newsletter53tracking redirects. It makes a better note than fetching would.5455Everything that works becomes a note and leaves this file. Anything that56fails stays behind under a "Failed" heading.57-->58```5960Never create or touch anything outside `<LIB>`.6162## Inputs — a URL is only the common case6364Anything that carries the content works. Same note schema, same folder, every65time. Never turn one of these away or ask the user to find a link instead.6667| Input | What to do |68|---|---|69| **URL** | Fetch it [`WebFetch`]. The default path. |70| **Pasted text** — an article, an email, a newsletter body, release notes | Read it as given. **Do not fetch anything.** Set `source: paste`. |71| **A file** — PDF, docx, markdown, txt, a saved page | Read the file directly. Set `source: file` and record the filename in the `Notes` section. Read the whole document, not the first page. |72| **A screenshot or image** | Read the image. Set `source: screenshot`. If text in it is unreadable, say which parts you couldn't make out rather than guessing. |7374Each arrives one of two ways — **in chat** ("save this: …", or handing over a75file path), or **dropped in the library** for later, which is the point of the76inbox and needs no session open:7778- **Text** → pasted straight into `inbox.md`, no syntax required.79- **Files** → dropped into `<LIB>/upload_documents/`. Any format. Filename doesn't matter.8081`confidence: high` for all of these — you saw the real content, which is a82stronger source than a fetched page, not a weaker one.8384**Paste beats a dead link.** Many URLs can't be fetched: newsletter tracking85redirects, paywalls, login walls, JS-only pages, PDFs behind a CDN. When a link86fails, tell the user the fix is to open it themselves and paste the text or drop87the file in. That's a first-class path, not a workaround.8889**A document may hold several things worth saving.** A conference PDF or a90newsletter issue can cover five tools. Write one note per thing, not one note91per file. If it's really a single work — a paper, a spec, a long essay — that's92one note, `category: reading` or `research`.9394**Longer documents get more room.** The schema is a floor, not a ceiling. For a95paper or a spec, keep the frontmatter exactly as specified, then write as much96in the body as the thing deserves — arguments, findings, numbers, caveats. A97`## Key points` section is a good addition here. The `problem_solved` rule never98relaxes: still one sentence, still the user's problem.99100## Run101102**If the user handed you content directly** (paste, file, or image), skip to103step 5 — build the note from what they gave you. Steps 1–4 are for the inbox.104Still do steps 6–9 so it's archived and indexed like anything else. For a paste105or file with no URL, put `unknown` in the `url:` field.1061071. **Read** `<LIB>/inbox.md` and collect two kinds of item, ignoring the comment108 header and anything under the `## Failed` heading:109 - **URLs** — any line starting with `http`, on its own line.110 - **Pasted text** — everything else. **Use judgment about where one item111 ends and the next begins. Do not split on blank lines** — a real pasted112 article is full of them, and splitting on them would shred one article into113 dozens of fake notes. Read the content: one coherent piece of writing is114 one item, however long. Two obviously unrelated pieces are two items. A115 `---` on its own line, if the user bothers to add one, is an explicit116 separator you should honour.117 - **URLs inside a pasted article** are part of that article, not separate118 items. Only treat a URL as its own item when it stands alone.1192. **List** `<LIB>/upload_documents/` if it exists. Every file there is an item.1203. **Read** `<LIB>/_processed.md`. Drop anything already listed — by URL, by121 `(pasted) <title>`, or by filename. Count the drops.122123 **Compare URLs with tracking parameters stripped**, not as literal strings.124 Newsletter links arrive carrying `utm_*`, `ref`, `dub_id`, `mid`, `cid` and125 similar, and the same article forwarded twice will have different ones —126 a literal match sees two URLs and writes two notes for one thing. Strip the127 tracking params, drop any trailing `#fragment`, ignore a trailing slash, and128 compare what's left. Store the **clean** URL in `_processed.md` and in the129 note's `url:` field, so the archive stays comparable over time.1304. **Announce** what you found, broken down by kind: "Found 3 links, 2 pasted131 items, 1 file (4 already processed). Processing now." The breakdown matters —132 it's the user's chance to catch a stray note-to-self that got read as an133 item, before any notes are written. Then go. Do **not** ask for per-item134 approval.1355. **Process in batches of 5.** For each item:136 - URL → fetch the page [`WebFetch`]. Pasted text → read it as given.137 File → read the file.138 - Extract into the schema below.139 - List `<LIB>/notes/*.md` [`Glob`] and check for a near-duplicate by title.140 If one exists, **update it** instead of creating a second file, and say so141 in the summary.142 - Write `<LIB>/notes/<kebab-case-name>.md`.1436. **Append** each success to the `_processed.md` table as `| YYYY-MM-DD | x |`,144 where `x` is the URL, `(pasted) <title>`, or `(file) <filename>`.1457. **Clear `inbox.md` — carefully. This is the one destructive step in the run.**146147 **Re-read `inbox.md` in full immediately before writing it.** The user may148 have pasted more into it while you were processing — runs take minutes, and149 dumping into the inbox is a thing they do without thinking. Then write back:150 the header, anything in the re-read that you did **not** just process, and151 the failures under `## Failed — retry or add manually`. A clean run with152 nothing newly added leaves it empty apart from the header.153154 **If a write to `inbox.md` fails with a "file has been modified" error, that155 is not a stale handle — it means the user just edited the file.** Re-read the156 **whole** file, never a truncated view, and diff it against what you157 collected in step 1 before writing anything. Content you never saw is content158 you are about to destroy.159160 Processed files in `<LIB>/upload_documents/` stay where they are — **never delete a161 source file.** They're skipped on the next run because `_processed.md` has162 their filenames. Tell the user they can clear the folder themselves whenever163 they like.1648. **Regenerate** `_index.md` — read every note's frontmatter, group by165 `category`, one line each: `- [[note-name]] — <problem_solved>`.1669. **Summarize**: saved / updated / skipped-duplicate / failed. Flag every167 note that landed in `category: other` — those need a human decision.168169## Note schema170171Exactly this, at `<LIB>/notes/<kebab-case-name>.md`:172173```markdown174---175title: Resend176url: https://resend.com177source: inbox178date_saved: 2026-07-22179category: dev-tool180problem_solved: Sending transactional email from an app without deliverability headaches181pricing: Free tier 3k emails/mo; paid from $20/mo182maturity: stable183confidence: high184tags: [email, api, backend, saas]185---186## What it is187188Two to three sentences, plain language. What it actually does.189190## Use this when191192- Concrete situation 1193- Concrete situation 2194195## Don't use this when196197- Concrete situation where it's the wrong choice198199## Notes200201Anything else worth remembering — alternatives, gotchas, integrations.202```203204### `problem_solved` — get this right, nothing else matters as much205206This is the field `shelf-find` matches on. A bad one makes the note invisible207forever.208209Write **the user's problem**, not the product's feature list. Phrase it as the210situation they'd be in, in their words, not the vendor's marketing.211212- Good: `Sending transactional email without fighting deliverability`213- Bad: `A modern email API for developers` — that answers "what is this".214 Retrieval asks "what do I need right now".215216One sentence. No product name in it.217218### Other fields219220| field | rule |221|---|---|222| `category` | exactly one of `dev-tool` `ai-agent` `marketing` `design` `data` `fundraising` `ops` `research` `reading`. No fit → `other`, and flag it in the summary. **This list is the user's to change** — it's a starting set, not a fixed taxonomy. If they want a category added or removed, edit it here **and** in `shelf-find`; recall goes blind to any category only one file knows about. |223| `source` | `inbox` \| `paste` \| `file` \| `screenshot` — where the content came from. |224| `url` | the source URL. `unknown` for a paste or file with no link. |225| `maturity` | `experimental` \| `stable` \| `enterprise` |226| `confidence` | `high` if fetched and parsed clean. `low` if it came from fallback search, paywall, or JS-blocked page. |227| `tags` | 3–6 lowercase keywords. Reuse existing tags over inventing near-synonyms — `auth`/`authentication`/`authn` as three tags means three partial matches instead of one good one. Check first by searching the notes for existing `tags:` lines. |228| `pricing` | write `unknown` if not findable. **Do not guess.** |229230**Never invent facts.** Any field you can't determine: `unknown`.231232## Failures233234- **Paywall / 403 / JS-only**: fall back to a web search [`WebSearch`] on the235 page title or domain. Build the note from that, set `confidence: low`, and put236 this at the top of the note body:237 `> [!warning] Fetched indirectly — verify before relying on this.`238- **Total failure** (search turns up nothing usable): leave the URL in239 `inbox.md` under the failed heading. **Never write a hallucinated note.**240 When reporting failures, tell the user they can open the link themselves and241 paste the text, or drop the file in — that always works and produces a better242 note than a fetch would have. Newsletter tracking redirects and CDN-hosted243 PDFs fail this way constantly; they are not worth retrying.244- Never delete a note. Never clear `_processed.md`.245- Note already exists and you're updating it: preserve the original246 `date_saved`, overwrite the rest.