| name | strava_cli |
| description | Running analytics, coaching automation, and data visualization with bwilczynski/strava-cli. Includes HR zone analysis, plan-vs-actual tracking, weekly volume charts, and automated cron-based coaching check-ins. |
| version | 1.0.0 |
| author | Duarte |
| metadata | {"hermes":{"tags":["strava","cli","sports","running","cycling"],"homepage":"https://github.com/bwilczynski/strava-cli"}} |
Strava CLI
Command-line interface for Strava using the Strava API.
How to Run
uv run --with strava-cli strava <command>
Configuration
Credentials are stored in ~/.strava-cli/:
config.json — Client ID and Client Secret
access_token.json — OAuth access token (expires ~May 2027)
Env vars also work: STRAVA_CLIENT_ID, STRAVA_CLIENT_SECRET
Available Commands
| Command | Description |
|---|
strava login | OAuth login (opens browser) |
strava config | Interactive config setup |
strava stats | Recent / YTD / all-time stats |
strava activities | List activities |
strava activity <id> | Detailed activity info |
strava profile | Athlete profile |
strava upload <file.gpx> | Upload GPX activity |
strava logout | Clear stored token |
Usage Examples
uv run --with strava-cli strava stats
uv run --with strava-cli strava activities -pp 5
uv run --with strava-cli strava activities --after="2 weeks ago" --before="1 week ago"
uv run --with strava-cli strava activities --after="2018-12-01"
uv run --with strava-cli strava activity 1958241710
uv run --with strava-cli strava activities -q --after="1 day ago" | xargs uv run --with strava-cli strava activity
uv run --with strava-cli strava activities -pp 1 -q | xargs uv run --with strava-cli strava activity --output json | jq ".name"
uv run --with strava-cli strava upload ./2020-09-27-145141.gpx
uv run --with strava-cli strava upload ./*.gpx
Output Formats
Default is table. Use --output json for machine-readable output.
uv run --with strava-cli strava activity <id> --output json
Stderr contamination pitfall: When redirecting JSON output to a file with >, strava-cli's deprecation warning (/etc/timezone is deprecated...) gets written to the file as line 1, breaking JSON parsers. Fix: strip the first line or use 2>/dev/null:
JSON truncation pitfall: Large --output json responses (e.g. 12-week activity windows) can exceed the tool's output buffer and truncate mid-JSON, causing parse failures at unpredictable line/column offsets. The polyline map data in each activity is the culprit — it's large and varies per activity. When you need bulk volume stats, use the table output instead of JSON. Parse it with grep "🏃" | awk:
uv run --with strava-cli strava activities --after="7 days ago" --output json > /tmp/data.json
uv run --with strava-cli strava activities --after="7 days ago" --output json 2>/dev/null > /tmp/data.json
with open("/tmp/data.json") as f:
lines = f.readlines()
data = json.loads("".join(lines[1:]))
Data Analysis & Visualization
Pagination trap: strava activities defaults to ~30 results. Use --per_page 200 for bulk export.
uv run --with strava-cli strava activities --after="2026-01-01" --per_page 200 --output json > /tmp/strava_2026.json
Date parsing gotcha: Strava JSON dates end with Z (UTC). Strip or handle it before parsing:
import json
from datetime import datetime, timedelta
from collections import defaultdict
with open("/tmp/strava_2026.json") as f:
data = json.load(f)
weekly_km = defaultdict(float)
for activity in data:
if activity["type"] != "Run":
continue
date_str = activity["start_date"].replace("Z", "+00:00").split("+")[0]
date = datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S")
year, week, _ = date.isocalendar()
weekly_km[f"{year}-W{week:02d}"] += activity["distance"] / 1000
Weekly bar chart (matplotlib):
import matplotlib.pyplot as plt
weeks = sorted(weekly_km.keys())
values = [weekly_km[w] for w in weeks]
fig, ax = plt.subplots(figsize=(14, 6))
bars = ax.bar(range(len(weeks)), values, color="steelblue", edgecolor="black", linewidth=0.5)
bars[-1].set_color("#e74c3c")
ax.set_xticks(range(len(weeks)))
ax.set_xticklabels(weeks, rotation=45, ha="right", fontsize=8)
ax.set_ylabel("km")
ax.set_title("Running km per week")
ax.grid(axis="y", linestyle="--", alpha=0.5)
for bar, val in zip(bars, values):
if val > 0:
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5,
f"{val:.0f}", ha="center", va="bottom", fontsize=7)
fig.patch.set_facecolor("white")
ax.set_facecolor("white")
plt.tight_layout()
plt.savefig("/tmp/weekly_km.png", dpi=200, facecolor="white")
Use uv run --with matplotlib if matplotlib is not already installed.
Heart Rate & Pace Analysis
Easy run zone check: Use detailed activity data to verify easy runs are actually easy.
uv run --with strava-cli strava activity <activity_id>
Key thresholds for easy aerobic running:
- Target HR: ~60–75% of max HR (conversational pace)
- Red flag: Avg HR >150–160 bpm on an "easy" day means you're running too hard
- Pace indicator: If easy pace is within 15–20 sec/km of tempo pace, it's not easy enough
Pattern to check: Compare easy-day HR against long-run HR. If they're similar, easy days are too hard.
uv run --with strava-cli strava activities --after="7 days ago" --output json | \
python3 -c "import json,sys; [print(a['id'], a['name'], a['average_heartrate']) for a in json.load(sys.stdin) if a.get('average_heartrate')]"
Plan vs. Actual Comparison
Weekly compliance check: Compare a training plan against actual Strava data.
-
Export the week's activities:
uv run --with strava-cli strava activities --after="monday" --output json > /tmp/this_week.json
-
Map actuals to planned days and flag:
- Missed sessions (no activity on planned day)
- Short runs (distance < plan)
- Hard easy runs (HR too high)
- Swapped sessions (e.g., gym replaced with run)
Volume tracking:
uv run --with strava-cli strava activities --after="monday" --output json | \
python3 -c "import json,sys; d=json.load(sys.stdin); runs=[a for a in d if a['type']=='Run']; print(f'Runs: {len(runs)}, Km: {sum(a[\"distance\"] for a in runs)/1000:.1f}')"
Automated Coaching Cron Jobs
Coaching crons are managed by the fitness-coach skill — see that skill for the full output format (casual human coach texting style). This section documents the Strava data commands the crons rely on.
Daily check-in (1 PM): Single cron (coach-daily) reviews yesterday's session, today's if already on Strava, prescribes tomorrow.
Data commands the crons use:
uv run --with strava-cli strava activities --after="7 days ago" --output json
uv run --with strava-cli strava activity <id> --output json
Key thresholds for the coach to enforce: easy run HR ceiling 145 bpm avg, tempo must hit planned distance, long runs 15+ km conversational pace.
OAuth Notes
strava login opens a local browser for OAuth authorization
- Token auto-refresh is handled by the CLI
- If token expires, re-run
strava login
- For headless servers, use manual OAuth: generate auth URL, visit in browser, exchange code for token