Skip to content

Repository files navigation

auto-medium

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.

Why this project

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.

Requirements

  • 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

Install

One command with pipx (recommended):

pipx install auto-medium
auto-medium init --account demo

Or with uv:

uv tool install auto-medium
auto-medium init --account demo

auto-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.

From source (contributors)

git clone https://github.com/xtea/auto-medium
cd auto-medium
uv sync
uv run auto-medium init --account demo

Configure an account

Edit config/<account>.yaml. The important fields:

  • handle — the part after @ in your Medium URL (display only)
  • user_agent, viewport, locale, timezonematch 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.

Authenticate

Two paths; pick whichever you prefer.

Option A — Headed manual login (simplest)

auto-medium login --account demo

A 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.

Option B — Import cookies from your real browser

If you already have a logged-in Medium tab in Chrome:

  1. Install Cookie-Editor (browser extension).

  2. Enable HttpOnly cookies in the extension settings — Medium's auth cookies are HttpOnly and the extension hides them by default.

  3. Open medium.com, click the extension, "Export" → "Export as JSON" → save to medium-cookies.json.

  4. Run:

    auto-medium import-cookies ./medium-cookies.json --account demo

The tool rejects the import if the required Medium auth cookies are missing.

Verify

auto-medium doctor --account demo

Should print OK: <handle> session is valid.

Publish a story

Layout

content/
└── my-story/
    ├── post.yaml
    ├── body.md
    └── media/
        └── cover.jpg          # optional

post.yaml schema

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 total

Validation 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

One-shot publish

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.

Scheduled / queued publish

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>&1

The queue stores state in sessions/queue.db (SQLite) with statuses: queued | running | succeeded | failed | paused. Use auto-medium list to inspect.

How the Playwright flow works

  1. Launch Chrome via Patchright (Chromium with CDP/webdriver leaks patched at the binary level). Vanilla playwright is detected by Medium's fingerprinting stack — don't use it.
  2. Load sessions/<account>.json as the Playwright storage_state.
  3. Navigate to medium.com/me, confirm we land on the authenticated feed.
  4. Open medium.com/new-story → fill title → subtitle → type the markdown body into the ProseMirror editor → optionally upload a cover image.
  5. Wait for the autosave indicator → click Publish → type tags into the modal → click Publish-now.
  6. Confirm by polling medium.com/@<handle>/latest until 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.

Pacing & safety

Built-in guardrails, tunable in config/<account>.yaml:

  • max_posts_per_day daily cap (enforced by the queue)
  • min/max_step_delay_seconds randomized delays between UI actions
  • pre_run_idle_seconds_* scroll/dwell on /me before 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.

Known limitations

  • 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.py is a hypothesis. Run auto-medium doctor after 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.

Non-goals (for now)

  • Web UI / dashboard (CLI + YAML only)
  • Long-running daemon (cron-friendly invocation instead)
  • Engagement automation (claps, follows, comments)

License

MIT.

About

Open-source medium.com publisher driven by Patchright and imported cookies.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages