An AI cycling coach you host yourself — a Claude agent that reads your rides, predicts your routes, tracks your form, and verifies every climb against 1-metre LIDAR so the pacing is actually right. A physicist's model with a directeur sportif's attitude — and a plan that bends when life doesn't.
A real route, profiled — grade-coloured climbs, UCI categories, a KOM-points total, every gradient re-checked against 1 m LIDAR.
I'm a self-coached rider, and I wanted a coach that actually knew two things most apps don't: the physics of my engine — the legs, not the Brompton's motor — on each of my bikes, and the real gradient of the roads I ride. TrainingPeaks knows my numbers; Strava knows the segments; neither knows that the routing engine flattened the 14% wall on Leith Hill into a polite 9%, or that I changed my position and dropped my CdA.
So I built one. It reads my rides, predicts my routes, tracks my form — and because it's a Claude agent, not a static plan, I just talk to it: "knee's grumbly, what should I do this week?", "I'm in France for 10 days, re-plan around these climbs." Training plans assume your life goes to plan. This one bends when it doesn't.
- Talks like a coach. Ask for the week's workouts, a route's pacing, or how to come back from an injury — it answers from your real data, not generic advice, and updates your rider profile as it goes.
- Adaptive plans. Life happens: injury, travel, a wrecked work week. It reshapes the block around what actually happened instead of handing you a plan you've already broken.
- Reads your rides (FIT). Parses TSS / NP / IF, builds a power curve, finds the climbs, writes a per-ride Markdown analysis.
- Predicts your routes (GPX). Speed and duration at FTP / MAP / Z2 / Z3 with honest uncertainty ranges, a pacing narrative, and a fuelling plan.
- Hi-fi LIDAR climb verification. Re-samples every climb against 1 m elevation data so a flattened gradient doesn't wreck your pacing — the clever bit, below.
- Training-load forecasting (CTL / ATL / TSB). Projects form using TrainingPeaks' lag-1 convention.
- Climb categorisation. UCI-style Cat 4 → HC with a Tour-de-France-scale KOM-points total per ride.
- Multiple bikes (N+1, obviously). Per-bike physics profiles (road / gravel / aero — CdA, CRR, weight) and Silca-extrapolated pressures for your actual front/rear weight split, not just the 50/50 preset.
Routing engines and GPS smear elevation. On a Cat-3+ climb that smear is the difference between "ride at threshold" and "blow up 200 m from the top." So for every climb it finds, the agent re-samples the road against 1 m LIDAR DEM tiles (UK DEFRA, FR IGN), map-matched to the actual tarmac with OSRM, with a GPXZ API fallback outside cached tiles — then blends the hi-fi climbs back into the route profile (Petrasova/Robinson DSF correction) so the numbers you pace to are the numbers on the road.
Every climb on one sheet: GPX (grey) vs 1 m-LIDAR (colour). On this route the router
under-reported peak gradients by up to 8 percentage points — a "6.5%" climb hiding a 14% wall.
Why it matters: a climb the router calls "1.6 km @ 6.5%" can really be "1.6 km @ 6.5% with a 14% pitch in the middle." The first number tells you to sit and spin; the second tells you which gear to be in before the wall. The agent always paces to the second.
The repo ships a CLAUDE.md "brain" that any Claude Code-compatible assistant auto-loads in the repo
root — the coaching workflows, the physics, the training-load definitions, and the conventions every
script follows. The assistant reads your USER_PROFILE.md, runs the scripts, and writes the analyses
back to disk, updating your profile as a side effect of every conversation. It's portable: the same brain
runs standalone, or hosted inside an OpenClaw workspace with memory and heartbeats.
You don't need the agent — every script runs standalone from the CLI. But the agent is what turns a box of tools into an everyday coach.
💬 "Shut up, legs." — when the data says you've got more in you.
💬 "It's not the bike. It's never the bike." — but I'll still nail your tyre pressure.
💬 "The correct number of bikes is N+1." — and I know the physics of all N.
💬 "Free speed is just aero you haven't bought yet." — and yes, I track your CdA.
💬 "Don't blame the map." — I checked the gradient against 1 m LIDAR. Blame the legs.
git clone https://github.com/fmasi/cycling-analysis-agent.git
cd cycling-analysis-agent
conda env create -f environment.yml # cross-platform (osx-arm64 / linux-64), loose pins
conda activate cycling
cp USER_PROFILE.example.md USER_PROFILE.md # fill in your numbers (or run with neutral defaults)
python scripts/analyse_fit.py rides/<your-ride>.fit --save # after a ride
python scripts/analyse_gpx.py routes/<your-route>.gpx --save # before a rideThen start Claude Code in the repo root and just talk to it. No Claude? The scripts all run on their own.
From examples/route-prediction.example.md (fictional rider):
| # | Climb | Length | Avg | Max | Cat | KOM |
|---|---|---|---|---|---|---|
| 1 | Box Hill | 2.5 km | 5.0% | 8% | Cat 3 | 2 |
| 2 | Leith Hill | 1.6 km | 6.5% | 11% | Cat 3 | 2 |
| 3 | White Down | 0.9 km | 8.5% | 14% | Cat 3 | 2 |
Predicted speed on White Down (8.5%): 11.2 km/h @ FTP, 14.4 @ MAP, 9.0 @ Z3 (± 1 km/h). For the 11% pitch on Leith Hill: ~280 W at 60 rpm to clear in the lowest gear (34×32 ≈ 10.5 km/h). Fuelling: 54 g carb/hr; one stop at km 35. Pacing: ride the first 20 km below 130 W, then target FTP on the climbs.
Per-climb hi-fi detail: where the gradient actually bites, coloured by steepness.
- Geospatial. 1 m DEM tile sampling (rasterio), OSRM map-matching to the real road, OSGB shapefile building for the UK DEM portal, and a Petrasova/Robinson-style blend that stitches hi-fi climbs into the GPX profile without visible seams.
- Sports science. CTL/ATL/TSB with the TrainingPeaks lag-1 convention; UCI climb categorisation + KOM points; power-curve extraction from FIT.
- Physics. One
predict_speed/predict_powerpair built on the standard cycling power equation, parameterised per bike by CdA, CRR, drivetrain efficiency, and system weight. - Engineering. Rider-agnostic with safe neutral defaults (a fresh clone runs out of the box), a
pytestsuite, and a cross-validation harness that scores the LIDAR verifier against FIT-recorded truth.
1 m LIDAR: UK DEFRA and FR IGN RGE ALTI. Elevation fallback: GPXZ.io (non-commercial tier). Map-matching: OSRM. Training-load conventions: TrainingPeaks. Tyre model: extrapolated from Silca's published data.
The framework is rider-agnostic: all personal data lives in a gitignored USER_PROFILE.md plus six
gitignored data folders (rides/, routes/, tests/, plans/, body-comp/, and personal notes/),
each with its own second-line .gitignore. *.fit, *.gpx, *.tcx, *.key, .env, and *.pem are
untrackable anywhere in the tree. Scripts fall back to neutral defaults, so the repo is safe to publish,
fork, and share.
I build tools at the intersection of AI agents and the things I actually care about. This one's my cycling coach — geospatial data, sports science, a physics model, and an agent with opinions.
More of my work:
- parley — private, on-device meeting transcription (macOS, Swift)
- mailrag — private, self-hosted email RAG
MIT. © 2026 Frédéric Masi.