Garmin Connect
Query your Garmin Connect account from the CLI. Zero npm dependencies, single-file Bun script. Auth uses the mobile JSON login API (the flow Garmin actually supports for third-party clients as of March 2026) and caches tokens locally — MFA is re-prompted only when the ~1-year OAuth1 token expires.
When to use
Use this skill whenever the user asks a question that requires data from their Garmin watch/account. Examples:
- "How did I sleep last night?"
- "How many gym sessions this week?"
- "What's my average resting HR this month?"
- "Show me my last run"
- "What's my training readiness today?"
- "Did I hit my step goal yesterday?"
For single metrics, call one subcommand. For analytical questions, pull summary or weekly and reason over the JSON.
Usage pattern
- First run: user sets env vars and invokes
login. If MFA is on, two-phase.
- Subsequent runs: any subcommand. Tokens auto-refresh.
- Answer the question: parse the JSON output and respond conversationally.
Always prefer --pretty when piping into your own context — compact JSON is for piping into other tools.
Environment setup (one-time)
export GARMIN_EMAIL="your@email.com"
export GARMIN_PASSWORD="yourpassword"
# Optional: persist in ~/.config/garmin-api/env and source from shell rc.
First login with MFA:
# Phase 1 — triggers the email/app MFA prompt and exits with code 2.
bun run scripts/garmin.ts login
# → "[auth] MFA required (email) — code sent."
# Phase 2 — supply the code (≤10 minutes after phase 1).
GARMIN_MFA=123456 bun run scripts/garmin.ts login
# → "[auth] authenticated as <displayName> — tokens cached."
Without MFA: bun run scripts/garmin.ts login completes in one call.
Subcommands
# Identity
bun run scripts/garmin.ts whoami --pretty
# Single-date queries (default: today)
bun run scripts/garmin.ts daily 2026-04-14 --pretty
bun run scripts/garmin.ts sleep 2026-04-14 --pretty
bun run scripts/garmin.ts hrv 2026-04-14 --pretty
bun run scripts/garmin.ts readiness 2026-04-14 --pretty
bun run scripts/garmin.ts training 2026-04-14 --pretty
# Bundle for one day (daily + sleep + hrv + readiness + training + activities)
bun run scripts/garmin.ts summary 2026-04-14 --pretty
# Activity list (date range, inclusive)
bun run scripts/garmin.ts activities --from 2026-04-08 --to 2026-04-14 --pretty
bun run scripts/garmin.ts activities --from 2026-04-08 --to 2026-04-14 --limit 100
# 7-day window ending on <date> (default today)
bun run scripts/garmin.ts weekly 2026-04-14 --pretty
All commands print JSON to stdout. Errors (including MFA prompts) go to stderr.
Analytical patterns
When the user asks an analytical question, pick the smallest query that contains the answer, then reason over the JSON.
| User question |
Command |
What to extract |
| "How did I sleep last night?" |
sleep <yesterday> |
dailySleepDTO.sleepScores, stage durations, total sleep time |
| "Steps today?" |
daily <today> |
totalSteps, dailyStepGoal |
| "Gym this week?" |
activities --from <mon> --to <sun> |
count entries where activityType.typeKey matches strength_training|fitness_equipment|indoor_cardio |
| "Runs this month?" |
activities --from <month-start> --to <today> |
filter typeKey starts with running |
| "Resting HR trend?" |
weekly <today> |
per-day daily.restingHeartRate |
| "HRV status?" |
hrv <today> |
hrvSummary.status, weeklyAvg, lastNightAvg |
| "Recovery?" |
readiness <today> + training <today> |
readiness score + training status |
| "Last workout?" |
activities --from <7-days-ago> --to <today> --limit 1 |
most recent entry |
For "this week" / "this month": compute the date range yourself before calling. Garmin's API is always date-explicit.
Activity type keys (partial)
Common activityType.typeKey values to filter on:
running, treadmill_running, trail_running, track_running, indoor_running
walking, hiking, casual_walking
cycling, road_biking, mountain_biking, indoor_cycling, virtual_ride
strength_training, fitness_equipment, indoor_cardio
swimming, lap_swimming, open_water_swimming
yoga, pilates, stretching
other
Failure modes
| Symptom |
Cause |
Fix |
MFA_REQUIRED exit code 2 |
Phase 1 of login succeeded |
Re-run with GARMIN_MFA=<code> within 10 min |
401 Unauthorized on preauthorized |
OAuth1 consumer secret rotated (auto-recovers) |
On next run the script refetches from OAUTH_CONSUMER_URL and retries once. If it still fails, see Self-repair below. |
404 Not Found on login |
Endpoint moved |
See Self-repair below. |
429 or 403 on login |
Cloudflare rate limit or UA block |
Wait 10+ minutes |
No valid cached tokens |
First run or cache cleared |
Run login subcommand |
Self-repair (for Claude)
This skill is designed to be patched in place when Garmin changes its auth or API surface. The compatibility surface is ~6 GET endpoints + the auth flow, and all of them live in scripts/garmin.ts. When something breaks, follow this escalation:
Step 1 — Check if consumer creds rotated
Symptom: 401 Unauthorized on /oauth-service/oauth/preauthorized with "Invalid signature for signature method HMAC-SHA1".
The script already auto-refreshes from https://thegarth.s3.amazonaws.com/oauth_consumer.json (24h cache, retry-on-401). If that URL itself stops working:
- Check the garth repo for the current canonical source of the consumer pair. Look at
garth/client.py or the sso.py constants.
- If
thegarth.s3.amazonaws.com is dead, find the new host in garth's OAUTH_CONSUMER_URL. Update the OAUTH_CONSUMER_URL constant in scripts/garmin.ts.
- As a last resort, inspect the current Garmin Connect mobile app traffic (mitmproxy + a rooted Android emulator) and read the pair off the first OAuth1-signed request.
Step 2 — Check if the login endpoint moved
Symptom: 404 Not Found on /sso/mobile/api/login or /sso/mobile/api/mfa/verifyCode.
- Install the latest garth:
uvx --with garth python3 -c "import garth.sso as s, inspect; print(inspect.getsource(s.login))"
- Compare the URL constants, method, body shape, and
clientId / service query params against loginInit() / loginComplete() in scripts/garmin.ts.
- Port any deltas. Things to watch:
SSO_CLIENT_ID, SSO_SERVICE_URL, the JSON body field names (rememberMe, captchaToken, mfaVerificationCode, etc.), and the responseStatus.type values (SUCCESSFUL, MFA_REQUIRED).
Step 3 — Check if the User-Agent fingerprint is being blocked
Symptom: 403 immediately, even from a fresh IP.
- Garmin cycles accepted mobile UAs. Current values:
- SSO endpoints expect an iPhone Safari UA (
SSO_MOBILE_UA constant).
- Connectapi endpoints expect
GCM-iOS-5.22.1.4 (CONNECTAPI_UA constant).
- Check garth's
http.py for the current USER_AGENT dict. Update the constants in scripts/garmin.ts to match.
Step 4 — Check if an API endpoint path or response shape changed
Symptom: 404 on one of the data endpoints (e.g. /usersummary-service/usersummary/daily/...), or JSON.parse failure.
- Log into Garmin Connect in a browser, open DevTools → Network tab.
- Find the equivalent request (the web app also hits
connectapi.garmin.com via a CSRF-protected proxy; the path is the part after /gc-api/).
- Update the path in the corresponding method on the
Api object in scripts/garmin.ts.
- Update
references/endpoints.md with the new path.
Step 5 — Verify with a fresh login
After any change:
rm -f ~/.config/garmin-api/tokens.json ~/.config/garmin-api/consumer.json
bun run scripts/garmin.ts login
# Phase 2 if MFA: GARMIN_MFA=xxxxxx bun run scripts/garmin.ts login
bun run scripts/garmin.ts whoami --pretty
bun run scripts/garmin.ts summary $(date +%F) --pretty | head -40
If all three succeed, the skill is healthy again.
What NOT to do
- Don't add new npm dependencies — the whole point is a zero-dep single file.
- Don't try to impersonate the web app's
connect-csrf-token session flow — it's more brittle than the mobile SSO flow and couples you to cookies.
- Don't hardcode a user-specific value (displayName, ticket, token). They go in
~/.config/garmin-api/tokens.json.
Files
scripts/garmin.ts — the CLI (single file, zero deps beyond Bun)
references/endpoints.md — Garmin endpoints this skill calls
README.md — human-facing intro and setup
Not in scope
- Writing data back to Garmin (activity uploads, settings, etc.)
- Syncing to external systems — that's a downstream concern
- Non-Garmin data sources (Apple Watch, Whoop, Oura, etc.)
1---2name: garmin-connect3description: Connect to Garmin Connect and query personal health/fitness data — steps, sleep, heart rate, HRV, body battery, training readiness, activities (runs, walks, gym, cycling, swimming). Use when the user asks about their Garmin data, watch data, sleep, workouts, runs, rides, HRV, recovery, training status, or any fitness metric. Supports ad-hoc analytical questions like "how many times did I go to the gym this week?", "how did I sleep last night?", "what's my running pace trend?", "how's my HRV trending?".4license: MIT5---67# Garmin Connect89Query your Garmin Connect account from the CLI. Zero npm dependencies, single-file Bun script. Auth uses the mobile JSON login API (the flow Garmin actually supports for third-party clients as of March 2026) and caches tokens locally — MFA is re-prompted only when the ~1-year OAuth1 token expires.1011## When to use1213Use this skill whenever the user asks a question that requires data from their Garmin watch/account. Examples:1415- "How did I sleep last night?"16- "How many gym sessions this week?"17- "What's my average resting HR this month?"18- "Show me my last run"19- "What's my training readiness today?"20- "Did I hit my step goal yesterday?"2122For single metrics, call one subcommand. For analytical questions, pull `summary` or `weekly` and reason over the JSON.2324## Usage pattern25261. **First run**: user sets env vars and invokes `login`. If MFA is on, two-phase.272. **Subsequent runs**: any subcommand. Tokens auto-refresh.283. **Answer the question**: parse the JSON output and respond conversationally.2930Always prefer `--pretty` when piping into your own context — compact JSON is for piping into other tools.3132## Environment setup (one-time)3334```bash35export GARMIN_EMAIL="your@email.com"36export GARMIN_PASSWORD="yourpassword"37# Optional: persist in ~/.config/garmin-api/env and source from shell rc.38```3940First login with MFA:4142```bash43# Phase 1 — triggers the email/app MFA prompt and exits with code 2.44bun run scripts/garmin.ts login45# → "[auth] MFA required (email) — code sent."4647# Phase 2 — supply the code (≤10 minutes after phase 1).48GARMIN_MFA=123456 bun run scripts/garmin.ts login49# → "[auth] authenticated as <displayName> — tokens cached."50```5152Without MFA: `bun run scripts/garmin.ts login` completes in one call.5354## Subcommands5556```bash57# Identity58bun run scripts/garmin.ts whoami --pretty5960# Single-date queries (default: today)61bun run scripts/garmin.ts daily 2026-04-14 --pretty62bun run scripts/garmin.ts sleep 2026-04-14 --pretty63bun run scripts/garmin.ts hrv 2026-04-14 --pretty64bun run scripts/garmin.ts readiness 2026-04-14 --pretty65bun run scripts/garmin.ts training 2026-04-14 --pretty6667# Bundle for one day (daily + sleep + hrv + readiness + training + activities)68bun run scripts/garmin.ts summary 2026-04-14 --pretty6970# Activity list (date range, inclusive)71bun run scripts/garmin.ts activities --from 2026-04-08 --to 2026-04-14 --pretty72bun run scripts/garmin.ts activities --from 2026-04-08 --to 2026-04-14 --limit 1007374# 7-day window ending on <date> (default today)75bun run scripts/garmin.ts weekly 2026-04-14 --pretty76```7778All commands print JSON to stdout. Errors (including MFA prompts) go to stderr.7980## Analytical patterns8182When the user asks an analytical question, pick the smallest query that contains the answer, then reason over the JSON.8384| User question | Command | What to extract |85|---|---|---|86| "How did I sleep last night?" | `sleep <yesterday>` | `dailySleepDTO.sleepScores`, stage durations, total sleep time |87| "Steps today?" | `daily <today>` | `totalSteps`, `dailyStepGoal` |88| "Gym this week?" | `activities --from <mon> --to <sun>` | count entries where `activityType.typeKey` matches `strength_training\|fitness_equipment\|indoor_cardio` |89| "Runs this month?" | `activities --from <month-start> --to <today>` | filter `typeKey` starts with `running` |90| "Resting HR trend?" | `weekly <today>` | per-day `daily.restingHeartRate` |91| "HRV status?" | `hrv <today>` | `hrvSummary.status`, `weeklyAvg`, `lastNightAvg` |92| "Recovery?" | `readiness <today>` + `training <today>` | readiness score + training status |93| "Last workout?" | `activities --from <7-days-ago> --to <today> --limit 1` | most recent entry |9495For "this week" / "this month": compute the date range yourself before calling. Garmin's API is always date-explicit.9697## Activity type keys (partial)9899Common `activityType.typeKey` values to filter on:100101- `running`, `treadmill_running`, `trail_running`, `track_running`, `indoor_running`102- `walking`, `hiking`, `casual_walking`103- `cycling`, `road_biking`, `mountain_biking`, `indoor_cycling`, `virtual_ride`104- `strength_training`, `fitness_equipment`, `indoor_cardio`105- `swimming`, `lap_swimming`, `open_water_swimming`106- `yoga`, `pilates`, `stretching`107- `other`108109## Failure modes110111| Symptom | Cause | Fix |112|---|---|---|113| `MFA_REQUIRED` exit code 2 | Phase 1 of login succeeded | Re-run with `GARMIN_MFA=<code>` within 10 min |114| `401 Unauthorized` on preauthorized | OAuth1 consumer secret rotated (auto-recovers) | On next run the script refetches from `OAUTH_CONSUMER_URL` and retries once. If it still fails, see *Self-repair* below. |115| `404 Not Found` on login | Endpoint moved | See *Self-repair* below. |116| `429` or `403` on login | Cloudflare rate limit or UA block | Wait 10+ minutes |117| `No valid cached tokens` | First run or cache cleared | Run `login` subcommand |118119## Self-repair (for Claude)120121This skill is designed to be patched in place when Garmin changes its auth or API surface. The compatibility surface is ~6 GET endpoints + the auth flow, and all of them live in `scripts/garmin.ts`. When something breaks, follow this escalation:122123### Step 1 — Check if consumer creds rotated124125Symptom: `401 Unauthorized` on `/oauth-service/oauth/preauthorized` with `"Invalid signature for signature method HMAC-SHA1"`.126127The script already auto-refreshes from `https://thegarth.s3.amazonaws.com/oauth_consumer.json` (24h cache, retry-on-401). If that URL itself stops working:1281291. Check the [garth repo](https://github.com/matin/garth) for the current canonical source of the consumer pair. Look at `garth/client.py` or the `sso.py` constants.1302. If `thegarth.s3.amazonaws.com` is dead, find the new host in garth's `OAUTH_CONSUMER_URL`. Update the `OAUTH_CONSUMER_URL` constant in `scripts/garmin.ts`.1313. As a last resort, inspect the current Garmin Connect mobile app traffic (mitmproxy + a rooted Android emulator) and read the pair off the first OAuth1-signed request.132133### Step 2 — Check if the login endpoint moved134135Symptom: `404 Not Found` on `/sso/mobile/api/login` or `/sso/mobile/api/mfa/verifyCode`.1361371. Install the latest garth: `uvx --with garth python3 -c "import garth.sso as s, inspect; print(inspect.getsource(s.login))"`1382. Compare the URL constants, method, body shape, and `clientId` / `service` query params against `loginInit()` / `loginComplete()` in `scripts/garmin.ts`.1393. Port any deltas. Things to watch: `SSO_CLIENT_ID`, `SSO_SERVICE_URL`, the JSON body field names (`rememberMe`, `captchaToken`, `mfaVerificationCode`, etc.), and the `responseStatus.type` values (`SUCCESSFUL`, `MFA_REQUIRED`).140141### Step 3 — Check if the User-Agent fingerprint is being blocked142143Symptom: `403` immediately, even from a fresh IP.1441451. Garmin cycles accepted mobile UAs. Current values:146 - SSO endpoints expect an iPhone Safari UA (`SSO_MOBILE_UA` constant).147 - Connectapi endpoints expect `GCM-iOS-5.22.1.4` (`CONNECTAPI_UA` constant).1482. Check garth's `http.py` for the current `USER_AGENT` dict. Update the constants in `scripts/garmin.ts` to match.149150### Step 4 — Check if an API endpoint path or response shape changed151152Symptom: `404` on one of the data endpoints (e.g. `/usersummary-service/usersummary/daily/...`), or `JSON.parse` failure.1531541. Log into Garmin Connect in a browser, open DevTools → Network tab.1552. Find the equivalent request (the web app also hits `connectapi.garmin.com` via a CSRF-protected proxy; the path is the part after `/gc-api/`).1563. Update the path in the corresponding method on the `Api` object in `scripts/garmin.ts`.1574. Update `references/endpoints.md` with the new path.158159### Step 5 — Verify with a fresh login160161After any change:162163```bash164rm -f ~/.config/garmin-api/tokens.json ~/.config/garmin-api/consumer.json165bun run scripts/garmin.ts login166# Phase 2 if MFA: GARMIN_MFA=xxxxxx bun run scripts/garmin.ts login167bun run scripts/garmin.ts whoami --pretty168bun run scripts/garmin.ts summary $(date +%F) --pretty | head -40169```170171If all three succeed, the skill is healthy again.172173### What NOT to do174175- Don't add new npm dependencies — the whole point is a zero-dep single file.176- Don't try to impersonate the web app's `connect-csrf-token` session flow — it's more brittle than the mobile SSO flow and couples you to cookies.177- Don't hardcode a user-specific value (displayName, ticket, token). They go in `~/.config/garmin-api/tokens.json`.178179## Files180181- `scripts/garmin.ts` — the CLI (single file, zero deps beyond Bun)182- `references/endpoints.md` — Garmin endpoints this skill calls183- `README.md` — human-facing intro and setup184185## Not in scope186187- Writing data back to Garmin (activity uploads, settings, etc.)188- Syncing to external systems — that's a downstream concern189- Non-Garmin data sources (Apple Watch, Whoop, Oura, etc.)