Research a topic on Medium using your own logged-in Medium session — without needing an interactive browser at request time. Extracts your Medium session cookies from Chrome once, then uses a headless browser to search Medium and fetch full article text (including member-only content your account can access).
Built as the engine behind a Claude Code skill
(~/.claude/skills/mediumlm/SKILL.md, installed separately, outside
this repo), but the mediumlm CLI works standalone from any terminal.
uv tool install . # installs the `mediumlm` CLI into ~/.local/bin
python3 -m playwright install chromiumAfter changing the source, refresh with uv tool install --reinstall ..
For a dev/editable install for hacking on the code, use
pip3 install -e ".[dev]".
Requires Python 3.9+ and Chrome, logged into Medium, for the first cookie extraction.
# Extract your Medium session cookies from Chrome (run once, and
# again whenever the session goes stale)
mediumlm cookies extract
# Confirm the stored session still authenticates
mediumlm cookies check
# Search Medium for a topic (Medium currently blocks this for
# headless clients; the command fails with a clear error pointing
# at the fallback — see "How it works" below)
mediumlm search "claude code mcp" --limit 8
# Fetch a specific article's full text using your session
mediumlm fetch "https://medium.com/@author/article-slug-abc123abc123"
# Fetch several articles in one call: the browser launch is shared
# (each article still gets a fresh context — Medium challenges reused
# ones), results come back as a JSON array in input order, and a
# per-URL failure is recorded as {"access": "error", "error": "..."}
# instead of aborting the batch
mediumlm fetch "https://medium.com/@a/first-abc123abc123" \
"https://medium.com/@a/second-def456def456"
# Repeat fetches are served from ~/.mediumlm/cache (marked
# "cached": true in the JSON); only full-access articles are cached,
# so previews and errors always retry. --no-cache forces a refetch.
mediumlm cache list # what's cached: [{url, title, fetched_at}]
mediumlm cache clear --url <url> # drop one entry (omit --url for all)
# If the stored session has expired, fetch automatically re-extracts
# cookies from Chrome once and retries just the expired URLs
# (disable with --no-refresh; a note is printed to stderr when this
# happens — stdout stays pure JSON)Every command prints a single JSON object/array to stdout on success.
On failure, stdout is empty, an error: <message> line goes to
stderr, and the exit code is 1.
fetch's output includes an explicit access: "full" | "preview"
field with an access_reason (blocked, cookies_expired, or
not_member) whenever the article wasn't fully readable — this is
never silently collapsed into a plain success. Batch mode prints a
JSON array (single-URL mode a single object), and the exit code is
1 only when every URL in a batch failed — single-URL failures keep
the error:-on-stderr, empty-stdout contract.
- Cookies are extracted from Chrome's local cookie store
(
browser_cookie3) and stored at~/.mediumlm/cookies.jsonwith0600permissions — this file is a bearer-token-equivalent secret for your Medium session; never commit it or share it. - Fetching an article uses a headless Playwright browser with your cookies injected. A plain HTTP client doesn't work here — Medium's Cloudflare bot detection blocks it; a real (headless) browser context does not trip the same defenses.
- Searching is currently unavailable. A 2026-07-15 finding showed
unauthenticated search working while authenticated search was
blocked; that was superseded on 2026-07-19 — Medium's search-results
GraphQL API now returns 403 to headless-browser XHRs regardless of
whether cookies are sent, and the page renders an error state instead
of results.
mediumlm searchdetects this and raises a clear "search unavailable" error (exit 1) rather than returning junk; it still returns a genuine empty list for real zero-result queries if search ever works again. The practical discovery path in the meantime is an external web search restricted tosite:medium.com, feeding the found URLs tomediumlm fetch(which still works fully with your session). The search code path stays in place sosearchresumes working automatically if Medium unblocks it.
See docs/superpowers/specs/2026-07-14-mediumlm-design.md for the
full design rationale, including the live-verification findings above.
Built for personal research against your own Medium account, at normal single-topic, on-demand volume. Automated access likely falls outside Medium's Terms of Service; this is not a bulk-scraping tool.
- The cookie file is the only secret.
~/.mediumlm/cookies.jsonis written with0600permissions, and the tool refuses to write it anywhere inside a git-tracked directory..gitignorealso blocks any*cookies*.jsonpattern as a second line of defense. Cookie values never appear on stdout/stderr or in error messages — only counts and status make it into output. Automatic refresh (seefetchabove) reuses this same guarded write path (0600, git-tracked-path refusal) and runs at most once per invocation, only when the session itself has expired — never in response to bot-challenge pages. - Dependencies are version-bounded in
pyproject.toml.browser_cookie3gets the tightest pin of the group, since its entire job is decrypting your local browser's cookie store (Keychain access on macOS) — that's not something to let bump automatically; upgrades there should be deliberate. - Fetched article content is untrusted input. It comes from a third party and is parsed with BeautifulSoup, which does not execute JavaScript at parse time (the page's own JS only runs inside Chromium's sandbox during fetch). Anything downstream that consumes the resulting markdown — an AI assistant, your notes — should treat it as data, never as instructions; the companion Claude skill enforces that boundary explicitly.
- The article cache is private, member-only content.
~/.mediumlm/cache/, created0700, stores the full text of member-only articles you've fetched.cache clearwipes it and refuses to delete anything outside its own directory. - Accepted, by design: any process running as your user can read
the cookie file, same as any other local secret under the standard
single-user trust model.
cookies extractneeds access to Chrome's Safe Storage key to work at all. And automated access sits in a gray area of Medium's ToS — see "Scope" above.
python3 -m pytest tests/ -vThe suite is fixture/mock-driven except one test that drives a real local HTTP server to verify cookie injection actually reaches the browser layer.