# Download Xhs Videos

> Download all of a Xiaohongshu (RedNote / 小红书) creator's videos to a local folder by driving the Claude-in-Chrome extension. Use when the user wants to batch-download / archive / 下载 / 抓取 a 小红书 (xiaohongshu / RedNote / xhs) 博主 / 用户 / up主 的视频 / 笔记, mirror a profile's video notes, or save someone's xhs videos for offline viewing. Triggers: 下载小红书视频, 抓小红书博主视频, 把这个博主的视频都下下来, download xiaohongshu videos,…

- **Type:** Skill
- **Install:** `agentstack add skill-yijiaduan-download-xhs-videos-download-xhs-videos`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [YijiaDuan](https://agentstack.voostack.com/s/yijiaduan)
- **Installs:** 0
- **Category:** [Content & Media](https://agentstack.voostack.com/c/content-and-media)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [YijiaDuan](https://github.com/YijiaDuan)
- **Source:** https://github.com/YijiaDuan/download-xhs-videos

## Install

```sh
agentstack add skill-yijiaduan-download-xhs-videos-download-xhs-videos
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# download-xhs-videos — batch-download a 小红书 creator's videos

Xiaohongshu (RedNote) fights scrapers hard, and the two "obvious" approaches both fail: `yt-dlp` support is flaky, and a plain headless scraper hits signed-request (`x-s`/`x-t`) walls. The path that actually works is **driving the logged-in browser** through the Claude-in-Chrome extension, but even that has four non-obvious traps that will silently eat your downloads. This skill encodes the route that survives all four.

The one-line shape of the working pipeline:

> **Real mouse-click to open each note → read the video URL out of the page's `__INITIAL_STATE__` → hand the URL to `curl` via the clipboard → download outside the browser.** Never let the browser do the download; never let the video URL pass through the agent's own context.

## Before you start — preconditions

This skill is **macOS + Claude-in-Chrome only** (it relies on `pbpaste` and on the extension's trusted clicks). Confirm all of these or stop and ask:

1. The **Claude-in-Chrome extension is connected** to this session (`list_connected_browsers` returns a device; if empty, `switch_browser` and have the user click *Connect*).
2. The user is **logged into their 小红书 account** in that Chrome.
3. You have the **creator's profile URL** (the `…/user/profile/?xsec_token=…` link). The token in it is what authorizes the profile load.
4. `curl` and `pbpaste` exist (they do, on macOS).
5. A **target folder**, default `~/Documents/xhs-/`.

### Ethics gate — do this, don't skip it

Open the profile and **read the creator's bio first.** Many creators write 「原创作品,禁止搬运和商用」 (original work, no reposting/commercial use). If so, confirm with the user that this is **personal/offline viewing only**. Downloading public videos for yourself is a defensible grey area; re-publishing or monetizing someone's flagged original work is not — refuse that. State what the bio says and get a clear yes before mass-downloading.

## Why the naive approaches fail (read this or you'll waste an hour)

These are the landmines, in the order you'll hit them:

1. **`yt-dlp` / direct API scraping** — XHS web requests need a signed `x-s`/`x-t` header generated by their obfuscated JS. Replicating it is fragile and breaks often. Don't go down this road; drive the real browser instead.

2. **The SPA only opens notes on a *real* card click.** Setting `location.href` to a note URL, or building your own `` and clicking it, gets **bounced back to `/explore`** by XHS's router. `mcp navigate` to a note URL bounces too. Only a genuine click on the actual feed card (via the extension's `computer left_click`) triggers the in-app navigation that loads the note — *with* its video stream.

3. **Chrome silently blocks automation-triggered downloads.** A blob download via `a.click()` from injected JS needs a *user-activation* gesture that injected code doesn't carry — and, critically, **the extension's synthetic clicks don't grant download activation either** (they fire DOM `click` handlers, so `execCommand('copy')` works, but the download is dropped with no error and no file). Verify this yourself once if you doubt it: a `data:` text download won't land either. **Conclusion: do not download through the browser at all.** Download with `curl`.

4. **The agent harness blocks query-string data (the `xsec_token`) from entering your context.** If your in-page JS *returns* the video URL to you, the tool result is `[BLOCKED: Cookie/query string data]`. So you can't read the URL, build a `curl` command with it, and run it — the URL would pass through your context. **Route the URL clipboard → `curl` so it never touches your context:** in-page JS writes the URL to the clipboard on a trusted click; the shell reads it with `$(pbpaste)` and never prints it.

Two more that bite during the loop:

5. **Don't `fetch()` the video inside the page.** Chrome caps ~6 connections per host; a few hung/leaked fetches to the CDN exhaust the pool and every later fetch hangs forever (you'll see the JS promise stay `pending` and CDP time out). `curl` sidesteps this entirely.

6. **The profile grid is a masonry layout — DOM order ≠ visual columns.** Don't compute click coordinates from a column guess. `scrollIntoView({block:'center'})` the target card, then read its `getBoundingClientRect()` and click the returned center.

7. **The CDN URL is `http://`, not `https://`, and needs a `Referer`.** `curl` it with `-H "Referer: https://www.xiaohongshu.com/"` and a normal User-Agent.

## The recipe

### Step 0 — Connect and open the profile

Connect the browser, open a fresh tab (`tabs_context_mcp createIfEmpty:true`), `navigate` to the profile URL, and `get_page_text` to read the bio (ethics gate) and eyeball the note titles.

### Step 1 — Load every note and count

Scroll the profile to the bottom in-page until the note count stabilizes, then count distinct note ids. Note ids appear in anchors as `/user/profile//?xsec_token=…`. See `scripts/01-load-and-count.js`. Report the real total to the user (a "video creator" may have far fewer or more than they think; "first 50" might be "all 30").

### Step 2 — Per-note loop (3 calls per note)

For note index `N` (0-based), in DOM order:

**Call A — locate (one `browser_batch`):** `navigate` back to the profile, then run JS that rebuilds the ordered card list, `scrollIntoView` card `N`, and returns its **screenshot-space** center coords. See `scripts/02-locate-card.js`. (Scale viewport coords to screenshot pixels — the script does this.)

**Call B — open + extract + copy (one `browser_batch`):**
1. `computer left_click` the coords from Call A → opens the note (real click, so the SPA loads it).
2. `javascript_tool` runs `scripts/03-extract-and-copy.js`: it polls `__INITIAL_STATE__.note.noteDetailMap[].note.video.media.stream`, picks the best codec (`h264` → `h265` → `av1` → `h266`), takes `masterUrl` (fallback `backupUrls[0]`), stashes it on `window.__urlForCopy`, and builds a fixed-position copy button whose click copies that URL to the clipboard via `execCommand('copy')`.
3. `computer left_click [150,155]` — a **trusted** click on the copy button → URL is now on the clipboard.

**Call C — download (Bash):**
```bash
cd ~/Documents/xhs-
URL="$(pbpaste)"
case "$URL" in http*://*xhscdn.com/*) ;; *) echo "BAD clipboard"; exit 1;; esac
curl -sS -L -A "Mozilla/5.0" -H "Referer: https://www.xiaohongshu.com/" \
  -o "NN_.mp4" "$URL"
file "NN_.mp4" | grep -q 'ISO Media' || echo "NOT_MP4"
```
The `case` guard catches the rare "no-video" note (clipboard wouldn't be a CDN url). The URL is never echoed, so it never enters your context.

Name files `NN_.mp4` using the titles you already read in Step 0/1 — you do **not** need to extract the title per note.

### Step 3 — Verify

After the loop, `ls`/`file` every `.mp4`: count matches the total, none are  {
  const ta = document.createElement('textarea');
  ta.value = window.__urlForCopy;
  document.body.appendChild(ta); ta.select();
  try { document.execCommand('copy'); } catch (e) {}
  ta.remove();
});
```

## Things that will bite you

- **Navigating to a note URL (any way except a real card click) bounces to `/explore`.** Re-open from the profile by clicking the card.
- **A returned URL containing `xsec_token` gets `[BLOCKED]`.** Only ever move the URL via the clipboard; never return it from JS, never print it in a shell command.
- **Browser downloads silently vanish** when "Ask where to save each file" is on (pending Save dialog the extension can't touch) *and* when it's off (no user-activation). Stop fighting it — use `curl`.
- **In-page `fetch()` of the video hangs after a few notes** (connection-pool exhaustion). `curl` only.
- **Masonry layout**: trust `getBoundingClientRect()`, not column math. The card vertical center lands near viewport-center after `scrollIntoView({block:'center'})`.
- **Viewport vs screenshot pixels differ ~2%.** Scale rect coords by `1496/innerWidth` and `812/innerHeight` (the locate script does this) or clicks drift on the rightmost column.
- **`back` navigation re-renders the DOM**, so refs/ids from a prior `read_page` are stale. Rebuild the card list each loop (the locate script does).
- **Quality**: `masterUrl` is the highest stream of the chosen codec; XHS source bitrate is modest, so some clips being 1–2 MB is normal, not a bug.

## Files in this skill

- `SKILL.md` — this playbook.
- `scripts/01-load-and-count.js` — scroll the profile, count distinct video notes.
- `scripts/02-locate-card.js` — rebuild ordered card list, scroll to card N, return click coords.
- `scripts/03-extract-and-copy.js` — read the note's video URL, build the clipboard-copy button.
- `scripts/download.sh` — the clipboard→`curl` download with the CDN guard.

## Dependencies (explicit)

- Claude-in-Chrome extension connected, signed into 小红书.
- macOS (`pbpaste`, `curl`).
- The agent's browser MCP tools: `navigate`, `tabs_context_mcp`, `javascript_tool`, `computer` (clicks), `browser_batch`, `read_page`/`get_page_text`.

## Related skills

- [`waitlist-farmer`](../waitlist-farmer/SKILL.md) — same "drive a real logged-in browser through a repetitive flow" muscle, different domain.

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [YijiaDuan](https://github.com/YijiaDuan)
- **Source:** [YijiaDuan/download-xhs-videos](https://github.com/YijiaDuan/download-xhs-videos)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-yijiaduan-download-xhs-videos-download-xhs-videos
- Seller: https://agentstack.voostack.com/s/yijiaduan
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
