en un clic
obsidian-annotator
// Create and manage PDF annotations in Obsidian using the Annotator plugin. Use when user asks to annotate, highlight, or comment on a PDF in their vault. Sub-skill of obsidian.
// Create and manage PDF annotations in Obsidian using the Annotator plugin. Use when user asks to annotate, highlight, or comment on a PDF in their vault. Sub-skill of obsidian.
Building terminal UI tools with Bubble Tea and the shared trenthaines.dev/tui package. Use when creating TUI pickers, popups, or any interactive terminal tools — covers the shared package, layout patterns, testing, chezmoi integration, and known gotchas.
Help with Obsidian Zettelkasten vault management - search, create, edit, and organize notes in the user's personal knowledge base with 1200+ notes. Use when the user asks about their Zettelkasten, vault, notes, knowledge management, or wants to create/modify markdown notes in ~/Documents/Zettelkasten. (project, gitignored)
Generate daily activity summaries from Claude Code logs. Use when the user asks for a daily summary, work log, what they did today/yesterday, or activity report.
Deploy the duet local development server pointing to production (DECAGON_ENV=prod). Use when the user asks to start the local server against prod, run the dev environment with prod, or test locally against production.
Deploy the duet local development server (backend + frontend) in tmux panes. Use when the user asks to start the local server, run the dev environment, or start backend/frontend.
Deploy the local voice development server with ngrok for outbound calling. Use when the user asks to test voice locally, start the voice server, or debug voice calls.
| name | obsidian-annotator |
| description | Create and manage PDF annotations in Obsidian using the Annotator plugin. Use when user asks to annotate, highlight, or comment on a PDF in their vault. Sub-skill of obsidian. |
Create PDF annotations programmatically that render correctly in the Obsidian Annotator plugin (elias-sundqvist/obsidian-annotator). Annotations are stored as markdown in the note file, not embedded in the PDF.
TextPositionSelector — character offsets (fast, but fragile across extractors)TextQuoteSelector — exact text + prefix/suffix context (fuzzy fallback, more reliable)TextQuoteSelector is the primary reliable anchor — even if position offsets are approximate, the quote matching will find the correct textThe note must have annotation-target in frontmatter pointing to the PDF:
---
annotation-target: "Files/My Paper.pdf"
---
The path is relative to the vault root.
Reuse the fingerprint from an existing annotation in the same note. If none exists, it's derived from the PDF's /ID trailer entry (or MD5 of first 1024 bytes as fallback). The fingerprint is a 32-char hex string.
If you need to extract it fresh:
# The fingerprint from the existing annotation JSON → document.documentFingerprint
# Or extract from PDF: it's the PDF /ID field, or MD5 of first 1024 bytes
python3 -c "
import hashlib
with open('path/to/file.pdf', 'rb') as f:
print(hashlib.md5(f.read(1024)).hexdigest())
"
Note: This fallback method may not match the PDF /ID method. Always prefer reusing the fingerprint from an existing annotation.
Use pdftotext to extract text and find the passage to highlight:
pdftotext "Files/My Paper.pdf" - | grep -n "target phrase"
You need:
exact: The exact text to highlightprefix: ~30-50 chars before the highlight (for anchoring)suffix: ~30-50 chars after the highlight (for anchoring)Important: The prefix/suffix help Hypothesis fuzzy-match the location. Be generous with context. The text should match what appears in the PDF, though whitespace differences are tolerated.
Append this to the note file (after the ## Annotations section):
>%%
>```annotation-json
>{"created":"TIMESTAMP","text":"YOUR COMMENT HERE","updated":"TIMESTAMP","document":{"title":"FILENAME","link":[{"href":"urn:x-pdf:FINGERPRINT"},{"href":"vault:/PATH/TO/PDF"}],"documentFingerprint":"FINGERPRINT"},"uri":"vault:/PATH/TO/PDF","target":[{"source":"vault:/PATH/TO/PDF","selector":[{"type":"TextPositionSelector","start":START,"end":END},{"type":"TextQuoteSelector","exact":"HIGHLIGHTED TEXT","prefix":"TEXT BEFORE ","suffix":" TEXT AFTER"}]}]}
>```
>%%
>*%%PREFIX%%TEXT BEFORE%%HIGHLIGHT%% ==HIGHLIGHTED TEXT== %%POSTFIX%%TEXT AFTER*
>%%LINK%%[[#^ANNOTATION_ID|show annotation]]
>%%COMMENT%%
>YOUR COMMENT HERE
>
>%%TAGS%%
>
^ANNOTATION_ID
| Field | Description | Example |
|---|---|---|
created/updated | ISO 8601 timestamp | 2026-04-05T01:30:00.000Z |
text | Your comment/note | Key insight about control |
documentFingerprint | 32-char hex from PDF | c2cc5ee7532df5f679608a9935615dd3 |
uri | Vault path to PDF | vault:/Files/My Paper.pdf |
start/end | Character offsets (approximate OK) | 350, 500 |
exact | The highlighted text verbatim | the exact passage |
prefix | ~30-50 chars before highlight | context before |
suffix | ~30-50 chars after highlight | context after |
ANNOTATION_ID | Unique ID (alphanumeric) | abc123xyz |
Use a random alphanumeric string, 8-12 chars. Must be unique within the file.
These are character offsets into the full concatenated PDF text as extracted by PDF.js. Since pdftotext output differs slightly from PDF.js, these will be approximate. This is fine — the TextQuoteSelector (exact/prefix/suffix) is the reliable fallback anchor.
For approximate offsets, use pdftotext and count characters:
pdftotext "file.pdf" - | head -c 1000 | wc -c # rough offset estimation
Or just use reasonable estimates. The quote selector does the heavy lifting.
To annotate "it will be increasingly important to prevent them from causing harmful outcomes" in a paper:
>%%
>```annotation-json
>{"created":"2026-04-05T01:30:00.000Z","text":"Core motivation for the paper.","updated":"2026-04-05T01:30:00.000Z","document":{"title":"My%20Paper.pdf","link":[{"href":"urn:x-pdf:c2cc5ee7532df5f679608a9935615dd3"},{"href":"vault:/Files/My%20Paper.pdf"}],"documentFingerprint":"c2cc5ee7532df5f679608a9935615dd3"},"uri":"vault:/Files/My%20Paper.pdf","target":[{"source":"vault:/Files/My%20Paper.pdf","selector":[{"type":"TextPositionSelector","start":350,"end":430},{"type":"TextQuoteSelector","exact":"it will be increasingly important to prevent them from causing harmful outcomes.","prefix":"more powerful and are deployed more autonomously, ","suffix":" Researchers have investigated a"}]}]}
>```
>%%
>*%%PREFIX%%more powerful and are deployed more autonomously,%%HIGHLIGHT%% ==it will be increasingly important to prevent them from causing harmful outcomes.== %%POSTFIX%%Researchers have investigated a*
>%%LINK%%[[#^ann001|show annotation]]
>%%COMMENT%%
>Core motivation for the paper.
>
>%%TAGS%%
>
^ann001
pdftotext to extract text around the target passageJust append multiple annotation blocks sequentially. Each needs a unique ^ID.
To add tags, include them in the JSON "tags":["tag1","tag2"] and in the markdown:
>%%TAGS%%
>#tag1, #tag2
When the user wants to use Claude for deep reading, comprehension, and annotation of a PDF:
Files/<Paper Name>.pdfpdftotext "Files/<Paper Name>.pdf" "Files/<Paper Name>.txt"annotation-target frontmatter.txt file enables instant Read access without re-converting each timeRead on the .txt file with offset/limit for specific sections[[wikilinks]] to connect ideasFor each paper, there are up to 3 files:
Files/<Paper Name>.pdf — the original PDF (for Annotator rendering)Files/<Paper Name>.txt — extracted text (for Claude fast reading)Zettelkasten/Notes/<Paper Name>.md — the annotation note + user's synthesis>)^ID anchor line must not start with > — it's outside the blockquote==highlighted text== in the HIGHLIGHT section is for Obsidian rendering, not for anchoring%20prefix and suffix should be long enough for unique matching (~30-50 chars)