Replacing Runna with Claude
Contents
- What Runna is good at
- Where it stops being the right tool
- What replaces it: two scheduled tasks
- Prerequisites
- Step 1: Connect Strava
- Step 2: Connect Garmin
- Step 3: Edit the Claude Desktop config
- Step 4: The two prompts
- Data traps that will bite you
- The four numbers worth tracking weekly
- Three decisions before you build
- What you give up
- Who should switch
- Run it manually first
Runna will sell you a training plan for $119.99 a year. It's a good product and I'm not going to pretend otherwise. But if you're already paying for Claude, you can build something that fits you better in about twenty minutes.
This works if you log to Strava, wear a Garmin, or both. Strava on its own gives you pace, distance and heart rate, which is enough for everything below. Garmin on its own gives you sleep, HRV, training readiness and race predictions that Strava never sees. Both together is where it starts to beat a stock plan, because now it can weigh your recovery and your history in the same decision.
Claude doesn't out-coach Runna. What it does is see your whole week, including the parts that aren't running, and explain itself when you disagree.
What follows is the case for switching, the setup including the bits that broke on me, and the two prompts I actually run.
What Runna is good at
Runna's plans come from a real coaching model built by real coaches, not a questionnaire wired to a lookup table. It adapts when you miss sessions, and it throws in strength, Pilates and mobility work with actual video. There's a human coaching team in the app.
It also pushes structured workouts to your watch, so a threshold session shows up on your wrist as intervals with pace alerts instead of a line of text you're trying to remember at mile four. Works on Garmin, Apple Watch, Coros, Suunto and Fitbit, which is broader coverage than Garmin's own free coach.
Strava bought Runna in April 2025, and the two now sell a bundle at $149.99 a year in the US.
If what you want is a coach-designed plan on your watch with no work on your part, buy it. The rest of this post isn't for you.
Where it stops being the right tool
Five gaps, roughly in order of how much they cost me.
It plans running, and you don't only run. My week has tennis on Sunday and a swim on Thursday, and both of those are load. A running-only planner calls Sunday a rest day, which it isn't, and then prescribes off that. You tell Claude "Sunday is tennis, treat it as 60 minutes of zone 2 with lateral load" once, in a sentence, and every plan after that respects it.
It starts the day you subscribe. Your Strava account has years in it. The single most useful line I get out of the weekly task is this week against the same week last year. Volume on its own tells you almost nothing. Volume against a season you actually finished tells you where you stand. Runna has no idea who you were eight months ago.
Garmin knows you ran on a treadmill. Runna's plan doesn't do anything about it. Garmin logs treadmill_running as its own activity type. Strava squashes both into "Run." So someone who did 88 treadmill runs and 54 outdoor ones last year looks exactly like someone who never went indoors. And belt pace at zero incline with no wind is 15 to 30 seconds a mile easier than road at the same effort, plus treadmill calibration drifts. If your goal pace only ever happened indoors, race day won't go the way the numbers say. A prompt can insist that long runs, time trials and goal-pace work happen outside, and call it out when they don't.
You can't ask it why. When a plan hands you 3 × 6 min at 8:40 and you think that's wrong, there's nowhere to put that. With Claude the plan is a conversation. "Why intervals on Wednesday when my resting HR is up 5?" gets you an answer, and sometimes the answer is that you're right.
The output format is theirs. I want one line most evenings. Runna gives me an app notification, and after three weeks I stopped opening them. My nightly task is under fifteen words unless something's actually wrong, and I read every single one, because a long message means something happened.
The scorecard:
| Runna | Two Claude tasks | |
|---|---|---|
| Cost | $119.99/yr | Claude subscription you likely already have |
| Plan author | Elite coaches' model | A prompt you wrote |
| Structured workouts on watch | Yes, with pace alerts | No |
| Non-running sport in the model | No | Yes |
| History before you subscribed | None | Your entire Strava archive |
| Treadmill vs outdoor enforcement | Recorded, not enforced | Enforceable |
| Ask it why | No | Yes |
| Strength and mobility video | Yes | No |
| Human coach access | Yes | No |
| Works with your laptop shut | Yes | Strava only, no Garmin |
| Where your plan lives | In the app | Markdown files you own |
Two of those rows are real losses. I'll come back to them.
What replaces it: two scheduled tasks
One runs Sunday night and writes next week's plan off your actual data. The other runs every evening, checks what you did against the plan, and adjusts tomorrow.
The Sunday task reads 28 days of activity, holds it up against the same window a year earlier, and writes a file:
# Week 6 of 16 | Build | target 16 mi
## Where you stand
Runs last 28 days: 11 (target was 14)
Miles: 43.2 (28d), vs 61.8 same window last year
Avg HR on easy runs: 158 ← too high, easy is 121-140
Long run: 7.1 mi, 42 sec stopped time
## The week
Mon Strength, full body, 30 min
Tue Easy 4 mi @ 10:20-11:10 /mi, HR 121-140
Wed QUALITY: 3 × 6 min @ 8:40 /mi, 2 min jog
Thu Swim 30 min + lower body strength
Fri Rest
Sat Long run 7 mi, outdoors, continuous
Sun Tennis
The nightly task posts one line most evenings:
4.1 mi easy at 10:52/mi, avg HR 134, on plan.
Tomorrow: 3 × 6 min @ 8:40.
It only gets longer when something needs to change: a missed quality session, an easy run you accidentally ran at threshold, a training-readiness score that says move tomorrow's intervals.
Prerequisites
- Claude Desktop with Cowork
- A Strava account, a Garmin account, or both
- Docker Desktop, if you want the live Garmin connection
- 20 minutes
Step 1: Connect Strava
Strava publishes an official MCP connector, so this part is one click. Find it in Claude's connector settings and authorise it.
You get eight tools. Two of them matter most:
| Tool | Returns |
|---|---|
list_activities | id, name, sport type, date, distance, time, elevation, avg speed, cadence, calories, relative effort |
get_activity_performance | heart rate, power, per-mile laps, best efforts, segment efforts |
Heart rate isn't in list_activities. If that's the only call you make, you'll build a plan on pace alone and miss the most useful signal you've got. Whatever task you write has to call get_activity_performance per activity.
Strava's heart rate zones are probably wrong
Call get_athlete_zones and look at the heart_rate_zone_source field. If it says MaxHeartRateFromAge, your zones came from the 220-minus-age formula, which knows nothing about you except your birthday.
Someone whose real max is 200 might have Strava putting zone 5 at 175. Now every easy run looks like threshold work and every threshold session looks like a max effort, and anything prescribed off those boundaries is wrong all season.
Go find your actual max instead. Pull get_activity_performance on your hardest recent race or interval session and read max_heartrate. That observed number beats any formula going. Fix this before you write a single prompt.
Step 2: Connect Garmin
There's no official Garmin connector. Nothing in Anthropic's registry, nothing in the plugin directory, and Garmin doesn't publish an MCP server. So, two workarounds.
Route A: bulk export
Log in at connect.garmin.com, then Account Settings, Account Management, Export Your Data, Request Data. A ZIP lands in your email within 24 to 48 hours with every original FIT file plus sleep, heart rate and step history.
It's official, nothing goes to a third party, and it's great for a one-time historical baseline. It gives you nothing live.
Route B: a community MCP server
Several exist on GitHub. I used eddmann/garmin-connect-mcp: MIT licensed, published to PyPI, Docker image available, 22 tools covering sleep with HRV, training readiness, VO2max and race predictions.
It's unofficial and Garmin hasn't endorsed it. It runs against Garmin's private auth endpoints, so it breaks whenever those change. Setup puts your Garmin password in a plaintext file. And it runs locally, so it only works while Claude Desktop is open.
Weigh all that against what Garmin gives you that Strava never sees:
- Sleep, HRV, Body Battery
- Training readiness and status
- Resting heart rate trend
- VO2max
- Race time predictions
- Walk and stop splits inside an activity
- Whether a run happened on a treadmill or outdoors
Docker setup
Pull the image, then make the token directory:
docker pull ghcr.io/eddmann/garmin-connect-mcp:latest
mkdir -p ~/.garminconnect-docker
Now authenticate. Notice what's missing from this command:
docker run -it --rm \
-v "$HOME/.garminconnect-docker:/root/.garminconnect" \
ghcr.io/eddmann/garmin-connect-mcp:latest \
auth
The README suggests mounting an .env file here too. Don't. Mounting it during auth gets you this:
OSError: [Errno 16] Device or resource busy: '/app/.tmp_xxx' -> '/app/.env'
The auth script uses python-dotenv's set_key, which writes a temp file and then calls os.replace() on the target. A bind-mounted file is a mount point, and you can't rename over a mount point. Leave the mount off and .env gets written inside the container's own layer, where the rename works fine. Your tokens still land in the mounted directory, which is the bit you actually need to keep.
Check they arrived:
ls -la ~/.garminconnect-docker
# garmin_tokens.json
Now make the env file the server reads at runtime. It checks for credentials at startup before it ever looks at tokens, so you need this file even though the tokens are already there:
cat > ~/garmin-connect-mcp.env <<'EOF'
GARMIN_EMAIL=you@example.com
GARMIN_PASSWORD=your-password
EOF
chmod 600 ~/garmin-connect-mcp.env
Step 3: Edit the Claude Desktop config
The file lives at ~/Library/Application Support/Claude/claude_desktop_config.json on macOS, and %APPDATA%\Claude\claude_desktop_config.json on Windows.
Merge it programmatically rather than by hand. One trailing comma in this file quietly kills every local MCP server you've got:
CFG="$HOME/Library/Application Support/Claude/claude_desktop_config.json"
[ -f "$CFG" ] && cp "$CFG" "$CFG.bak.$(date +%s)"
python3 - <<'EOF'
import json, os
p = os.path.expanduser("~/Library/Application Support/Claude/claude_desktop_config.json")
cfg = json.load(open(p)) if os.path.exists(p) else {}
cfg.setdefault("mcpServers", {})["garmin"] = {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-v", os.path.expanduser("~/garmin-connect-mcp.env") + ":/app/.env:ro",
"-v", os.path.expanduser("~/.garminconnect-docker") + ":/root/.garminconnect",
"ghcr.io/eddmann/garmin-connect-mcp:latest",
],
}
json.dump(cfg, open(p, "w"), indent=2)
print("servers configured:", ", ".join(cfg["mcpServers"]))
EOF
The :ro flag mounts .env read-only, so the rename bug can't come back at runtime.
Quit Claude Desktop properly, Cmd-Q rather than just closing the window, then reopen it. Local MCP servers only load at launch.
If the script prints only garmin and you were expecting Strava too, that's correct. Strava is a remote connector, managed in Claude's connector settings, and it never shows up in this file.
Step 4: The two prompts
Ask Claude to create the tasks, or use the Scheduled panel in the sidebar. Every scheduled run starts with no memory of the conversation that created it, so the prompt has to carry its own context: the training block, your zones, the constraints, the output format.
These are the two I run. Swap the block, the zones and the constraints for yours.
The weekly planner: Sundays, 7pm
You are writing next week's training plan. I am in week {N} of a 16-week
block for a half marathon on {DATE}. Goal time 1:55.
MY ZONES (observed, not formula):
Max HR 197. Easy 121-140. Threshold 168-178.
Easy pace 10:20-11:10 /mi. Goal pace 8:47 /mi.
FIXED CONSTRAINTS:
Sunday is tennis, ~90 min. Count it as load, never schedule a run.
Thursday is a pool day. Swim + lower body strength.
Friday is rest, always.
Long runs, time trials and goal-pace work must be OUTDOORS.
DO THIS:
1. list_activities for the last 28 days. Use range_start and range_end.
The tool caps at 100 results and truncates silently.
2. list_activities for the SAME calendar window one year ago.
3. get_activity_performance on every run in the recent window. That is
the only place heart rate lives.
4. Garmin: query_goals_and_records for race predictions,
query_health_summary for resting HR trend, and the activity-type
breakdown so treadmill and outdoor runs are counted separately.
If Garmin is unavailable, continue with Strava only and say so in
one line. Do not fail.
5. Exclude walks. Drop duplicate activities that start within 60s of
each other, keeping the one with real distance. Count zero-distance
indoor rides by time, not distance.
THEN WRITE, in this exact shape and nothing more:
# Week {N} of 16 | {phase} | target {X} mi
## Where you stand
Runs last 28 days: {n} (target was {n})
Miles: {x} (28d), vs {x} same window last year
Avg HR on easy runs: {n} ← flag if above 140
Long run: {x} mi, {n} sec stopped time
Race prediction: {time} (was {time} last week)
## The week
Mon through Sun. One line each. Distance, pace range, target HR.
Mark quality sessions with QUALITY:. Mark required-outdoor with
"outdoors".
Save to training/plans/{YYYY-MM-DD}.md.
No encouragement. No explanation of the training philosophy.
That year-earlier comparison is the most useful line in the whole report. It tells you whether you're ahead, behind, or quietly repeating a pattern that already failed once.
The nightly check: Monday to Saturday, 9pm
Give the weekly planner Sunday to itself so the two don't collide.
Read training/plans/ and find the most recent plan file. Get today's
prescribed session from it.
Pull today's activities from Strava. Call get_activity_performance on
any run for heart rate. Check Garmin training readiness for tomorrow;
if Garmin is down, skip that check silently.
LENGTH RULES (these are hard):
- On plan, nothing to change: ONE line.
- Rest day correctly taken: ONE line.
- One missed session: one line naming it and whether to shift or drop.
Neutral. No moralizing, no encouragement.
- Needs a real change: up to 5 lines. The only triggers are a missed
quality session, two or more misses this week, an easy run above
140 bpm average, training readiness under 40 with quality scheduled
tomorrow, or tomorrow being the long run.
Format when on plan:
{distance} {type} at {pace}, avg HR {n}, on plan.
Tomorrow: {tomorrow's session, abbreviated}.
Write the length rules in, or it'll hand you a paragraph every night and you'll stop reading them. That's what happened to me in week one.
Data traps that will bite you
Every one of these gave me a wrong answer before I caught it.
list_activities caps at 100 results. Ask for a year and you quietly get the most recent 100. My first pull stopped three days short of a half marathon PR, so the early analysis decided my goal was a stretch when I'd nearly hit it eight months before. Window your queries with range_start and range_end.
Duplicate activities. If you run a smart trainer app alongside a watch, both of them push the same session. Look for two activities starting within 60 seconds of each other, keep the one with real distance, count the pair once. Skip this and your ride volume doubles on the weeks it happens.
Zero-distance entries. Some indoor rides log distance: 0 with perfectly good moving_time and calories. Those are real sessions. Count them by time or they just disappear from your totals.
Daily walks aren't training. If you track steps, those walks turn up as activities. Anything that counts them overstates your volume and, worse, marks a walk-only day as a session you completed. Filter them out by sport type.
query_heart_rate_data returns enormous payloads. Two weeks of it came back at 200,000 characters and blew the context limit. Query one day at a time.
Garmin separates treadmill_running from running. Strava doesn't. Covered above, and it's the trap that changes what you get prescribed the most.
The four numbers worth tracking weekly
Once both sources are wired in, four numbers will tell you more than a training log does.
Race prediction. Garmin's query_goals_and_records gives you predicted times for 5K, 10K, half and marathon. Track the one that matches your goal distance, week over week. It folds everything together and has no opinion about how motivated you were feeling.
The predictions also disagree with each other in useful ways. Say Garmin predicts a 25-minute 5K but a 2:07 half. Run the 5K through the Riegel formula and it implies about 1:55. That 12-minute gap is an endurance deficit with a number on it, and it's telling you to add long runs rather than intervals. No plan generator hands you that read, because it has no reason to compare its own outputs against each other.
Resting heart rate. Get a baseline over two normal weeks. A sustained 4 or 5 bpm above it is the earliest overreaching signal you'll get, and it turns up before the workouts start feeling bad.
Average heart rate on easy runs. The classic amateur mistake is running your easy days at threshold, which develops nothing in particular and wrecks your recovery for the sessions that do matter. Have the nightly task flag any easy run above your zone 2 ceiling.
Stopped time. Compare elapsed_time to moving_time on your long runs. Garmin goes further: get_activity_details with include_splits=true gives you RWD_WALK and RWD_STAND split counts, so you can see the 38 separate walk breaks inside a race that felt continuous at the time. If you're chasing a time goal, that gap is often bigger than the fitness gap.
Three decisions before you build
Local files or embedded prompts. Writing plans to markdown files gives you a readable history and lets the nightly task see what Sunday prescribed. It also means the tasks fail when your laptop's closed, because scheduled tasks run remotely and can't reach local folders. Putting the plan in the prompt works anywhere and survives a closed laptop, but you lose the week-to-week continuity. Putting the static block in the prompt while writing output to files gets you most of both.
Garmin makes the local requirement stricter. A Docker MCP server only runs while Claude Desktop is running. Write both tasks to treat Garmin as optional and fall back to Strava instead of erroring, or you'll get a failure notification every night your laptop is shut. Both prompts above already do this.
Credential storage. The Garmin server wants your password in a plaintext file. Mode 600 keeps it to your user account. If that trade doesn't sit right with you, use the bulk export and skip the live connection.
What you give up
Two things, and neither one is small.
Workouts don't arrive on your watch. Runna pushes a threshold session to Garmin as a structured workout with pace alerts and lap prompts. You get a line of text, and you run it off memory and feel. For easy runs that's fine. For 3 × 6 min at 8:40 it's worse, and I've run the interval long more than once because I lost count out there. You can build the workout by hand in Garmin Connect, which takes two minutes, and I do that for quality sessions only.
No strength video, no human coach. Runna's strength and mobility library is real content made by actual people. "Strength, full body, 30 min" is a line in a text file. If you don't already know what to do in a gym, that line is worth less than nothing.
Who should switch
Switch if you're multi-sport, you've got years of Strava history you want the plan to reason about, you own a Garmin, and you're the sort of person who wants to see the reasoning rather than just trust the output. This setup rewards people who'll go edit the prompt when it gets something wrong.
Stay on Runna if you mostly want structured workouts on your wrist, you're new enough to running that a coach-designed model beats anything you'd write yourself, or the strength library is worth more than $10 a month to you.
Run it manually first
Trigger both tasks by hand from the Scheduled panel before the first automatic run. Tool approvals you grant during a manual run get stored and reused, which is what stops the first scheduled run stalling on a permission prompt at 9pm while you're asleep.
Then leave the prompts alone for two weeks. The nightly output will be boring most evenings, which is how you know it's working. By the end you'll know which flags you actually act on and which ones you scroll straight past, and you can cut the rest.