Most transcript tools hand you a wall of caption text and stop. YouTube Fetcher to Markdown produces a filed record instead: YAML frontmatter, creator metadata, the description with chapter markers, the full transcript, and a note of which caption language it actually used. One link in, one queryable note out, no API key. It ships as a portable SKILL.md, so Claude Code, Codex, Cursor, Windsurf and Gemini CLI all load it the same way.

| Author | JimmySadek |
| Language | Python, one 676-line script |
| Licence | MIT |
| Requires | Python 3.8 or newer |
| Created | 4th March 2026 |
| Latest release | v1.1.0, 3rd August 2026 |
| Traction | 331 stars, 28 forks, 0 open issues |
The repository reports HTML as its main language. That is one 51 KB release-spec document in specs/, not a web app. The working code is Python.
What it produces
---
title: "Obsidian: The King of Learning Tools (FULL GUIDE + SETUP)"
channel: "Odysseas"
url: "https://www.youtube.com/watch?v=hSTy_BInQs8"
video_id: "hSTy_BInQs8"
fetched: "2026-03-04"
source_project: "my-project"
language: "en"
caption_type: "manual"
duration: "36m 26s"
upload_date: "2024-04-24"
tags:
- yt-transcript
---
Below the frontmatter it writes a Video Details table, the creator's description with any chapter markers, and the transcript. The filename encodes the date, a slug and the video ID:
~/yt_transcripts/2026-03-04_obsidian-the-king-of-learning-tools_[hSTy_BInQs8].md
That video_id field is the load-bearing part. It makes the collection queryable through Dataview and it is what the script reads to detect a duplicate before writing.
Installing
npx skills add JimmySadek/youtube-fetcher-to-markdown
python3 -m pip install -r requirements.txt
brew install yt-dlp # optional but do it anyway
npx skills add comes from the skills CLI, an open installer for agent skills. Cloning the repo works just as well.
Runtime dependencies are thin: youtube-transcript-api and requests. yt-dlp is listed as optional, but without it you lose the description, chapters, duration and upload date, which is most of what separates this from a plain caption dump. Install it.
Check the setup without fetching anything:
python3 scripts/fetch_transcript.py --check-deps
Options worth knowing
| Flag | What it does |
|---|---|
--output-dir |
Write into an Obsidian vault or any directory |
--output / -o |
Write to one exact file, wins over --output-dir |
--stdout |
Print instead of saving |
--timestamps / -t |
Keep timestamps on transcript lines |
--lang / -l |
Request a caption language, falls back to English |
--format / -f |
Export json or srt instead of Markdown |
--list |
Show the caption languages a video offers |
--no-description |
Skip description and chapters |
--force |
Overwrite an existing note |
Output location resolves in a fixed order: --output, then --output-dir, then the YOUTUBE_FETCHER_DIR environment variable, then ~/yt_transcripts/.
Two design choices I like
Exit codes that mean something. The script defines four, and I confirmed them in the source rather than trusting the README.
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Invalid input or fetch failure |
2 |
Missing dependency |
3 |
An existing note was found and left untouched |
Code 3 is the interesting one. In a non-interactive run the script refuses to clobber a note you already have and exits rather than asking. The skill instructions tell the agent to report that and wait for a decision, and to never install packages on your behalf. That is the right default for anything an agent runs unattended.
Truthful language reporting. If you ask for Spanish captions and only English exists, the note records English. The skill file explicitly tells the agent not to claim the requested language was used. Small thing, and the sort of thing that quietly corrupts an archive when a tool gets it wrong.
The URL parser also holds up. It keeps an allowlist of YouTube hosts, handles youtu.be, /embed/, /shorts/, /live/, legacy /v/, music and mobile URLs, privacy-enhanced youtube-nocookie.com, and a bare 11-character ID. A lookalike host such as youtube.com.example.org gets rejected.
How it compares to what I already run
I have my own helper doing the fetching side, so the overlap is worth being precise about.
| This skill | My yt.py |
|
|---|---|---|
| Captions from | youtube-transcript-api |
yt-dlp |
| Metadata from | yt-dlp, oEmbed fallback | yt-dlp |
| Output | One archival .md file |
stdout, composable |
| Audio and video download | No | Yes |
| Duplicate detection | Yes, by video_id |
No |
| Provenance frontmatter | Yes | No |
Different jobs. Mine is a building block that other skills call and pipe; this one is a filing cabinet. Neither replaces the other.
Two ideas here are worth stealing. Duplicate detection keyed on video_id is the obvious one: reading frontmatter in the target directory before writing costs nothing and stops an archive filling with near-identical notes. Capture provenance is the subtler one. Recording the fetch date, the caption type (manual or auto-generated) and the source project means that in two years you can tell a human-written transcript from a machine guess. Auto-generated captions are good enough to mislead you and not good enough to quote.
The split dependency is the trade-off to weigh. Captions come from youtube-transcript-api while metadata comes from yt-dlp, so the two halves break independently when YouTube changes something. yt-dlp is patched aggressively; a second library is a second thing to wait on. Against that, the oEmbed fallback means a missing yt-dlp degrades the note rather than failing the run.
Limits
- Captions must exist and be reachable. Private, restricted and caption-disabled videos fail. There is no Whisper fallback.
- No video or audio download, no speaker identification, no translation. It reads captions and metadata, nothing else.
- Auto-generated captions carry their usual errors into the archive. The note flags the caption type, which is the honest handling, but it does not clean the text.
- Quiet since 3rd August 2026. Zero open issues at 331 stars reads as a finished small tool rather than an abandoned one, though the repository has not moved in weeks.
Where it fits
The obvious pairing is an Obsidian vault: point --output-dir at it, or set YOUTUBE_FETCHER_DIR once, and every video you watch lands as a Dataview-queryable note. The Markdown stays portable to Logseq or plain files if you leave Obsidian later, because the only Obsidian-specific thing about it is the frontmatter convention.
The case for running it alongside a fetching helper you already own is the archive, not the fetch. If your current setup pipes transcripts into a summary and throws the source away, this gives you the durable copy underneath, with enough provenance that the copy is still trustworthy later.
Further reading
- JimmySadek/youtube-fetcher-to-markdown
- skills, the open agent skills installer
- youtube-transcript-api on PyPI
- Obsidian Dataview