← All Writing
February 28, 20269 min read

How I Automated Daily Garmin Recaps to My Inbox

Building a Mac automation that fetches Garmin health data at 7am, generates AI recaps with Claude CLI, and emails them — built across two sessions with Claude.ai and Claude Code

YieldDaily health recaps emailed to your inbox every morning with zero manual work
DifficultyIntermediate (Python API integration, launchd scheduling, Claude CLI automation)
Total Cook Time~2 hours initial build (Feb 26) + 45 minutes debugging & email setup (Feb 28)

Ingredients

The Problem: Garmin Data Sits Unused

I wear a Garmin watch 24/7. It tracks sleep stages, body battery, resting heart rate, stress, VO2 max, training status, steps, and yesterday’s activity. All this data syncs to the Garmin Connect app, where it sits in a dashboard I rarely open. The data is there — I just don’t look at it consistently.

What I wanted: a daily health recap delivered to my inbox every morning with the metrics that matter, formatted with context and recommendations. No opening apps. No checking dashboards. Just: wake up, check email, see if I should prioritize recovery or push harder today.

The tools existed (Garmin API, Claude for formatting), but no off-the-shelf solution combined them into an automated morning email. So I built it.

Session 1: The Initial Build with Claude.ai

Evening, February 26 — ~2 hours

Pace: Deliberate. Lots of debugging around launchd, Claude CLI tool permissions, and getting the Garmin API working. Claude.ai handled the code generation; I handled the Mac system integration.

Phase 1: Fetch Garmin Data with Python

The first piece was a small program (a Python script) that logs into Garmin Connect and downloads the day’s health data as a structured file. Claude wrote the script from a plain-English description. Running it for the first time took 3 seconds and successfully pulled all nine metrics.

🔧 Developer section: Python fetch script

First run: worked immediately. Garmin credentials loaded from a local config file, API authenticated, all metrics fetched. Took 3 seconds.

Security tip

Your Garmin credentials live in a local config file in plaintext. Keep it that way — local only. Two rules before touching git:

  1. Store the credentials file outside any project folder that could become a git repo. A dedicated directory in your home folder (not inside a project) means there’s no risk of accidental commits, even without a .gitignore.
  2. If the credentials file is anywhere near a git project, add it to .gitignore immediately and run git status before every commit to confirm it’s not showing up as a tracked file.

Credentials in a public repo — even briefly — should be treated as compromised. Change the password immediately if that ever happens.

Phase 2: Generate the Recap with Claude CLI

The raw health data needed to become something readable — a formatted summary with a daily focus recommendation and context for each metric. Claude generated a shell script (a sequence of automated commands) that feeds the data directly to the Claude CLI and saves the result as a formatted text file.

🔧 Developer section: Recap generation requirements

Claude.ai generated a shell script (run_recap.sh) — a file that runs a sequence of steps automatically, like a recipe the computer follows:

🔧 Developer section: Shell script steps

Claude CLI tip

Embedding data directly in the prompt (RAW_DATA=$(cat file.json) then passing it in the prompt text) prevents Claude from trying to use browser tools or file readers. All context is in the prompt — Claude just formats and writes.

Phase 3: Schedule with launchd

With the scripts working manually, the last step was making them run automatically every morning. macOS has a built-in scheduler called launchd — similar to a calendar alarm for programs — that can trigger a script at any time, every day, without any manual action required.

🔧 Developer section: launchd scheduling configuration

Installed the job with:

launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.jose.garmin-recap.plist

(Not launchctl load — that’s deprecated on modern macOS. Learned that the hard way after the first attempt failed silently.)

Bugs Fixed During Session 1

By the end of Session 1, the system worked: 7am trigger → fetch data → generate recap → show popup. Tested it manually, verified the next morning it ran automatically. Success.

Session 2: Debugging & Email Integration with Claude Code

Morning, February 28 — ~45 minutes

Pace: Fast debugging, then adding email automation using existing Resend setup from the contact form.

The 401 Unauthorized Error

Woke up on February 28, expected a recap email — didn’t get one. Checked the logs:

requests.exceptions.HTTPError: 401 Client Error: Unauthorized for url: https://connectapi.garmin.com/oauth-service/oauth/preauthorized

The Garmin session had expired overnight. The garminconnect library uses token-based auth that occasionally needs re-authentication. Running the fetch script manually (in Claude Code) fixed it:

python3 garmin_fetch.py → re-authenticated, tokens refreshed, data fetched successfully.

Tomorrow at 7am, it should work again. But I still wanted the recap delivered to my inbox, not just saved to a file.

Adding Email Notifications via Resend

Saving the recap to a file was useful, but delivering it to my inbox meant I’d actually see it. I already had an email-sending account set up from the contact form build, so this was a matter of reusing the same credentials rather than setting up anything new. Claude added the email step to the existing automation script.

🔧 Developer section: Email delivery script

Updated run_recap.sh to call node send-email.js after saving the recap. Tested it:

✅ Email arrived in my inbox in under 2 seconds with today’s health recap.

Reuse what you have

Resend was already integrated for the contact form. No new account, no new API key, no new environment variables. Just reuse the existing setup. If you’ve built one email-sending feature, the second one is 5 minutes of work.

Auto-Wake with pmset

One problem remained: if the Mac was asleep at 7am, the job wouldn’t run until I woke it (could be hours later). Solution: schedule the Mac to wake at 6:59am every morning.

After debugging the pmset syntax (the man page examples helped), the working command:

sudo pmset repeat wakeorpoweron MTWRFSU 06:59:00

Verified with pmset -g sched:

Repeating power events: wakepoweron at 6:59AM every day

Now the Mac wakes at 6:59am, the recap runs at 7:00am, and the email arrives in my inbox — even if I left it asleep overnight.

Final Output

A fully automated daily health recap system that:

Built in ~2 hours initial setup (Claude.ai for Python + shell scripts + launchd) + 45 minutes debugging & email (Claude Code terminal for fixes and Resend integration).

What went fast

What needed patience

Claude.ai vs Claude Code: The Right Tool for the Job

This project used both Claude interfaces, matching Anthropic’s recommended use cases:

From Anthropic’s docs: "Use Claude.ai when you want to explore and understand. Use Claude Code when you want to build and iterate." This project proved it — the browser for initial architecture, the terminal for production debugging.

Both are the same Claude model (Opus 4.6), same account, same $200/yr subscription. The interface changes the workflow, not the intelligence. Choose based on whether you need exploration or execution.

Terminal — Morning Automation
[7:00:01] Fetching data from Garmin...
✓ user_summary
✓ sleep
✓ body_battery
✓ stress
✓ vo2max

[7:00:04] Generating recap with Claude...
Recap saved to ~/.garmin-recap/recaps/garmin-recap-2026-02-28.md

[7:00:05] Sending email...
✅ Email sent to my email inbox

Every morning at 7am: fetch → generate → email. All automated, zero manual work.

The biggest lesson? Garmin data is only useful if you see it. An automated daily email beats a dashboard you never open. And when the automation breaks (401 errors, expired tokens), having Claude Code in the terminal means you can debug and fix it in minutes instead of hours.

Now every morning starts with a health recap in my inbox. Body battery at 60? Prioritize recovery. Sleep score 85+? Push the workout. VO2 max trending down? Add interval training. The data was always there. Now it’s impossible to ignore.

← Back to all writing