AgentStack
SKILL verified MIT Self-run

Download Xhs Videos

skill-yijiaduan-download-xhs-videos-download-xhs-videos · by YijiaDuan

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,…

No reviews yet
0 installs
2 views
0.0% view→install

Install

$ agentstack add skill-yijiaduan-download-xhs-videos-download-xhs-videos

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Download Xhs Videos? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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.
  1. **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.
  1. 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.
  1. 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:

  1. 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.
  1. 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.
  1. 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 (h264h265av1h266), 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):

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.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.