Pulls your LeetCode submission history and writes each accepted solution into this repository, grouped by the problem's primary topic tag:
two-pointers/
0015-3sum/
README.md <- problem link, difficulty, tags, per-language stats
solution.py
solution.cpp
One file per problem per language, always the most recent accepted version.
LeetCode has no public API, but the site's own history endpoint
(GET /api/submissions/) returns your submissions newest-first including the full
source code. The sync pages through it with your session cookie, keeps the first
accepted hit for each (problem, language) pair, and asks the GraphQL endpoint for
each problem's number, difficulty, and topic tags.
State lives in three files so repeat runs stay cheap:
| File | Purpose |
|---|---|
.sync_state.json |
Newest submission already seen, so later runs stop there instead of re-walking history. |
.problem_cache.json |
Problem metadata, fetched once per problem, ever. |
.solution_index.json |
Everything in the repo, so the READMEs can be regenerated from an incremental run. |
-
Install the one dependency:
pip install -r requirements.txt -
Copy
.env.exampleto.envand paste in your cookies (see the comments in that file for where to find them in devtools)..envis gitignored. -
Smoke test, which reads 40 submissions and writes nothing:
python -m leetcode_sync --dry-run --max-submissions 40 -
Backfill everything:
python -m leetcode_sync --fullA few thousand submissions takes a few minutes; the endpoint returns 20 per request and the sync pauses between pages to stay polite.
python -m leetcode_sync
Picks up where the last run stopped and commits. Add --push if you've set a remote.
| Flag | Effect |
|---|---|
--full |
Ignore saved state and walk the entire history. |
--dry-run |
Print what would be written, change nothing. |
--max-submissions N |
Stop after N submissions. |
--delay SECONDS |
Pause between pages (default 1.0). Raise if rate-limited. |
--no-commit |
Write files but don't commit. |
--push |
Push after committing. |
NeetCode's judge is separate from LeetCode's, so problems solved there never
appear in the LeetCode submission history. --with-neetcode refreshes a local
clone of the NeetCode GitHub Sync repo (NEETCODE_REPO in config.py, cloned
to the gitignored _sync/.neetcode/) and folds anything new into this tree:
python -m leetcode_sync --with-neetcode --push
The scheduled task does this by default. Imported solutions are marked
Accepted on NeetCode in the file header and carry a Source column in their
problem README. A problem already solved on LeetCode is left alone, and a later
LeetCode submission always supersedes an import.
NeetCode kept its original slugs after renaming its problems to match LeetCode,
so count-paths is Unique Paths and pow-x-n is powx-n. Those are resolved
through the ALIASES table in neetcode.py; if an import reports an unresolved
slug, add it there.
To import from a clone somewhere else instead: --import-neetcode PATH.
run_sync.ps1 wraps an incremental sync and appends to logs/. Register it as a
daily Windows scheduled task, launched through conhost.exe --headless so it
never opens a window:
schtasks /create /tn "LeetCode Sync" /sc daily /st 21:00 /tr "conhost.exe --headless powershell.exe -NoProfile -ExecutionPolicy Bypass -File \"%USERPROFILE%\LeetCode Submissions\_sync\run_sync.ps1\" -Push"
Drop -Push if no remote is configured. Then, in the task's settings, turn on
"Run task as soon as possible after a scheduled start is missed", so a night the
machine was asleep is caught up when it wakes.
Why headless. A visible console window can be closed, and closing it kills
the run mid-sync with exit 0xC000013A. That is how two catch-up runs died the
moment the machine woke. The cost is that conhost does not pass the exit code
back, so Task Scheduler reports every run as a success. The log is the record
instead: every entry ends with exit code N.
| Exit code | Meaning |
|---|---|
| 0 | synced (or nothing new) |
| 3 | session cookie expired: run --set-cookies |
| 4 | no network: the next run picks up where this one stopped |
| 5 | another sync was already running, so this one stepped aside |
| 6 | LeetCode's Cloudflare check blocked every endpoint, or a --full run: try again later |
What protects a run:
- A lock (
.sync.lock) lets only one sync touch the repo at a time. A manual run that overlaps the scheduled one exits with code 5 rather than racing it. A lock left behind by a killed run is ignored once its process is gone. - Catch-up runs wait. A run starting outside the 21:00 window was triggered by the machine waking, so it pauses two minutes and then waits for DNS before syncing.
- The log streams. Each line is written as it is printed, so a run cut short still shows how far it got.
- Commits are named after what they contain, counted from the staged files. Work left uncommitted by a killed run is committed by the next run under its real name, for example "Sync 3 LeetCode solutions".
- A run that cannot reach leetcode.com retries with backoff and then exits with code 4 and a one-line message. DNS is often not ready when a scheduled run fires just after boot, so this rides that out rather than failing the sync.
- LeetCode sometimes puts the submission-history endpoint behind a Cloudflare
bot check, which a script cannot pass. An incremental run then reads your
recent accepted submissions over GraphQL instead, which reaches back about 20
submissions; a daily run never needs more.
--fullcannot fall back, and exits with code 6 until the check is lifted. - The session cookie expires every week or two. When that happens the sync exits
with
auth errorand you paste a fresh cookie into.env; nothing else breaks, and the next run resumes from where it stopped. - Premium-only problems sync like any other; if LeetCode won't return metadata for a slug, the sync reports it under "Skipped" and moves on rather than failing.
- Tests:
python -m pytest tests(no network; the client is faked).
githooks/pre-commit refuses to commit anything that looks like a live LeetCode
cookie, and refuses to stage _sync/.env at all. It is already active in this
clone; a fresh clone has to opt in, because git does not run hooks it was handed
by a remote:
git config core.hooksPath _sync/githooks
To bypass it deliberately: git commit --no-verify.