Open-source medium.com publisher that drives the web editor with Playwright and your own imported cookies. Write a story in markdown, configure your account once, publish.
Status: Alpha. Personal-account use only. Browser automation falls outside Medium's published terms — use a scratch account, conservative pacing, and a residential IP that matches where you got the cookies.
Medium deprecated their integration-token API on 2026-01-01 (no new tokens issued). Their official OAuth tooling is also gone — for a single operator with a personal account, browser automation against the web editor is now the only durable path. auto-medium automates the website you already use, with the session you already have.
Supported in v1 (April 2026):
- Stories on your personal profile (
medium.com/@<handle>) - Markdown body with auto-converted formatting (headings, lists, links, code blocks)
- Optional cover image
- Up to 5 topic tags
Not yet supported (v2): publications (medium.com/<publication>), scheduled publishing, "import a story" from a URL, HTML body source.
- Python 3.11+
- macOS or Linux (tested on macOS)
- A real Chrome/Chromium you can log in from on the same network you'll run the bot on
- (Recommended) a residential proxy if you plan to run this on a different machine from where you logged in
One command with pipx (recommended):
pipx install auto-medium
auto-medium init --account demoOr with uv:
uv tool install auto-medium
auto-medium init --account demoauto-medium init installs the patched Chrome channel Patchright needs and scaffolds a working directory in .:
./config/demo.yaml # account config from the shipped template
./content/example-post/ # sample post.yaml + body.md
./sessions/ # session files (gitignored)
./.gitignore # appended with auto-medium entries
It is safe to re-run — existing files are preserved.
git clone https://github.com/xtea/auto-medium
cd auto-medium
uv sync
uv run auto-medium init --account demoEdit config/<account>.yaml. The important fields:
handle— the part after@in your Medium URL (display only)user_agent,viewport,locale,timezone— match the browser you'll log in from. Drift here is the #1 cause of an invalidated session.pacing.max_posts_per_day— start at 1–2. Medium's leaked 2025 limits are 3/day for non-members, 15/day for members.
Two paths; pick whichever you prefer.
auto-medium login --account demoA Chrome window opens. Sign in by hand (Medium uses email magic link or OAuth — there's no password form). When the authenticated feed appears, the session is saved to sessions/demo.json.
If you already have a logged-in Medium tab in Chrome:
-
Install Cookie-Editor (browser extension).
-
Enable HttpOnly cookies in the extension settings — Medium's auth cookies are HttpOnly and the extension hides them by default.
-
Open
medium.com, click the extension, "Export" → "Export as JSON" → save tomedium-cookies.json. -
Run:
auto-medium import-cookies ./medium-cookies.json --account demo
The tool rejects the import if the required Medium auth cookies are missing.
auto-medium doctor --account demoShould print OK: <handle> session is valid.
content/
└── my-story/
├── post.yaml
├── body.md
└── media/
└── cover.jpg # optional
type: story
title: "Your headline (≤100 chars recommended)"
subtitle: "Optional one-liner under the title"
body_path: ./body.md
cover_image: ./media/cover.jpg # optional, ≤25 MB
tags:
- automation
- python # ≤5 tags totalValidation runs before any browser work:
| Field | Rule |
|---|---|
title |
non-empty, ≤100 chars (recommended) |
subtitle |
≤140 chars |
body_path |
exists, non-empty, ≤100k chars |
cover_image |
exists, ≤25 MB, .jpg/.jpeg/.png/.gif |
tags |
≤5 items, ≤25 chars each |
auto-medium publish content/my-story --account demo --dry-run # safe first run
auto-medium publish content/my-story --account demo # actually publishes--dry-run walks the full editor flow and stops before clicking Publish-now — useful when patching selectors.
Medium's native schedule is not supported in v1; instead, drop posts into the content directory and run auto-medium queue from cron / launchd / systemd-timer:
*/5 * * * * cd /path/to/auto-medium && auto-medium queue --account demo >> sessions/queue.log 2>&1The queue stores state in sessions/queue.db (SQLite) with statuses: queued | running | succeeded | failed | paused. Use auto-medium list to inspect.
- Launch Chrome via Patchright (Chromium with CDP/webdriver leaks patched at the binary level). Vanilla
playwrightis detected by Medium's fingerprinting stack — don't use it. - Load
sessions/<account>.jsonas the Playwrightstorage_state. - Navigate to
medium.com/me, confirm we land on the authenticated feed. - Open
medium.com/new-story→ fill title → subtitle → type the markdown body into the ProseMirror editor → optionally upload a cover image. - Wait for the autosave indicator → click Publish → type tags into the modal → click Publish-now.
- Confirm by polling
medium.com/@<handle>/latestuntil the most-recent slug differs from what we recorded before publishing.
All selectors are in src/auto_medium/publisher/selectors.py — when Medium changes the UI, that is the file to patch.
Built-in guardrails, tunable in config/<account>.yaml:
max_posts_per_daydaily cap (enforced by the queue)min/max_step_delay_secondsrandomized delays between UI actionspre_run_idle_seconds_*scroll/dwell on/mebefore opening the editor
On redirect to /m/signin / /suspended, the runner pauses the job and records the reason. Re-authenticate with auto-medium login and retry.
- Block-level markdown only. Medium's editor converts block markdown shortcuts (
#,##,*,1.,>,```) at the start of a fresh paragraph, but does NOT convert inline syntax.**bold**,*italic*,[link text](url), and`code`will appear as literal text — apply that formatting manually after publish, or skip inline syntax in the source. - Selectors rot. Medium ships UI changes periodically. Expect occasional patches to
selectors.py. - Cookie set is provisional. Medium does not publicly document its auth cookies; the required-cookie set in
auth/cookie_import.pyis a hypothesis. Runauto-medium doctorafter a successful login and tighten if needed. - Mid-run challenges. If Medium invalidates the session mid-publish, the tool pauses; manual re-login is required.
- Shared IP. Using cookies captured from residence A while running the bot on residence B's IP is the single most reliable way to get challenged.
- ToS risk. Browser-driven automation falls outside Medium's published terms. Use a scratch account.
- Web UI / dashboard (CLI + YAML only)
- Long-running daemon (cron-friendly invocation instead)
- Engagement automation (claps, follows, comments)
MIT.