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 is a good product and I am not going to pretend otherwise. But if you already pay 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 alone gives you pace, distance and heart rate, which is enough to run everything below. Garmin alone gives you sleep, HRV, training readiness and race predictions that Strava never receives. Both together is where it beats a stock plan, because the plan can then weigh your recovery and your history in the same decision.
Claude does not out-coach Runna. It sees your whole week, including the parts that are not running, and it explains itself when you disagree.
What follows is the case for switching, the setup including the parts that broke when I did it, and the two prompts I run.
What Runna is good at
Runna's plans are built on a real coaching model by real coaches, not assembled from a questionnaire and a lookup table. It adapts when you miss sessions, and it bundles strength, Pilates and mobility work with actual video. There is a human coaching team in the app.
It also pushes structured workouts to your watch, so a threshold session arrives on your wrist as intervals with pace alerts rather than as a line of text you have to remember mid-run. It works on Garmin, Apple Watch, Coros, Suunto and Fitbit, which is more device coverage than Garmin's own free coach offers.
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 delivered to your watch with no work on your part, buy it. The rest of this post is not for you.
Where it stops being the right tool
Five gaps, in rough order of how much they cost me.
It plans running, and you do not only run. My week has tennis on Sunday and a swim on Thursday, and both are load. A running-only planner treats Sunday as rest, which it is not, and prescribes accordingly. You can tell Claude "Sunday is tennis, treat it as 60 minutes of zone 2 with lateral load" in one sentence and every subsequent plan respects it.
It starts the day you subscribe. Your Strava account has years in it. The most useful single comparison I get out of the weekly task is this week against the same calendar week last year. Volume in isolation tells you almost nothing. Volume against a season you finished tells you where you stand. Runna has no view of the athlete you were eight months ago.
Garmin knows you ran on a treadmill. Runna's plan doesn't act on it. Garmin records treadmill_running as a separate activity type. Strava collapses both into "Run." An athlete who did 88 treadmill runs and 54 outdoor runs in a year looks identical in Strava to one who never went indoors. Belt pace at zero incline with no wind runs 15 to 30 seconds per mile easier than road at the same effort, and treadmill calibration drifts. If your goal pace only ever happened indoors, race day will not go the way the numbers suggest. A prompt can require that long runs, time trials and goal-pace work happen outdoors, and flag it when they do not.
You cannot ask it why. When a plan gives you 3 × 6 min at 8:40 and you think that is wrong, there is nowhere to put that. With Claude the plan is a conversation. "Why intervals on Wednesday when my resting HR is up 5?" gets an answer, and sometimes the answer is that you are 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 it. My nightly task is under fifteen words unless something is wrong, and I read every 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 will come back to them.
What replaces it: two scheduled tasks
One runs Sunday night and writes next week's plan from your actual data. The other runs each evening, checks what you did against the plan, and adjusts tomorrow.
The Sunday task reads 28 days of activity, compares it to 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 gets longer only when something needs to change: a missed quality session, an easy run at threshold, a training-readiness score that argues for moving 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 a click. Find it in Claude's connector settings and authorize it.
You get eight tools. Two 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 is missing from list_activities. If you only call that one, you will build a plan on pace alone and miss the most useful signal you have. Any task you write needs to call get_activity_performance per activity.
Strava's heart rate zones are probably wrong
Call get_athlete_zones and check the heart_rate_zone_source field. If it says MaxHeartRateFromAge, the zones come from the 220-minus-age formula, which knows nothing about you except your birthday.
An athlete whose real max is 200 might see Strava place zone 5 at 175. Every easy run then looks like threshold work, and every threshold session looks like a max effort. Prescriptions built on those boundaries will be wrong all season.
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. Fix it before you write a single prompt.
Step 2: Connect Garmin
No official Garmin connector exists. Nothing in Anthropic's registry, nothing in the plugin directory, and Garmin publishes no MCP server. Two workarounds exist.
Route A: bulk export
Log in at connect.garmin.com, open Account Settings, then Account Management, then Export Your Data, then Request Data. A ZIP arrives by email within 24 to 48 hours with every original FIT file plus sleep, heart rate and step history.
Official, nothing shared with third parties, and good 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.
The project is unofficial and not endorsed by Garmin. It runs on Garmin's private auth endpoints, so it breaks when those change. Setup stores your Garmin password in a plaintext file. And it runs locally, meaning it only works when Claude Desktop is open.
Weigh that against what Garmin adds that Strava never receives:
- 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 create the token directory:
docker pull ghcr.io/eddmann/garmin-connect-mcp:latest
mkdir -p ~/.garminconnect-docker
Authenticate. Note what is 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 also mounting an .env file here. Do not. Mounting it during auth produces:
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 then calls os.replace() on the target. A bind-mounted file is a mount point, and you cannot rename over a mount point. Leave the mount off and .env gets written inside the container's own layer, where the rename works. Tokens still land in the mounted directory, which is the part you need to keep.
Confirm they arrived:
ls -la ~/.garminconnect-docker
# garmin_tokens.json
Now create the env file the server reads at runtime. It checks for credentials at startup before it looks at tokens, so this file is required even though tokens exist:
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, %APPDATA%\Claude\claude_desktop_config.json on Windows.
Merge programmatically rather than by hand. A trailing comma in this file silently disables every local MCP server you have:
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 cannot recur at runtime.
Quit Claude Desktop fully, with Cmd-Q rather than closing the window, and reopen. Local MCP servers load at launch.
If the script prints only garmin and you expected Strava too, that is correct. Strava is a remote connector managed in Claude's connector settings and never appears in this file.
Step 4: The two prompts
Ask Claude to create the tasks, or use the Scheduled panel in the sidebar. Each scheduled run starts with no memory of the conversation that created it, so the prompt has to carry its own context: the training block, the zones, the constraints and the output format.
These are the two I run. Replace the block, zones and constraints with 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.
The year-earlier comparison is the most useful line in the whole report. It tells you whether you are ahead, behind, or repeating a pattern that already failed once.
The nightly check: Monday to Saturday, 9pm
Give the weekly planner Sunday to itself so the two never 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 will produce a paragraph every night and you will stop reading it. That happened to me in week one.
Data traps that will bite you
Every one of these produced a wrong answer before I caught it.
list_activities caps at 100 results. Query a year and you get the most recent 100, silently. My first pull ended three days before a half marathon PR, so the early analysis concluded a goal was a stretch when I had nearly hit it eight months earlier. Window your queries with range_start and range_end.
Duplicate activities. If you use a smart trainer app alongside a watch, both 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. Without this, ride volume doubles on affected weeks.
Zero-distance entries. Some indoor rides log distance: 0 with valid moving_time and calories. They are real sessions. Count them by time or they vanish from your totals.
Daily walks are not training. If you track steps, those walks appear as activities. Any analysis that counts them overstates volume and, worse, flags a walk-only day as a completed session. Exclude them by sport type.
query_heart_rate_data returns enormous payloads. Two weeks of data came back at 200,000 characters and blew the context limit. Query one day at a time.
Garmin separates treadmill_running from running. Strava does not. Covered above, and it is the trap that changes prescriptions most.
The four numbers worth tracking weekly
Once both sources are wired in, four numbers tell you more than a training log does.
Race prediction. Garmin's query_goals_and_records returns predicted times for 5K, 10K, half and marathon. Track the one matching your goal distance week over week. It integrates everything and has no opinion about how motivated you felt.
The predictions also disagree with each other in useful ways. If Garmin predicts a 25-minute 5K but a 2:07 half, run the 5K through the Riegel formula: it implies roughly 1:55. A 12-minute gap between the two is an endurance deficit, quantified, and it tells you to add long runs rather than intervals. That is the kind of read a plan generator will not hand you, because it has no reason to compare its own outputs against each other.
Resting heart rate. Establish a baseline over two normal weeks. Sustained elevation of 4 to 5 bpm is the earliest overreaching signal you get, and it shows up before the workouts start feeling bad.
Average heart rate on easy runs. The most common amateur error is running easy days at threshold, which develops nothing well and prevents recovery for the sessions that matter. Have the nightly task flag any easy run above your zone 2 ceiling.
Stopped time. Compare elapsed_time to moving_time on long runs. Garmin goes further: get_activity_details with include_splits=true returns RWD_WALK and RWD_STAND split counts, so you can see 38 separate walk breaks inside a race that felt continuous. If you are chasing a time goal, that gap is often larger 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 read what Sunday prescribed. It also means the tasks fail when your laptop is closed, since scheduled tasks run remotely but cannot reach local folders. Embedding the plan in the prompt works anywhere and survives a closed laptop, at the cost of week-to-week continuity. Embedding the static block in the prompt while writing output to files gets most of both.
Garmin makes the local requirement stricter. A Docker MCP server only runs when Claude Desktop runs. Write both tasks to treat Garmin as optional and fall back to Strava rather than erroring, or you will get failure notifications every night your laptop is shut. Both prompts above do this.
Credential storage. The Garmin server needs your password in a plaintext file. Mode 600 limits it to your user account. If that trade does not sit right, use the bulk export and skip the live connection.
What you give up
Two things, and neither is small.
Workouts do not 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 have to run it off memory and feel. For easy runs this is fine. For 3 × 6 min at 8:40 it is worse, and I have run the interval long more than once because I lost count. You can build the workout manually in Garmin Connect, which takes two minutes and which I do for quality sessions only.
No strength video, no human coach. Runna's strength and mobility library is real content produced by people. "Strength, full body, 30 min" is a line in a text file. If you do not already know what to do in a gym, that line is worth less than nothing.
Who should switch
Switch if you are multi-sport, have years of Strava history you want the plan to reason about, own a Garmin, and are the kind of person who wants to see the reasoning rather than trust the output. The setup rewards people who will edit the prompt when it gets something wrong.
Stay on Runna if you mostly want structured workouts on your wrist, you are new enough to running that a coach-designed model beats one you write yourself, or you value the strength library at more than $10 a month.
Run it manually first
Trigger both tasks by hand from the Scheduled panel before the first automatic run. Tool approvals granted during a manual run get stored and reused, which stops the first scheduled run from stalling on a permission prompt at 9pm while you are asleep.
Then leave the prompts alone for two weeks. The nightly output will be boring most evenings, which means it is working. By the end you will know which flags you act on and which you scroll past, and you can cut the rest.