← Back to Lessons
Claude Code Skill

YouTube Transcript Skill

Fetch the full transcript of any YouTube video or playlist by pasting a URL into Claude Code. Runs locally, no API keys.
⏱ ~10 seconds to install

What you get. Drop the skill in, paste a YouTube URL in your next message, Claude returns the transcript. Supports plain text, markdown with timestamps, SRT / VTT subtitle files, and JSON. Also handles whole playlists.

How it works. Cascading fallback — youtube-transcript-api first, yt-dlp as backup. Runs entirely on your machine, so there's no token cost for the fetch itself.

1
Install

Open Claude Code in any folder, paste the prompt below, hit enter. Claude will create the skill in your global skills folder (~/.claude/skills/youtube-transcript/), write both files, install Python deps, and verify the install.

One-paste install prompt — self-contained, ~16 KB, SKILL.md + Python script inline.

Install a global Claude Code skill called "youtube-transcript". Do all of the following: 1. Create directory: ~/.claude/skills/youtube-transcript/scripts/ 2. Write the file ~/.claude/skills/youtube-transcript/SKILL.md with EXACTLY this content (between the SKILL_START and SKILL_END markers, do not include the markers themselves): ===SKILL_START=== --- name: youtube-transcript description: Fetch transcripts from any YouTube video or playlist using a local Python script (yt-dlp + youtube-transcript-api). Use whenever the user gives a YouTube URL and asks to get, fetch, download, extract, summarize, or analyze the transcript. Runs locally — no API keys, no cloud, no token cost for the fetch itself. --- # YouTube Transcript Extractor Extracts transcripts from YouTube videos and playlists using a bundled Python script with a cascading fallback (youtube-transcript-api → yt-dlp). Everything runs locally on the user's machine. ## When to use Trigger this skill when the user: - Pastes a YouTube URL and asks for the transcript - Asks to summarize, analyze, or extract insights from a video - Asks to download captions or subtitles - Mentions a YouTube playlist they want transcripts from ## How to run it The bundled script lives next to this `SKILL.md` at `scripts/yt_transcript.py`. From a Claude Code session, invoke it via Bash. The absolute path is: ``` ~/.claude/skills/youtube-transcript/scripts/yt_transcript.py ``` ### Default — plain text (best for feeding to Claude) ```bash python3 ~/.claude/skills/youtube-transcript/scripts/yt_transcript.py "YOUTUBE_URL" ``` ### Other formats ```bash # Markdown with metadata header + timestamps python3 ~/.claude/skills/youtube-transcript/scripts/yt_transcript.py "URL" --format markdown # Timestamped plain text python3 ~/.claude/skills/youtube-transcript/scripts/yt_transcript.py "URL" --format timestamped # SRT / VTT subtitle files python3 ~/.claude/skills/youtube-transcript/scripts/yt_transcript.py "URL" --format srt python3 ~/.claude/skills/youtube-transcript/scripts/yt_transcript.py "URL" --format vtt # Full structured JSON python3 ~/.claude/skills/youtube-transcript/scripts/yt_transcript.py "URL" --format json ``` ### Playlist (extracts every video) ```bash python3 ~/.claude/skills/youtube-transcript/scripts/yt_transcript.py --playlist "PLAYLIST_URL" --format plain ``` ### Save to file ```bash python3 ~/.claude/skills/youtube-transcript/scripts/yt_transcript.py "URL" --format markdown -o transcript.md ``` ### Non-English ```bash python3 ~/.claude/skills/youtube-transcript/scripts/yt_transcript.py "URL" --lang es ``` ## Format selection | Goal | Format | |------|--------| | Feed to Claude for analysis/summary | `plain` (default) | | Save a readable reference with timestamps | `markdown` | | Quickly scan what's said when | `timestamped` | | Generate subtitle files | `srt` or `vtt` | | Programmatic processing | `json` | ## Behavior rules - Default to `--format plain` when the user just wants the transcript or wants analysis. - Use `-o filename.ext` when the user asks to save/download. Match the extension to the format (`.txt`, `.md`, `.srt`, `.vtt`, `.json`). - For playlists, warn the user it may take a moment, then proceed. - **Always present the full transcript.** Never truncate or summarize the raw output unless the user explicitly asks for a summary. ## Error handling Relay the actual error message from stderr — do not use a generic failure response: - **"Could not extract video ID from: …"** — URL is invalid. Ask the user to double-check it. - **"All extraction methods failed for …"** — The video has no captions/subtitles available, or YouTube blocked the request. - **Network timeout / connection error** — Ask the user to check their connection and retry. - **`ImportError: No module named youtube_transcript_api` or `yt_dlp`** — Tell the user to install the missing dependency: ```bash pip install youtube-transcript-api brew install yt-dlp # or: pip install yt-dlp ``` ## Dependencies The script requires both packages to be installed on the user's machine: - `youtube-transcript-api` (pip) - `yt-dlp` (brew or pip) If either is missing the script will print a clear `ImportError`. Walk the user through the install one-liner above. ===SKILL_END=== 3. Write the file ~/.claude/skills/youtube-transcript/scripts/yt_transcript.py with EXACTLY this content (between SCRIPT_START and SCRIPT_END markers, do not include the markers themselves): ===SCRIPT_START=== #!/usr/bin/env python3 """ yt_transcript.py — Lightweight YouTube transcript extractor for Claude Code. Cascading fallback: youtube-transcript-api → yt-dlp subtitles → yt-dlp + innertube. Based on YT Transcript Pro by BizzyForge. Usage: python3 yt_transcript.py <URL_or_ID> [--format markdown|plain|timestamped|srt|vtt|json] [--lang en] python3 yt_transcript.py --playlist <PLAYLIST_URL> [--format markdown] """ import argparse import json import html import os import re import sys import tempfile from pathlib import Path # --------------------------------------------------------------------------- # Video ID / URL helpers # --------------------------------------------------------------------------- def extract_video_id(url_or_id): url_or_id = url_or_id.strip() patterns = [ r'(?:youtube\.com/watch\?.*v=)([a-zA-Z0-9_-]{11})', r'(?:youtu\.be/)([a-zA-Z0-9_-]{11})', r'(?:youtube\.com/shorts/)([a-zA-Z0-9_-]{11})', r'(?:youtube\.com/embed/)([a-zA-Z0-9_-]{11})', r'(?:youtube\.com/live/)([a-zA-Z0-9_-]{11})', ] for p in patterns: m = re.search(p, url_or_id) if m: return m.group(1) if re.match(r'^[a-zA-Z0-9_-]{11}$', url_or_id): return url_or_id raise ValueError(f"Could not extract video ID from: {url_or_id}") def is_playlist_url(url): return "playlist" in url or ("list=" in url and "watch" not in url) # --------------------------------------------------------------------------- # Timestamp formatting # --------------------------------------------------------------------------- def fmt_ts(seconds): total = int(seconds) h, rem = divmod(total, 3600) m, s = divmod(rem, 60) return f"{h}:{m:02d}:{s:02d}" if h else f"{m}:{s:02d}" def fmt_srt_ts(seconds): h = int(seconds // 3600) m = int((seconds % 3600) // 60) s = int(seconds % 60) ms = int((seconds - int(seconds)) * 1000) return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}" def fmt_vtt_ts(seconds): h = int(seconds // 3600) m = int((seconds % 3600) // 60) s = int(seconds % 60) ms = int((seconds - int(seconds)) * 1000) return f"{h:02d}:{m:02d}:{s:02d}.{ms:03d}" # --------------------------------------------------------------------------- # Layer 1: youtube-transcript-api # --------------------------------------------------------------------------- def extract_via_api(video_id, languages=None): languages = languages or ["en"] try: from youtube_transcript_api import YouTubeTranscriptApi except ImportError: return None try: api = YouTubeTranscriptApi() tl = api.list(video_id) t = tl.find_transcript(languages) fetched = t.fetch() segments = [ {"text": s.text.strip(), "start": s.start, "duration": s.duration} for s in fetched ] return { "video_id": video_id, "title": "", # filled later "language": t.language, "language_code": t.language_code, "is_generated": t.is_generated, "method": "youtube-transcript-api", "segments": segments, } except Exception as e: print(f"[Layer 1 youtube-transcript-api] {e}", file=sys.stderr) return None # --------------------------------------------------------------------------- # Layer 2: yt-dlp subtitle download # --------------------------------------------------------------------------- def extract_via_ytdlp(video_id, languages=None): languages = languages or ["en"] try: import yt_dlp except ImportError: return None try: url = f"https://www.youtube.com/watch?v={video_id}" with tempfile.TemporaryDirectory() as tmpdir: ydl_opts = { "writesubtitles": True, "writeautomaticsub": True, "subtitleslangs": languages, "subtitlesformat": "json3", "skip_download": True, "outtmpl": os.path.join(tmpdir, "%(id)s.%(ext)s"), "quiet": True, "no_warnings": True, } with yt_dlp.YoutubeDL(ydl_opts) as ydl: info = ydl.extract_info(url, download=True) title = info.get("title", "Unknown") sub_files = list(Path(tmpdir).glob("*.json3")) if not sub_files: return None with open(sub_files[0], "r", encoding="utf-8") as f: sub_data = json.load(f) segments = [] for event in sub_data.get("events", []): if "segs" not in event: continue parts = [s.get("utf8", "").strip() for s in event["segs"] if s.get("utf8", "").strip() and s.get("utf8", "").strip() != "\n"] if parts: segments.append({ "text": " ".join(parts), "start": event.get("tStartMs", 0) / 1000.0, "duration": event.get("dDurationMs", 0) / 1000.0, }) if not segments: return None lang_code = sub_files[0].stem.split(".")[-1] if "." in sub_files[0].stem else "en" return { "video_id": video_id, "title": title, "language": lang_code, "language_code": lang_code, "is_generated": True, "method": "yt-dlp", "segments": segments, } except Exception as e: print(f"[Layer 2 yt-dlp] {e}", file=sys.stderr) return None # --------------------------------------------------------------------------- # Metadata fetch (for title when layer 1 doesn't provide it) # --------------------------------------------------------------------------- def fetch_title(video_id): try: import yt_dlp url = f"https://www.youtube.com/watch?v={video_id}" with yt_dlp.YoutubeDL({"quiet": True, "no_warnings": True, "skip_download": True}) as ydl: info = ydl.extract_info(url, download=False) return info.get("title", "Unknown") except Exception: return "Unknown" # --------------------------------------------------------------------------- # Orchestrator # --------------------------------------------------------------------------- def extract_transcript(url_or_id, languages=None): video_id = extract_video_id(url_or_id) languages = languages or ["en"] # Try Layer 1 result = extract_via_api(video_id, languages) if result and result["segments"]: if not result["title"]: result["title"] = fetch_title(video_id) return result # Try Layer 2 result = extract_via_ytdlp(video_id, languages) if result and result["segments"]: return result raise RuntimeError(f"All extraction methods failed for {video_id}") def resolve_playlist(playlist_url): import yt_dlp ydl_opts = {"quiet": True, "no_warnings": True, "extract_flat": True, "skip_download": True} with yt_dlp.YoutubeDL(ydl_opts) as ydl: info = ydl.extract_info(playlist_url, download=False) entries = info.get("entries", []) return [{"id": e["id"], "title": e.get("title", "Unknown")} for e in entries if e] # --------------------------------------------------------------------------- # Formatters # --------------------------------------------------------------------------- def format_result(result, fmt="plain"): segs = result["segments"] title = result["title"] vid = result["video_id"] url = f"https://www.youtube.com/watch?v={vid}" full_text = " ".join(s["text"] for s in segs) word_count = len(full_text.split()) duration = fmt_ts(segs[-1]["start"] + segs[-1]["duration"]) if segs else "0:00" if fmt == "plain": return full_text elif fmt == "timestamped": return "\n".join(f"[{fmt_ts(s['start'])}] {s['text']}" for s in segs) elif fmt == "markdown": lines = [ f"# {title}", "", f"**Video:** [{vid}]({url})", f"**Language:** {result['language']} ({result['language_code']})", f"**Auto-generated:** {'Yes' if result['is_generated'] else 'No'}", f"**Method:** {result['method']}", f"**Duration:** {duration}", f"**Words:** {word_count:,}", "", "---", "", ] for s in segs: lines.append(f"**[{fmt_ts(s['start'])}]** {s['text']}") lines.append("") return "\n".join(lines) elif fmt == "srt": lines = [] for i, s in enumerate(segs, 1): end = s["start"] + s["duration"] lines.extend([str(i), f"{fmt_srt_ts(s['start'])} --> {fmt_srt_ts(end)}", s["text"], ""]) return "\n".join(lines) elif fmt == "vtt": lines = ["WEBVTT", ""] for s in segs: end = s["start"] + s["duration"] lines.extend([f"{fmt_vtt_ts(s['start'])} --> {fmt_vtt_ts(end)}", s["text"], ""]) return "\n".join(lines) elif fmt == "json": return json.dumps(result, indent=2, ensure_ascii=False) else: return full_text # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- def main(): parser = argparse.ArgumentParser(description="YouTube transcript extractor") parser.add_argument("url", nargs="?", help="YouTube video URL or ID") parser.add_argument("--playlist", help="YouTube playlist URL") parser.add_argument("-f", "--format", default="plain", choices=["plain", "markdown", "timestamped", "srt", "vtt", "json"]) parser.add_argument("-l", "--lang", default="en", help="Language code (default: en)") parser.add_argument("-o", "--output", help="Output file path") args = parser.parse_args() if not args.url and not args.playlist: parser.error("Provide a video URL/ID or --playlist URL") languages = [l.strip() for l in args.lang.split(",")] if args.playlist: videos = resolve_playlist(args.playlist) print(f"Found {len(videos)} videos in playlist", file=sys.stderr) all_output = [] for i, v in enumerate(videos): print(f"[{i+1}/{len(videos)}] {v['title']}", file=sys.stderr) try: result = extract_transcript(v["id"], languages) all_output.append(format_result(result, args.format)) except Exception as e: print(f" FAILED: {e}", file=sys.stderr) all_output.append(f"[Error extracting {v['title']}]: {e}") output = ("\n\n" + "=" * 60 + "\n\n").join(all_output) else: result = extract_transcript(args.url, languages) output = format_result(result, args.format) if args.output: with open(args.output, "w", encoding="utf-8") as f: f.write(output) print(f"Saved to {args.output}", file=sys.stderr) else: print(output) if __name__ == "__main__": main() ===SCRIPT_END=== 4. Make the script executable: chmod +x ~/.claude/skills/youtube-transcript/scripts/yt_transcript.py 5. Install Python dependencies on my machine: pip3 install --user youtube-transcript-api yt-dlp (If you have Homebrew, also run: brew install yt-dlp) 6. Verify by listing the installed files and running a quick test: ls -la ~/.claude/skills/youtube-transcript/ python3 ~/.claude/skills/youtube-transcript/scripts/yt_transcript.py --help When done, tell me the skill is installed and that I can restart Claude Code or start a new session to use it. The trigger phrase is anything like "transcript of [YouTube URL]" or I can invoke it explicitly with /youtube-transcript.
Paste once

Paste this single message into Claude Code. Takes ~10 seconds. After it finishes, restart Claude Code (or start a new session) so the skill loads.

Try it

Start a new Claude Code session and paste this:

Get the transcript of https://www.youtube.com/watch?v=dQw4w9WgXcQ

If the skill is installed correctly, Claude will return the full transcript of the video. If not, the troubleshooting section below covers the usual culprits.

2
How to use it

Once installed, just talk to Claude normally. The skill auto-triggers on YouTube URLs, or invoke it explicitly with /youtube-transcript.

Example prompts

  • "Grab the transcript of https://youtu.be/abc123"
  • "Summarize this video: https://www.youtube.com/watch?v=xyz"
  • "Save the transcript of this video as markdown with timestamps"
  • "Get transcripts for every video in this playlist: https://www.youtube.com/playlist?list=..."
  • "Pull the Spanish captions from this video"

Formats available

You want Format flag
Plain text (best for Claude to analyze) --format plain (default)
Markdown with metadata + timestamps --format markdown
Timestamped plain text --format timestamped
SRT subtitles --format srt
WebVTT subtitles --format vtt
Structured JSON --format json
3
Troubleshooting
"Could not extract video ID from: ..."

The URL is malformed. Double-check it actually points to a YouTube video. Supported formats: youtube.com/watch?v=, youtu.be/, youtube.com/shorts/, youtube.com/embed/, or a bare 11-character video ID.

"All extraction methods failed for ..."

The video genuinely has no captions, or YouTube is rate-limiting your IP. Try a different video to confirm the skill is working; if it's just one video, that video has no transcript available.

"ImportError: No module named youtube_transcript_api"

You skipped the pip install. Run:

pip3 install --user youtube-transcript-api yt-dlp
Skill not found / doesn't auto-trigger

Run ls ~/.claude/skills/youtube-transcript/ — you should see SKILL.md and a scripts/ directory. If they're missing, the install didn't complete. Restart Claude Code after installing so it picks up the new skill. You can also force-invoke it with /youtube-transcript.

Don't have Claude Code yet?

Install it in 5 minutes: aiserviceengine.com/claude-code-install

Made by AI Service Engine

Based on youtube-transcript-api + yt-dlp.