ytdlp-cookie-keeper
v0.1.0
Published
Keep your yt-dlp YouTube cookies fresh on a headless server: export from a persistent browser profile, validate against real videos, atomically swap — never clobber a working cookie file with a broken one.
Maintainers
Readme
ytdlp-cookie-keeper
Keep your yt-dlp YouTube cookies fresh on a headless server — export from a persistent browser profile, validate against real videos, swap atomically. Never clobber a working cookie file with a broken one.
The problem
Running yt-dlp on a server, you've probably seen this:
ERROR: Sign in to confirm you're not a bot. Use --cookies-from-browser or --cookies ...So you export cookies from a logged-in browser and pass --cookies cookies.txt. It works — until YouTube rotates the session and the file goes stale, usually within days. The obvious fix, re-exporting on a cron with --cookies-from-browser pointed at a browser profile on the server, has a nasty failure mode: the moment the browser session dies, the export silently produces logged-out cookies and overwrites the working file you had. Playback breaks at the worst possible time and you don't find out until it does.
ytdlp-cookie-keeper closes that gap. Every refresh cycle:
- Exports cookies from a persistent browser profile into a temp candidate file (
yt-dlp --cookies-from-browser). - Validates the candidate end-to-end: yt-dlp must successfully extract real watch URLs using only the candidate.
- Swaps the live file in place (same path, same inode,
0600perms) — only after validation passes.
A candidate that fails export or validation is discarded and your previous cookie file stays untouched. A stale-but-working file beats a fresh-but-broken one.
Extras you'd otherwise reimplement: a cooldown window so error-triggered refreshes can't stampede, in-flight deduplication so concurrent triggers join one refresh, and log/error redaction so cookie values and signed CDN URLs never leak into your logs.
Requirements
- Node.js ≥ 20 (or Bun)
- A recent yt-dlp on PATH (YouTube extraction increasingly needs a JS runtime — install
yt-dlp[default]via pip, or have Deno/Node available for its challenge solver) - A browser installed on the server with a logged-in YouTube session in a persistent profile (see setup)
CLI
npm install -g ytdlp-cookie-keeper # or: npx ytdlp-cookie-keeper ...One-shot refresh (cron-friendly)
ytdlp-cookie-keeper refresh \
--cookies /srv/secrets/youtube_cookies.txt \
--browser chrome+basictext:/srv/youtube-browser-profileExit 0 on success, 1 on failure. The --browser value is passed straight to yt-dlp's --cookies-from-browser — the +basictext selects Chrome's plaintext keyring, typical for a dedicated headless-server profile.
Daemon
ytdlp-cookie-keeper daemon \
--cookies /srv/secrets/youtube_cookies.txt \
--browser chrome+basictext:/srv/youtube-browser-profile \
--interval 6h --refresh-on-startFreshness check (alerting)
The refresh loop only rewrites the file on success — so if the browser session dies, failures are silent and the file just quietly ages. Alert on that from anywhere (cron, CI, a monitoring box):
ytdlp-cookie-keeper check --cookies /srv/secrets/youtube_cookies.txt --max-age 13h| Exit code | Meaning |
|---|---|
| 0 | fresh |
| 2 | file missing |
| 3 | stale — refresh loop is failing; the browser profile likely needs a re-login |
A 13h threshold on a 6h interval tolerates one missed cycle without flapping.
Durations everywhere accept 90s, 5m, 6h; bare numbers mean seconds.
Library
import { CookieKeeper } from 'ytdlp-cookie-keeper';
const keeper = new CookieKeeper({
cookiesFile: '/srv/secrets/youtube_cookies.txt',
browserSpec: 'chrome+basictext:/srv/youtube-browser-profile',
refreshIntervalMs: 6 * 60 * 60 * 1000,
});
keeper.on('refresh:success', (e) => metrics.gauge('cookie_last_refresh', e.timestampMs / 1000));
keeper.on('refresh:failure', (e) => logger.warn('cookie refresh failed', { error: e.error }));
keeper.start(); // background loop
// Or trigger on demand, e.g. when playback hits a 403:
const result = await keeper.refreshNow({ reason: 'playback-403' });Concurrent refreshNow() calls join the in-flight refresh; calls inside the cooldown window (default 5m) are skipped unless { force: true }. Failure messages are redacted (no URLs, no cookie values) and safe to log.
Setting up the persistent browser profile
The keeper refreshes cookies from a browser profile; keeping that profile's Google session alive is the part you own. A recipe for a Linux VPS:
# 1. Virtual display + Chrome with a dedicated profile dir
apt install xvfb x11vnc chromium xdotool
Xvfb :94 -screen 0 1280x800x24 &
DISPLAY=:94 chromium --user-data-dir=/srv/youtube-browser-profile \
--password-store=basic --no-first-run &
# 2. Log in interactively, once, over VNC
x11vnc -display :94 -localhost -once
# tunnel: ssh -L 5900:localhost:5900 you@server, then VNC to localhost:5900
# and sign in to YouTube in the Chrome window
# 3. Point the keeper at the profile
ytdlp-cookie-keeper refresh \
--cookies /srv/secrets/youtube_cookies.txt \
--browser chrome+basictext:/srv/youtube-browser-profileTips that make the session last:
- Use a throwaway Google account, not your own — automated-looking activity can trip account security.
- Keep Chrome running (or restart it periodically) so its background token refresh keeps the session warm; a profile that's never opened goes stale faster.
--password-store=basicat first launch matches the+basictextkeyring in the browser spec — cookies stay decryptable without a desktop keyring.- When Google eventually forces a re-login (weeks to months), log in manually over VNC again. Don't script the login: Google actively detects automated sign-ins, and CAPTCHA/2FA challenges can't be bypassed — a typed-credentials script fails exactly when you need it and puts the account at risk.
Recipes
systemd
# /etc/systemd/system/cookie-keeper.service
[Unit]
Description=Keep yt-dlp YouTube cookies fresh
After=network-online.target
[Service]
ExecStart=/usr/bin/ytdlp-cookie-keeper daemon \
--cookies /srv/secrets/youtube_cookies.txt \
--browser chrome+basictext:/srv/youtube-browser-profile \
--interval 6h --refresh-on-start
Restart=on-failure
User=ytdlp
[Install]
WantedBy=multi-user.targetGitHub Actions staleness alert
name: Cookie Health
on:
schedule: [{ cron: '0 */6 * * *' }]
workflow_dispatch:
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Check cookie freshness on the server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USER }}
key: ${{ secrets.SSH_KEY }}
script: npx ytdlp-cookie-keeper check --cookies /srv/secrets/youtube_cookies.txt --max-age 13h
- name: Alert on stale cookies
if: failure()
run: |
curl -s -X POST "${{ secrets.WEBHOOK_URL }}" -H 'Content-Type: application/json' \
-d '{"text":"YouTube cookies are stale — the browser profile likely needs a manual re-login."}'Docker
Run the daemon as a sidecar sharing a secrets/ volume with whatever consumes the cookie file; mount the browser profile read-only into the sidecar. The consumer only ever sees a validated file.
Scope and non-goals
- No login automation. This tool assumes a working browser session and keeps cookies flowing from it; establishing the session is a manual, interactive step by design (see above).
- Not a bypass. If YouTube challenges the account, the validation step fails closed and your previous cookie file is preserved — nothing here evades bot detection; it just stops session rot from breaking you silently.
- YouTube-flavored defaults, but the export→validate→swap pattern is site-agnostic: point
--validate-urlat any site yt-dlp supports.
Provenance
Extracted from FM-Hachimi, a Discord music bot that has run this refresh loop unattended in production. MIT.
