My AI Running Coach Is a Git Repo and a Claude Agent

I finish a run, my watch syncs, and three minutes later the analysis is written in my repository, the week has been adjusted, and my coach has left a comment under the Strava activity. No app built, no server written, no per-token API bill: a Claude subscription, AgentsRoom, and Markdown files. Here is the whole build, reproducible.

I finish my session. My watch syncs to Strava on its own, as usual. I go and take a shower.

By the time I am out, three things have happened without me touching anything. The analysis of the session is written in my training repository. The week has been adjusted, with the reason for the change noted next to it. And under the Strava activity there is a comment from my coach, telling me what the session was worth and what it changes for Friday.

That coach is not an application I built. It is a Git repository of Markdown files, a Claude subscription, and AgentsRoom holding it together. No server written, no per-token bill, about a weekend of assembly.

The whole thing is published as a template: github.com/AgentsRoomDev/running-performance-coach. You can clone it and make it yours by filling in the placeholders. This article explains how it works, piece by piece, assuming you have heard the word "API" but never written a webhook.

For context: I have been running for a long time, 2:47 on the marathon, 1:13:59 on the half, 33:45 on 10 km. The goal of the current cycle is to get back under 34 minutes over 10 km. That matters for what follows: a generic coach that re-explains what a threshold session is is of no use to me, and that is exactly the problem this build solves.

What happens between the end of my run and the comment

The full chain is six steps:

  1. My watch sends the activity to Strava. That part already happens for everyone.
  2. Every 15 minutes, a small Python script asks Strava whether anything is new.
  3. When it finds a new session, it builds a Markdown session sheet in my repository: laps, splits, volume, heart rate. Measured data only.
  4. It also rewrites the title and the description of the activity on Strava, so my feed stops saying "Afternoon Run".
  5. Then it sends a signed message to AgentsRoom, which opens a Claude agent with the session already in hand.
  6. That agent does the coaching work: it reads, it compares, it writes the analysis, it adjusts the week, it commits, it pushes, it comments on Strava, it emails me the long report.

The first five steps are plumbing. The sixth is what this article is about.

The training log is a Git repository, not a database

This is the decision that changes everything, and it is also the one that surprises people most.

One session = one file, journal/2026/2026-09-03.md. One week = one file, plan/weeks/2026-W36.md. One plan change = one commit, with its reason in the message. There is no database, no schema, no migration, no interface.

Three consequences, in order of importance:

The coach can re-read its own history. It knows what it prescribed three weeks ago, and it can check whether that worked. A chatbot you tell your session to starts from zero on every conversation. An agent with a repository has a memory, and that memory is readable by a human.

I read my plan on my phone, in the GitHub app. The repository's README.md is not a presentation page: it is my dashboard. The contract written in CLAUDE.md is explicit about it, no planning is finished until the README reflects it. The result: I have no interface to maintain, and yet I have a screen that tells me what I am doing today.

Nothing is irreversible. Everything the agent writes is a commit. I can read it, argue with it, revert it. That is very different from an application deciding on its own.

Step 1: Strava wakes up a small script

Strava exposes an API: a way, for a program, to ask "give me this athlete's recent activities". The strava_sync.py script does exactly that, and turns the answer into a session sheet.

The interesting part is not the network call, it is the reconstruction. A watch records raw laps. The script has to work out what session that was:

Lap 1  : 4.40 km in 26'07 (5:56/km)   ← warm-up
Lap 2  : 1.00 km in 3'41  (3:41/km)   ← rep 1
Lap 3  : 0.20 km in 1'59  (9:55/km)   ← recovery
...                                     → "5 x 1000m r' 2'"

It tries every split of the form "the k fastest laps are the repetitions" and keeps the best one that holds up. That sounds trivial and it is not: naive clustering by speed gets caught the moment a warm-up is faster than a recovery.

Above all, the shape of the session is reconstructed from the watch, never from the plan. It is tempting to do the opposite (the plan says 5 x 1000m, so write that) and it is exactly the mistake: the whole point is to detect the days when I did something else. When the two disagree, that disagreement is the finding, and the coach sees it:

Planned 3 x 8' → run continuously

Two warnings before you start.

Strava's API has required a paid developer subscription since June 2026. Without it, every call answers 403 Application Status Inactive. The fallback exists and is built into the template: export a TCX file from your watch and hand it to import_tcx.py. Everything downstream of the import works identically.

The quotas are generous, but real. On my application, 300 requests every 15 minutes and 3,000 a day for reads. The script uses one per pass in steady state, so 96 a day. That is nowhere near the ceiling, but it is the kind of thing you check before, not after.

Settings page of a Strava API application: standard developer tier, client ID, masked client secret, access token and refresh token in read scope, and the rate limits shown, 600 requests every 15 minutes and 6,000 per day overall, 300 every 15 minutes and 3,000 per day for reads.

Step 2: the script wakes the agent, with a signature

This is where it gets interesting.

A webhook is the opposite of a question. Rather than asking every five minutes whether anything is new, you hand a web address to a program, and it sends you a message when the event happens. You pay nothing as long as nothing happens.

AgentsRoom exposes exactly that: a webhook trigger. You create a trigger in the app, it hands you back a URL and a secret. Anyone who sends a JSON message to that URL opens an agent, with the prompt you wrote and the content of the message already injected into it.

The AgentsRoom tool picker, with a tooltip on the Triggers icon reading "Agent runs on a schedule or a webhook".

The message my script sends is deliberately tiny:

{
  "type": "created",
  "title": "03/09 · 5 x 1000m r' 2'",
  "body": "Session of 03/09/2026 imported from Strava.\n\nQuality session: 5 x 1000m r' 2'\nSplits: 3'41 - 3'40 - 3'38 - 3'40 - 3'35\n\nTotal volume: 12.51 km in 1h07'42 (5:25/km), elev+ 56 m\nPlanned session: RP10-5x1000\n\nSession sheet: journal/2026/2026-09-03.md\nWeek sheet: plan/weeks/2026-W36.md"
}

Notice what is not in there: the text of the plan. The webhook carries the code of the planned session and the path to the sheets, never their content. An agent that has the repository will go and read them itself; an agent that does not has no business receiving my internal instructions. It is the same rule as for descriptions published on Strava.

The signature, and the trap that comes with it

A public URL that opens an agent cannot stay open to anyone who finds it. So the trigger is signed: the script computes a fingerprint of the message with the shared secret (an HMAC-SHA256, if the term means something to you) and sends it in the X-AgentsRoom-Signature header. The server recomputes the same fingerprint on its side; if they do not match, it refuses.

Without a signature, the answer is blunt:

{"error":"REJECTED","message":"Signature missing."}

And here is the trap, which cost me an evening. The signature covers the exact bytes that go out on the wire, not the object in memory. If you sign a file as it sits on disk, then let another layer re-serialise the object (one extra space, a different key order, an accent escaped another way), you get a perfectly valid signature for a message the server will never receive. The rejection is undebuggable: everything looks correct on both sides.

The fix fits in one sentence: serialise and sign in the same place. In the template, the post_json function does both, and nothing else is allowed to touch the body of the message.

Step 3: three layers tell the coach who it is, how things work here, and what to do right now

An agent that coaches is not one big prompt. It is three separate texts, and the separation matters.

Layer 1, the persona: who it is

A system prompt attached to the agent in AgentsRoom. It carries the training philosophy, and it is deliberately generic to the sport: it would coach anyone.

Your job is not simply to generate training plans. You continuously coach the athlete by analyzing their training, understanding their current fitness, adapting upcoming sessions. […] Talk like an experienced coach, not like a motivational chatbot.

It also says what it does not do: not judging a session solely on whether the target pace was held, being explicit about the uncertainty of a race prediction, and not validating a target simply because the athlete wants it. That last line is the one that makes the coach useful.

You do not have to write it yourself: this persona is published in the AgentsRoom agent catalog as Running Performance Coach. One click installs it, ready to use.

Layer 2, CLAUDE.md: how things work here

This is the contract, read at the start of every session. It holds the file layout, the rules that keep it consistent, the training principles that constrain every proposal, and above all the ritual: the exact sequence to run when a session is reported.

One extract, because it shows the level of precision:

Order of sacrifice when the week derails: first the extra minutes on easy runs, then the strength work, then the length of the long run, then one quality session. Never the whole week.

This is where the coach stops being a chatbot. It does not improvise a workflow each time, it follows the one I wrote once. If you only read one file of the template repository, read that one.

Layer 3, the trigger prompt: what to do right now

This is the message handed to the agent when a session lands. It receives the activity through template variables: {{event.title}}, {{event.body}}, {{event.url}}. So the agent starts with the session already in hand, instead of going to look for it.

The AgentsRoom trigger editor, with the name "Coach · {{event.title}}" and the coach prompt, which opens with "New session imported from Strava" then the event variables, the instruction to read CLAUDE.md first, the warning about running unattended, and the first step of the ritual.

Here is its shape, as it stands in the trigger:

New session imported from Strava.

**{{event.title}}** · activity {{event.id}}
{{event.url}}

{{event.body}}

---

You are in the `training-plan` repository. Read `CLAUDE.md` first: it is the law.
You write in my language and you address me directly throughout (§3).

The §6 ritual applies, but its **step 1 is already done**: `strava_publish.py`
created the session sheet and committed it. You resume at step 2 and go all the
way. Three deliverables, in this order: **the analysis in the repository**,
**the comment under the Strava activity**, **the email**.

⚠️ **You are running unattended: nobody will read a question.** Never ask for
arbitration: you decide, you act, and you say in your report what you settled
and why.

## 1 · Analyse and adjust the plan (§6 ritual, steps 2 to 6)

1. `git pull --rebase` first: the sheet may come from the server.
2. Read, in this order: today's sheet, the week sheet,
   `athlete/zones-and-paces.md`, and **the last 3 session sheets**:
   a session is never judged alone.
3. Write the `## Analysis` section: **verdict first**, then the signals that
   carry it, then what it changes.
   ⛔ If `## Analysis` is already filled, do not rewrite it.
4. Update the week sheet and trace **every** plan change under
   `## Adjustments`, with its reason.
5. **Regenerate the `README.md`**: it is the screen I read on my phone.
6. Commit and push, explicit paths, ⛔ never `git add -A`.

## 2 · Kudos and comment on Strava
   ⛔ A Strava comment is PUBLIC: no target heart rate, no niggle,
   no internal trade-off, no predicted finishing time.

## 3 · The full report by email

The line doing the most work is the one in the middle: "nobody will read a question". An agent running with no one in front of the screen that asks for arbitration is not making a mistake, it simply stops, and you find out the next day.

Which model, and why a million tokens is not vanity

SettingValue
ModelClaude Opus, 1M context
Reasoning effortHigh
Permission modeAutonomous
Browser accessOn

The AgentsRoom trigger list, with the row "Coach · {{event.title}}", the webhook label, the source "Any service (JSON)", the Running Performance Coach project, and the agent settings: Opus model, high effort, autonomous mode, browser on.

The long context is not a flourish. To judge one session properly, the coach reads today's sheet, the week sheet, the reference pace table, and the previous three sessions. A session is never judged alone: accumulated load, the sequencing of days and the watch items in progress change the verdict completely. Three reps at 3:38 the day after a two-hour long run do not tell the same story as the same 3:38 after a rest day.

Autonomous mode is not carelessness, it is a consequence: a run with nobody in front of the screen has nobody to approve a git push. And browser access is what lets the agent go and comment on Strava and send the email, two things with no convenient API here.

What is automated, and what deliberately is not

This is the design decision I am happiest with, and it is easy to miss.

The import job logs and publishes, it never judges.

What the script doesWhat it does not do
Fetch new activitiesFill the Analysis section
Create the session sheetTouch the week sheet
Write title and description on StravaTouch the reference paces
Commit the sheets it createdOffer any opinion

A script that started judging would produce verdicts without context, with logic frozen in code that nobody re-reads. Judging means holding together the week's load, the current shape, what was said last time: that is coaching work, and the agent does it, with the whole file in front of it.

The practical benefit is immediate: when the agent has not run (machine off, API down), the sheet exists anyway. Nothing is lost, only the comment is missing, and a replay is enough.

Another choice pulls in the same direction: the script keeps no state file to know what it has already handled. The description on Strava is the source of truth. Empty, it writes; carrying its signature, it moves on; non-empty and unsigned, you wrote it and it does not touch it. A local state file could have said nothing about what another machine did; this way, two machines can run in parallel without stepping on each other.

Two more separation rules, carved into the repository and not to be worked around:

  • the description published on Strava never copies the text of the plan: my week sheet holds target heart rates and trade-offs that have no business on a public activity;
  • a description written by hand is never overwritten.

The comment that lands under the activity

The point is not self-congratulation. It is that the coach's verdict is readable from my phone, under the activity, without opening the repository, and that it stays there, attached to the session, forever.

So the comment is deliberately narrow: a verdict emoji, the number that carries it, and what it changes for the next session. Around 250 characters.

✅ Five reps at 3'39 average for a 3'38-3'44 target, and heart rate flat across the whole block. The pace table holds. Friday stays easy: you have spent this week's margin.

The long version, the one with heart rates, the watch item I flagged and the arbitration on next week's volume, goes into the repository and into the email. Two channels, two audiences, and it is the prompt that holds the boundary.

Three things that only break in production

Each of these lines exists because something broke without it. They are more instructive than the rest of the article.

1. Pin the browser. I have two Claude extensions connected in Chrome. Nothing guarantees which one the agent gets, and only one holds the Strava session. The result: every other run, the agent ended up in the wrong browser, logged out, unable to comment on anything. Selecting the browser by device id is not persisted from one session to the next: it therefore belongs in the prompt, with an explicit ban on going and asking the user which one to pick. Running unattended, a question is a deadlock.

2. The Strava comment field has no maxlength. Nothing in the browser stops you writing too long: it is the server that refuses on submit. An agent that composes a beautiful 600-character paragraph types the whole thing, clicks "Post", and gets a failure it does not understand. So the prompt has to enforce brevity before writing, and plan for the case: if the submit fails, shorten and repost, never split into two comments.

3. One coach comment per activity. When you replay an event to test (which you do a lot at the start), without this rule the agent stacks comments on an activity already handled. So the prompt makes it read the "Comments" tab before writing, and skip its turn if it is already there. Same logic on the repository side: if the ## Analysis section is already filled, it is not rewritten.

What this costs

PieceWhereCost
The coach agentMy machine, through AgentsRoommy Claude subscription
The 15-minute pollA small always-on Linux box~5 €/month, or nothing on a Raspberry Pi
The logA private Git repositoryfree
The Strava APIStrava Developer Programsee Strava's pricing

There is no API key metered per token in this build. That is the point I find most underestimated: the same thing built on a usage-billed API would have a meter running on every session, and I probably would not have kept it.

Build it this weekend

The steps, in order. Count on an evening if you already have a Strava account and a Claude subscription.

1. Clone the template and make it yours.

git clone https://github.com/AgentsRoomDev/running-performance-coach.git my-coach
cd my-coach
rm -rf .git && git init

Make your copy private. A training log holds health data: heart rate, sleep, injuries. The template is public, your copy should not be.

Then fill in, in this order: athlete/profile.md (who you are as a runner), athlete/records.md (your PBs), athlete/constraints.md (the slots you actually have), athlete/zones-and-paces.md (your reference paces), plan/objective.md (the race and the target), then CLAUDE.md, where you replace every {{...}} placeholder.

Finally, open the repository with your Claude agent and say: "read CLAUDE.md and athlete/, then build me the first week."

2. Connect Strava.

cp .env.example .env && chmod 600 .env
python3 scripts/strava_oauth.py     # one browser click, once
python3 scripts/strava_sync.py --dry-run

The --dry-run prints what would be written without writing anything. That is the moment to check that the session reconstruction suits you.

3. Create the trigger in AgentsRoom. Under Triggers, New trigger:

FieldValue
KindWebhook, source generic
Promptthe content of docs/trigger-prompt.md
Role / personadocs/coach-persona.md
Permission modeAutonomous
Browser accessOn

AgentsRoom mints a URL and a signing secret. Put both in your .env:

WEBHOOK_URL=https://agentsroom.dev/api/triggers/t_xxxxxxxxxxxx
WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxx

4. Test it before trusting it.

python3 scripts/webhook_replay.py scripts/examples/webhook-session.json --dry-run
python3 scripts/webhook_replay.py scripts/examples/webhook-session.json

This replays a session into the trigger without waiting for your next run and without touching the state of the automated job. You should see ✅ HTTP 202, and an agent tab should open in AgentsRoom.

5. Run it every 15 minutes.

bash scripts/systemd/install.sh          # on a Linux server

A oneshot unit plus a timer: no resident process, and a pass missed while the machine was off is caught up on the next boot.

If you have no always-on machine, skip this step: run strava_sync.py by hand whenever you feel like it, or simply describe your session to the agent in a conversation. The ritual in CLAUDE.md works identically. You lose the automation, not the coach.

What I take from this, beyond running

Nothing about this build is specific to running. What it shows is a reusable pattern for just about any domain where you accumulate personal data and would like a competent opinion on it.

Three parts, and that is all. A Git repository of Markdown files as a memory readable by the machine and by you. An event that wakes an agent instead of an agent polling in a loop and burning tokens for nothing. Three configuration layers that cleanly separate who the agent is, how it works at your place, and what it should do right now.

Replace "running session" with "bank statement", "coding session", "blood-sugar reading" or "reading note": the mechanism does not change.

Frequently asked questions

Do I need to know how to code to build an AI running coach?

You need to be able to run a command in a terminal and edit a text file. The template repository is ready to clone, the Python scripts use nothing but the standard library (no pip install), and the coaching part is configured by writing plain prose in Markdown files. The real work is not technical: it is describing honestly who you are as a runner and what you are aiming at.

How much does it cost per month?

The agent runs on the Claude subscription you already have (Pro or Max): there is no API key metered per token. On top of that you may want a small always-on machine to poll Strava every 15 minutes, around 5 euros a month on a VPS, or nothing at all on a Raspberry Pi. The private Git repository is free. That leaves the Strava API, which has required a paid developer subscription since June 2026.

Why a Git repository instead of a database?

Because the history becomes readable, by the coach and by you. Every session is a Markdown file, every plan change is a commit with its reason. The agent can re-read what it prescribed three weeks ago and check whether it worked, and you read your plan on your phone in the GitHub app, without writing a line of interface.

What is a webhook, in plain terms?

A webhook is a service that calls you instead of you calling it. Rather than asking every five minutes whether anything is new, you hand a web address to a program, and it sends you a message when the event happens. Here, the script that imports the session sends that message to AgentsRoom, which opens a Claude agent within the second. That is also what makes the build cheap: an agent that polls in a loop burns tokens on every turn, a webhook trigger costs nothing until something happens.

Does this work for a sport other than running?

Yes. The import reconstructs watch laps, and cycling and swimming record laps too. What changes is the strategy files and the session catalogue, which are text you rewrite. The mechanism (import, webhook, agent, repository) does not move.

Can the agent get it wrong and wreck my plan?

It can get things wrong, but it cannot wreck much: everything it writes is a Git commit you can read, argue with and revert. The CLAUDE.md file explicitly forbids it from rewriting history, inventing data you did not provide, changing the plan without recording the reason, and giving medical advice. Suspicious pain, and it sends you to a professional.


The template repository is here: AgentsRoomDev/running-performance-coach. Clone it, fill in your paces, and you have your coach. If you want to see the part that wakes the agent, it is described on the webhook triggers page, and AgentsRoom downloads here.

Download AgentsRoom

Run all your AI agents, on all your projects, from a single window.

FreeDownload AgentsRoom

Companion app: monitor your agents on the go

Bring your own: Claude, Codex, Antigravity CLI, or other AI provider.

Get the extension
Chrome Web Store

Push bugs and requests straight to your public backlog.

A glimpse of AgentsRoom in action.

Multiple projects
Multi-provider
Multiple agents
Live status
File diff & commit
Mobile companion
Live preview
Agent teams
Browser automation
Backlog-driven dev
Prompt Library
Skills Library
View all features

Keep reading