Automated daily .puz crossword puzzle downloader. A companion to xword-dl that orchestrates a full daily fetch across 20+ sources and fills gaps where xword-dl can't reach.
fetch-crosswords.sh— Runs hourly (via cron, launchd, etc.), downloads puzzles from all sources into monthly folders (~/Crosswords/2026-03/). Skips any source whose puzzle already exists, so repeated runs only fetch what's still missing.fetch-extras.py— Downloads puzzles that xword-dl can't reliably handle: Universal (API user-agent workaround), WSJ, and Universal Sunday.backfill.sh— Downloads historical puzzles for a date range. Skips anything already downloaded.rename-library.py— Bulk-renames.puzfiles to a consistent human-readable format.
| Source | Code | Handler | Notes |
|---|---|---|---|
| New York Times | nyt |
xword-dl | Requires NYT subscription |
| NY Times Mini | nytm |
xword-dl | Requires NYT subscription |
| NY Times Midi | nytd |
xword-dl | Requires NYT subscription; needs PR #319 |
| NY Times Variety | nytv |
xword-dl | Requires NYT subscription |
| Los Angeles Times | lat |
xword-dl | Free |
| LA Times Mini | latm |
xword-dl | Free |
| USA Today | usa |
xword-dl | Free |
| Universal | — | fetch-extras.py | xword-dl blocked by user-agent filter |
| Universal Sunday | — | fetch-extras.py | Not in xword-dl; via herbach.dnsalias.com |
| Wall Street Journal | — | fetch-extras.py | Not in xword-dl; via herbach.dnsalias.com |
| Newsday | nd |
xword-dl | Free |
| Washington Post | wp |
xword-dl | Free |
| Daily Pop | pop |
xword-dl | Free |
| Puzzmo | pzm |
xword-dl | Free |
| Puzzmo Big | pzmb |
xword-dl | Free (weekly) |
| The New Yorker | tny |
xword-dl | Free |
| New Yorker Mini | tnym |
xword-dl | Free |
| The Atlantic | atl |
xword-dl | Free |
| Billboard | bill |
xword-dl | Free (latest only) |
| Vox | vox |
xword-dl | Free (latest only) |
| Vulture | vult |
xword-dl | Free |
| The Daily Beast | db |
xword-dl | Free (latest only) |
| The Walrus | wal |
xword-dl | Free (latest only) |
| Crossword Club | club |
xword-dl | Free |
- uv — Python package manager
- xword-dl — Install from git for the latest fixes:
uv tool install --python 3.12 xword-dl --from 'git+https://github.com/thisisparker/xword-dl.git' - NYT Games subscription (optional, for NYT puzzles)
-
Clone the repo:
git clone https://github.com/clodpated/crossword-fetch.git cd crossword-fetch -
Configure NYT authentication (optional):
xword-dl nyt --authenticate
If that doesn't work, you can manually grab the cookie from your browser:
- Log in to nytimes.com/crosswords with your NYT subscription
- Open Developer Tools (
F12orCmd+Opt+I/Ctrl+Shift+I) - Go to the Application tab (Chrome/Edge) or Storage tab (Firefox)
- Under Cookies, select
https://www.nytimes.com - Find the cookie named
NYT-Sand copy its Value - Add it to
~/.config/xword-dl/xword-dl.yaml:nyt: NYT_S: "your-cookie-value"
-
Test it:
./fetch-crosswords.sh
Check
~/Crosswords/for today's puzzles. The script findsfetch-extras.pyrelative to its own location, so both scripts must stay in the same directory. -
Schedule it — see Scheduling below for cron, launchd, or systemd setup.
The script is designed to run hourly. Each run skips sources whose puzzles have already been downloaded, so repeated runs only attempt what's still missing. This means you catch puzzles regardless of when each publisher drops theirs throughout the day.
The simplest option. Open your crontab:
crontab -eAdd this line (update the path):
0 * * * * /path/to/crossword-fetch/fetch-crosswords.sh
This runs at the top of every hour. Output goes to ~/Crosswords/logs/YYYY-MM-DD.log.
Tip: If your machine might be off at midnight, hourly is better than a single daily run. Already-downloaded sources are skipped instantly, so extra runs are cheap.
Better than cron on a Mac — launchd fires missed jobs after waking from sleep.
-
Edit
scheduling/com.crosswords.fetch.plistand replace/Users/YOURUSER/with your actual home directory path in all three places. -
Copy it into place and load it:
cp scheduling/com.crosswords.fetch.plist ~/Library/LaunchAgents/ launchctl load ~/Library/LaunchAgents/com.crosswords.fetch.plist
-
Verify it's loaded:
launchctl list | grep crosswords
To unload later: launchctl unload ~/Library/LaunchAgents/com.crosswords.fetch.plist
Create two files:
~/.config/systemd/user/crossword-fetch.service
[Unit]
Description=Fetch daily crossword puzzles
[Service]
Type=oneshot
ExecStart=/path/to/crossword-fetch/fetch-crosswords.sh~/.config/systemd/user/crossword-fetch.timer
[Unit]
Description=Run crossword-fetch hourly
[Timer]
OnCalendar=hourly
Persistent=true
[Install]
WantedBy=timers.targetEnable and start:
systemctl --user daemon-reload
systemctl --user enable --now crossword-fetch.timerCheck status:
systemctl --user status crossword-fetch.timer
journalctl --user -u crossword-fetch.service -n 20Persistent=true means systemd will run a missed job after boot, similar to launchd on macOS.
Puzzles are organized into monthly folders with the date baked into each filename:
~/Crosswords/
├── 2026-01/
│ ├── 2026-01-01 - NY Times - Thursday, January 01, 2026 - Topher Booth.puz
│ ├── 2026-01-01 - Puzzmo - Blast Off! - Willa.puz
│ ├── 2026-01-01 - USA Today - Untitled - Unlisted.puz
│ └── ...
├── 2026-02/
├── 2026-03/
└── logs/
Filename format: YYYY-MM-DD - Publisher - Title - Author.puz
If a puzzle has no title in its metadata, Untitled is used. If no author, Unlisted (as in the USA Today example above).
To download puzzles for a range of past dates:
./backfill.sh --start 2026-01-01 --end 2026-03-01This iterates day by day, downloading from all sources that support date-based fetching (17 xword-dl sources plus Universal, WSJ, and Universal Sunday via fetch-extras.py). It skips any date+source that already has a .puz file, so it's safe to re-run if interrupted.
Sources that only support --latest (Billboard, Daily Beast, Vox, The Walrus) are excluded — they can't fetch by date.
Progress is logged to ~/Crosswords/logs/backfill.log.
Tip: Backfilling months of puzzles takes a while. The script sleeps 1 second between downloads to avoid hammering servers. If it gets interrupted, just run it again — it picks up where it left off.
If you have existing .puz files with different naming, rename-library.py can standardize them:
# Preview what would change
uv run --with puzpy python3 rename-library.py --dry-run
# Do it
uv run --with puzpy python3 rename-library.pyIt reads each file's embedded metadata, maps source codes to publisher names, and renames everything to the standard format.
Edit fetch-crosswords.sh to add or remove sources from the download calls. Comment out any you don't want.
The fetch-extras.py script runs independently and can be called on its own:
uv run --with requests --with puzpy python3 fetch-extras.py --date 2026-03-13The NYT Midi crossword launched in early 2026 but isn't yet supported in a released version of xword-dl. This setup includes the nytd downloader from PR #319, which adds it as a subclass of the existing NYT downloader. If you install xword-dl from git (as recommended above), you can apply the patch manually — see the PR for the one-file change to newyorktimesdownloader.py.
Three sources need special handling that xword-dl doesn't provide:
- Universal — The AMUniversal API returns 403 to Python's default user-agent.
fetch-extras.pysends a browser user-agent string. If the API fails, it falls back to downloading the raw.puzfrom herbach.dnsalias.com. - WSJ — Not in xword-dl. Available as direct
.puzdownloads from herbach.dnsalias.com. - Universal Sunday — Same as WSJ, published Sundays only.
uv run --with puzpy --with pytest pytest tests/ -vTests cover the pure functions in fetch-extras.py and rename-library.py — filename generation, format detection, and file existence checks. No network calls or real filesystem changes.
MIT