Skip to content

denki-san/garmin-sync

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

garmin-sync

A Garmin skill for AI agents — syncs your Garmin Connect health data to local JSON so Claude Code / Codex / Hermes / ChatGPT can answer your daily "how am I doing?" questions.

Natively supports both garmin.com and garmin.cn (China region).

English | 简体中文

skills.sh

garmin-sync is a small Python CLI + library that pulls each day's health metrics from your Garmin account and writes them to disk as structured JSON. There's no daemon, no third-party server, no cloud — just a script and a folder of files your AI assistant can read.

# As an Agent Skill (Claude Code / Codex / Cursor / etc.)
npx skills add denki-san/garmin-sync

# Or as a plain Python package
pip install garmin-sync          # core (garminconnect under the hood)
pip install 'garmin-sync[plots]' # add matplotlib trend plots
HRV trend
HRV (nightly)
Sleep score trend
Sleep score

Sample plots: 7 days from the maintainer's account (2026-05-17 → 23). Each generated by garmin-sync plot --metric <name> --days 7 --rolling 3.

Designed as a Garmin skill, not a full-archive tool

garmin-sync is built as a skill an AI agent installs once to give you a fresh, structured snapshot of your body each morning. The design follows from that:

  • Skill-shaped: ships a SKILL.md so any agent can drop it in and immediately know how to use it
  • Agent-first I/O: one JSON per day, no DB, no schema migration — any LLM can just cat it
  • Daily-tracking, not full-archive: ~13 requests/day, runs in seconds from a daily cron
  • Health-first scope: sleep / HRV / stress / Body Battery / RHR — the "how am I doing" metrics
  • Training data: planned, not promised: more activity/training fields will land as I need them; PRs welcome

We deliberately skip:

  • GPS tracks, FIT file ingestion, per-second cadence / power / heart-rate streams from activities
  • PMC (CTL/ATL/TSB), training load, Training Effect, recovery time
  • Workout / training-plan management

activities and training_readiness are included but only at the summary level (name + duration + distance + calories for activities; overall score + status for readiness). If you need full training analytics, nrvim/garmin-givemydata or tcgoetz/GarminDB cover that territory better.

What gets synced

One JSON file per day (e.g. 2026-05-28.json). Top-level keys:

Key What it covers
🛏️ sleep Score, start/end, stage breakdown (deep/light/REM/awake/nap), sleep-window SpO2 + HR + respiration + stress
👣 steps Total steps, distance, goal
📊 hrv Weekly avg, last-night avg, 5-min high, balanced baseline + marker, status, feedback phrase
🩸 spo2 Daytime SpO2 — avg, min, avg HR during readings
🔋 body_battery Charged, drained, daily max, daily min
❤️ resting_heart_rate RHR + min/max HR when available
🫀 heart_rate Daily HR range (min / max / resting) + 7-day avg RHR
🔥 calories Total / active / BMR (Garmin-computed energy expenditure)
⬆️ floors Floors ascended / descended / daily goal
⏱️ activity_seconds Daily time budget — sleeping / sedentary / active / highly active
💨 vo2_max Running + cycling values (last reported, since updates are sparse)
😰 stress Overall 0–100 + duration in rest / low / medium / high buckets
🫁 respiration Lowest / highest / avg / awake respiration rates
💪 intensity_minutes Moderate + vigorous + weekly goal
🚦 training_readiness (summary only) Overall score + per-factor + status
🏃 activities (summary only) List of {name, duration_sec, distance_km, calories} per activity

See the full sample JSON further down.

Prerequisite: your data must already be on Garmin Connect

Important

garmin-sync reads from Garmin's cloud, not from your watch. If your watch hasn't synced to the Garmin Connect mobile app — and the app to the cloud — for a given day, that day's JSON will be empty or missing fields.

garmin-sync reads from Garmin's web API. It does not talk to your watch directly. The data flow is:

Watch  →  Garmin Connect app on your phone  →  Garmin Connect cloud  →  garmin-sync  →  your disk

If your watch hasn't synced to the mobile app — or the app hasn't pushed to the cloud — for a given day, that day's JSON will be empty or missing fields. To force a sync before running garmin-sync:

  1. Open the Garmin Connect app on your phone.
  2. Make sure the watch is in Bluetooth range and pull-to-refresh on the home screen.
  3. Wait for the "Last synced" timestamp to update.

This is why running garmin-sync sync --days 1 minutes after midnight often returns half-empty data: the watch hasn't been near the phone yet.

Why this exists

The Garmin Connect app is fine for a quick glance, but you can't ask it "how has my HRV trended against my sleep score over the last month?" or "did my Body Battery debt correlate with the headache I had on Thursday?" — those are LLM-shaped questions, and they need the data on disk in a format an LLM can chew on.

garmin-sync is the boring plumbing. Once a day's JSON is on disk, everything else (analysis, reports, alerts, plots) is just reading files.

Not what you need?

If your goal is archive + retrospective analysis rather than daily agent tracking, one of these is likely a better fit:

Full feature-by-feature comparison below.

Comparison with adjacent projects

garmin-sync nrvim/garmin-givemydata ⭐108 tcgoetz/GarminDB ⭐3.1k diegoscarabelli/garmin-health-data ⭐61
Scope Health-only (~14 metrics) Everything — 48 SQLite tables incl. FIT files Everything — daily + activities + FIT files Everything — health + activities (FIT/TCX) + training
Storage One JSON file per day SQLite + raw FIT copies SQLite + raw FIT copies SQLite + raw files (4-folder lifecycle)
AI interface Read JSON files (any tool) Built-in MCP server (45 tools) Jupyter notebooks Read SQLite (documented schema)
Backfill speed seconds per day ~30 min for 10 years (first run) similar to givemydata resumable, single-pass
Install pip install pip install / brew tap pip install / make pip install
Auth garminconnect (curl_cffi) garminconnect (similar) own garth-based stack self-contained SSO client
Region International + China (garmin.cn) International only International only International only
License MIT AGPL-3.0 GPL-2.0 Apache-2.0

TL;DR: if you want the full Garmin firehose with an MCP server, go garmin-givemydata. If you want mature SQL tables + Jupyter notebooks, go GarminDB. If you want lightweight per-day JSON files an AI assistant can read directly, that's this one.

Quick start

This section is written so an AI agent can install garmin-sync from start to finish without further prompts. Human readers can follow the same steps directly.

Step 1 — Place the skill files

Detect which AI agent skill directories exist on this machine:

Path Agent
~/.claude Claude Code
~/.openclaw OpenClaw
~/.hermes Hermes
~/.config/opencode OpenCode

For each detected directory $AGENT_DIR, clone this repo into its skills/ subdirectory:

mkdir -p "$AGENT_DIR/skills"
git clone https://github.com/denki-san/garmin-sync \
  "$AGENT_DIR/skills/garmin-sync"

If $AGENT_DIR/skills/garmin-sync already exists, run git -C "$AGENT_DIR/skills/garmin-sync" pull --ff-only to update instead.

If none of the four directories exist, ask the user which AI tool they plan to use this skill with, then create that directory and treat it as the only target.

Step 2 — Install the Python package

pip install git+https://github.com/denki-san/garmin-sync
# Or, with optional plotting support:
pip install 'git+https://github.com/denki-san/garmin-sync#egg=garmin-sync[plots]'

This installs the garmin-sync CLI on $PATH. All AI agents on this machine share the same CLI — install once. Requires Python 3.10 or later.

Step 3 — Configure a profile

Ask the user for:

  • their Garmin email
  • their region (garmin.com international or garmin.cn China)
  • where they want JSON files written (default: ~/.local/share/garmin-sync)

Write ~/.config/garmin-sync/profiles.toml:

[profiles.me]
email      = "you@example.com"
domain     = "garmin.com"
token_dir  = "~/.garminconnect-garmin_com"
output_dir = "~/.local/share/garmin-sync"

For multiple users (family, spouse), see docs/multi-user.md.

Step 4 — One-time SSO authorization

Ask the user for their Garmin password (export it as $GARMIN_PASSWORD or set password_env_var in the profile), then run:

garmin-sync setup --profile me

If Garmin requires MFA, the CLI will interactively prompt for the 6-digit code — relay the prompt to the user. Tokens cache under token_dir and auto-refresh while syncing continues; re-run setup only if the password changes or a stale-token error occurs.

See docs/auth-troubleshooting.md for non-interactive (TOTP) workflows.

Step 5 — Run a sync

garmin-sync sync --profile me --days 1     # yesterday
garmin-sync sync --profile me --days 30    # backfill 30 days

Verify the file landed:

test -f "$(dirname "$(garmin-sync sync --profile me --days 1 2>&1 | grep -oE '/.*\.json' | head -1)")/$(date -v-1d +%Y-%m-%d).json" \
  && echo OK
# Linux: replace `date -v-1d` with `date -d yesterday`

If the JSON is missing fields the user expects (Body Battery, HRV, etc.), the watch may not have pushed that day's data to Garmin's cloud yet — see Prerequisite above.

Step 6 — (Optional) Schedule a daily sync

garmin-sync runs on demand — installing it does not start a daemon or register a cron job. To keep data fresh automatically, add a cron entry:

30 6 * * * GARMIN_PASSWORD='...' /usr/local/bin/garmin-sync sync --profile me --days 1 >> /var/log/garmin-sync.log 2>&1

Or use the host agent's scheduler (Hermes cron jobs, launchd on macOS, systemd timers on Linux, etc.).

Important

Open the Garmin Connect mobile app and pull-to-refresh before the scheduled sync runs. garmin-sync reads from Garmin's cloud, so if the watch hasn't pushed today's data yet, the sync will write a half-empty JSON. Keep the phone next to the watch overnight so the morning push happens automatically.

Sample output

{
  "date": "2026-05-28",
  "display_name": "Lei",
  "sleep": {
    "score": 88,
    "start": "2026-05-28 00:56",
    "end": "2026-05-28 08:30",
    "stages": {
      "total_min": 450, "deep_min": 114, "light_min": 272, "rem_min": 64,
      "awake_min": 4, "avg_spo2": 93.0, "lowest_spo2": 86,
      "avg_spo2_hr": 60.0, "avg_respiration": 12.0,
      "lowest_respiration": 10.0, "avg_sleep_stress": 10.0
    }
  },
  "steps": {"total": 8833, "distance_km": 7.269, "goal": 7540},
  "hrv": {
    "weekly_avg_ms": 47, "last_night_ms": 46, "status": "BALANCED",
    "last_night_5_min_high_ms": 61,
    "baseline": {"balanced_low": 39, "balanced_upper": 51, "marker_value": 0.58},
    "feedback_phrase": "HRV_BALANCED_6"
  },
  "spo2":              {"avg_pct": 93.0, "min_pct": 86, "avg_hr_bpm": 60.0},
  "body_battery":      {"charged": 86, "drained": 92, "max": 99, "min": 7},
  "resting_heart_rate":{"value": 56.0},
  "heart_rate":        {"min": 54, "max": 130, "resting": 56, "last_7d_avg_resting": 56},
  "calories":          {"total_kcal": 2556, "active_kcal": 537, "bmr_kcal": 2019},
  "floors":            {"ascended": 0.0, "descended": 6.45, "goal": 10},
  "activity_seconds":  {"highly_active_sec": 977, "active_sec": 8202, "sedentary_sec": 49981, "sleeping_sec": 27240},
  "vo2_max":           {"running": 43.0, "running_precise": 42.5},
  "stress":            {"overall": 43, "level": "", "rest_min": 494, "low_min": 189, "medium_min": 259, "high_min": 288},
  "respiration":       {"low": 9.0, "high": 22.0},
  "intensity_minutes": {"moderate_min": 3, "vigorous_min": 0, "weekly_goal_min": 150}
}

API budget

A single sync of one day makes 13–14 HTTP requests (14 if VO2 Max isn't reported for that day — the fetcher widens to a 1-year range). For a daily cron job this is comfortably inside Garmin's per-account limits; backfilling --days 365 once is also fine. Tight loops (e.g. --days 1 in a busy loop) will eventually trip rate limiting.

CSV export

garmin-sync export-csv --profile me --start 2026-05-01 --end 2026-05-29 --out ~/garmin-may.csv

Flattens the daily JSON into one row per day. Column order is fixed; new metrics are appended at the end across releases so existing spreadsheets don't break.

Full header row (44 columns)
date, sleep_score, sleep_total_min, sleep_deep_min, sleep_light_min, sleep_rem_min,
sleep_awake_min, sleep_avg_spo2, sleep_lowest_spo2, sleep_avg_respiration,
sleep_avg_stress, steps_total, steps_distance_km, steps_goal, hrv_weekly_avg_ms,
hrv_last_night_ms, hrv_status, hrv_5min_high_ms, hrv_baseline_low,
hrv_baseline_upper, spo2_avg_pct, spo2_min_pct, spo2_avg_hr_bpm,
body_battery_charged, body_battery_drained, body_battery_max, body_battery_min,
stress_overall, stress_level, stress_rest_min, stress_low_min, stress_medium_min,
stress_high_min, respiration_low, respiration_high, respiration_avg,
intensity_moderate_min, intensity_vigorous_min, intensity_weekly_goal_min,
training_readiness_score, training_readiness_status, resting_heart_rate,
vo2_max_running, vo2_max_cycling, activities_count

Important

Missing values are written as empty strings, not as 0 or NaN, so downstream tools can distinguish "no data" from "value was zero".

Trend plots

The two examples at the top of this README were generated by plot:

pip install 'garmin-sync[plots]'
garmin-sync plot --profile me --metric hrv --days 30 --out hrv.png

Single-metric line chart + 7-day rolling mean. Missing days break the line (no zero-fill). Headless-safe (Agg backend). Run garmin-sync plot --list-metrics for the supported metric list.

FAQ

Does it work with garmin.cn (China region) accounts?

Warning

Sleep, steps, HRV, SpO2, stress, intensity minutes, daily summary, and activities are confirmed working. Body Battery, Resting HR, VO2 Max, Training Readiness, and Respiration are almost certainly 404 on garmin.cn (based on community reports + path-prefix inference — we haven't tested directly on a .cn account). If you have a CN account and need those, you'll need Garmin support to migrate the account region.

Does it sync on a schedule automatically? No. garmin-sync runs only when you invoke it (or when your AI agent does so via the skill). For a daily snapshot, schedule it yourself — see Quick start.

Does it handle MFA? Yes. setup prompts for the 6-digit code interactively. Token persistence means one prompt per setup, not per sync.

Where do tokens / passwords go? Tokens: plaintext JSON in token_dir (default ~/.garminconnect-<domain>/). Passwords: only read from env vars (or ~/.hermes/.env if present); nothing is ever written back.

Will it get rate-limited? One sync --days N = ~13×N HTTP requests. Daily cron is fine. Burst backfilling 1–2 years works. Looping sync --days 1 in a busy loop will eventually 429 you.

Status

Pre-1.0. The JSON schema is "stable enough that I use it daily" but I may add fields. Removing or renaming an existing field requires a minor version bump.

License

MIT

About

A Garmin Connect health-sync skill for AI agents — daily JSON, supports .com + .cn, MFA, multi-profile.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages