Skip to main content
Glama

cadence

CI

A local-first health-context engine. Pulls WHOOP and Apple Health into one local store, and gives Claude real, current knowledge of how you're actually doing -- sleep, recovery, strain, workouts -- instead of working blind.

demo

The honest privacy story

Unlike the rest of this portfolio (lantern, stacks, orchard), cadence can't claim "nothing leaves the machine" -- WHOOP data necessarily comes from WHOOP's own cloud API over OAuth, there's no way around that. What's real and true instead: once pulled, your health data lives only on this machine (a local SQLite file at ~/.cadence/cadence.db, permissioned 0600, owner-only), your OAuth tokens live in the macOS Keychain (never on disk in plaintext, never in this repo), and cadence never re-uploads or re-shares anything to a third party. Apple Health data never leaves the machine at all -- it's a local file import, no network call involved. Claude reads your data locally via MCP; nothing about that transmits it anywhere else either.

Related MCP server: WHOOP MCP Server

Install

pip install -e .

Apple Health (no setup, fully local):

cadence import-apple-health ~/Downloads/export.zip
cadence brief

(Export from iPhone: Health app -> profile icon -> Export All Health Data.)

WHOOP (one-time OAuth setup, see below):

cadence auth      # opens a browser once, stores tokens in Keychain
cadence sync      # pulls the last 30 days of recovery/sleep/workouts
cadence brief

MCP server (so Claude can query this live instead of just reading the brief):

cadence mcp

Point your Claude client at it (claude mcp add cadence -- cadence mcp, or the equivalent stdio-server config for your client).

WHOOP setup (one-time)

  1. Register a personal app at developer.whoop.com (sign in with your normal WHOOP account, create a Team if prompted, then create an App). Fill in:

    • Privacy Policy: a real link is required -- https://github.com/<your-fork>/cadence#the-honest-privacy-story (this section) is a reasonable choice for a personal-use app.

    • Redirect URL: must exactly match cadence.auth.REDIRECT_URI (https://github.com/rajanshxrma/cadence by default). WHOOP's own form only accepts https:// or a custom URL scheme -- a plain http://localhost redirect is rejected, which is why cadence auth uses a copy-paste flow instead of a local callback server (see auth.py's module docstring).

    • Scopes: check all six data scopes (read:recovery, read:cycles, read:sleep, read:workout, read:profile, read:body_measurement). There's no offline checkbox on the form -- that scope is requested dynamically in the actual authorization request, not a per-app setting.

    • Webhooks: leave empty -- cadence is pull-based (cadence sync), no webhook receiver.

    • Apps work immediately for up to 10 WHOOP accounts in dev status -- no approval needed for personal use.

  2. Export your credentials (shown once on the app's page after creation -- copy both immediately):

    export CADENCE_WHOOP_CLIENT_ID="..."
    export CADENCE_WHOOP_CLIENT_SECRET="..."
  3. Run cadence auth. It opens your browser; after you approve, WHOOP redirects there with a code in the URL -- copy the full address-bar URL and paste it back into the terminal when prompted. Tokens land in Keychain. cadence sync after that just works -- refresh happens automatically (WHOOP rotates refresh tokens on every use; cadence persists the new one each time, per their API's requirement).

Client id/secret are read from environment variables only -- never written to disk, never committed. keyring (the library used for token storage) needs an OS credential store; on macOS that's Keychain, no extra setup.

Health-aware AI sessions (v0.2.0)

The piece that makes this more than a data viewer: cadence hook emits a Claude Code SessionStart hook payload, so the first Claude session of each day opens already knowing your physiological state -- last night's sleep, today's recovery, recent strain -- plus a one-line adaptive hint when the numbers warrant one (low recovery: "favor lighter work over marathon builds"; short sleep: flagged; data gone stale: a re-sync nudge instead of silent death). Second session of the day: silent, by design -- a hook that talks every session is noise.

cadence hook          # prints the SessionStart JSON (or nothing -- silence is a feature)
cadence sync --quiet  # unattended mode for a launchd/cron job; macOS-notifies if re-auth is ever needed
cadence auth --store-client   # one-time: persist client creds to Keychain so unattended sync needs no env vars

Wire-up: a 15-line shim script in your hooks config calls cadence hook; a launchd job runs cadence sync --quiet twice daily. The hook never blocks a session -- no network calls (local SQLite reads only), and every failure path chooses silence over a broken session startup.

Personal baselines and anomaly detection (v0.3.0)

Every threshold below is PERSONAL, never a population norm -- cadence builds a real 30-day rolling mean/std of your own history per metric (recovery score, HRV, resting heart rate, respiratory rate, sleep duration/efficiency) and flags today's value only when it's a real statistical deviation (|z| >= 1.5) from your normal, not some generic chart's idea of normal.

The honesty guardrail that matters most here: a metric is silently omitted from baselines (not shown with a misleading small n) until there are 14+ days of real history behind it. An anomaly built on 3 data points is noise dressed up as insight -- this project would rather say nothing than say something confidently wrong.

cadence baseline   # prints current baselines + any deviations today

Anomalies surface three ways, each calibrated to how urgent they are:

  • In brief/the session hook -- inline, every time they exist.

  • A focused re-alert mid-day -- if a NEW serious deviation (|z| >= 2.5) appears after the day's first session already happened (e.g. an 8pm sync lands something the 8am one didn't have), the hook fires again just for that -- a real new signal outranks the once-daily quota. Already-seen deviations never re-fire the same day.

  • A macOS notification from the unattended sync job for the serious tier specifically -- the one thing worth interrupting silence for.

Weekly digest: cadence digest writes a dated summary to a configurable directory and can optionally commit it to a notes/journal repo. Email delivery is intentionally not wired up -- see digest.py's module docstring for the reasoning.

The mirror: body, code, and AI usage in one place (v0.4.0)

This is the piece that doesn't exist anywhere else. A few WHOOP-MCP servers exist publicly; a health tool alone isn't novel. What's actually new: cadence is the first place your git commit history, your Claude usage, and your physiological data live in one local correlation layer -- all three time-series were already sitting on the same machine, just never joined.

cadence config add-repo ~/Downloads/your-project   # point it at your own repos -- fully generic
cadence mirror                                     # commit-hour rhythm + observational correlations

The honesty discipline that makes this real rather than a toy: every correlation is a simple two-group mean comparison on YOUR OWN history -- no regression, no significance testing dressed up as more than it is -- and it refuses to report anything with fewer than 14 days of real overlapping data per group, full stop. Every result that does print carries an explicit methodology note (exactly how the two variables were date-joined, stated plainly as "observational, not causal"). Silence is the honest answer below that threshold, never a confident-sounding number built on noise.

The honesty guardrail in practice: the commit-hour histogram is available immediately (a pure histogram has no threshold to clear), but the deeper correlations (late-night coding vs. next-morning recovery, short sleep vs. commit output, heavy Claude usage vs. next-day recovery) return nothing until there are 14+ days of real overlapping data per group -- on a fresh setup they correctly report nothing rather than manufacture a "finding" from 2-3 data points, and start reporting real numbers only as the logs accumulate past that threshold.

MCP tools: get_developer_rhythm(), get_body_code_correlations().

On-device narration (v0.5.0)

cadence digest --narrate turns the structured weekly digest into 2-3 plain-spoken paragraphs, generated entirely on-device via langchain-apple-foundation-models. Zero cloud, zero cost, pip install cadence[narrate] (optional -- cadence never depends on Apple Intelligence to work).

cadence digest --narrate --no-commit   # try it without committing anything

A real hallucination, found and fixed live, not glossed over: the first working prompt produced a real run where the digest's own baseline section plainly showed a fully-populated recovery baseline (well past the n >= 14 threshold) and the model still wrote "there isn't enough data to compare your recovery score to your 30-day baseline" -- flatly contradicting its own input. Same bug class already documented in lantern's README. Fixed by making the instructions explicit about what "not enough data" is allowed to mean (only when that literal phrase appears in the input); verified across 3 consecutive real generations post-fix with zero hallucinated claims and every number reproduced verbatim. Not proof it can never recur -- prompting alone never fully eliminates hallucination -- which is exactly why the narration is inserted ABOVE the deterministic structured digest, never replacing it: the real numbers are always sitting right there to cross-check against.

How it works

WHOOP

Apple Health

Where the data comes from

Cloud API, OAuth 2.0

Local export.zip (manual export from the Health app)

What it uniquely has

Recovery score, HRV (RMSSD), strain, sleep performance/efficiency

Steps, active/basal energy, exercise minutes, distance, HRV (SDNN), workout GPS-adjacent detail

Network calls

Yes, to pull data (cadence sync)

None -- pure local file parse

Sync model

Pull-based, run cadence sync whenever

One-time import per export (re-import to refresh)

Both sources get normalized into one local SQLite schema (sleep, recovery, workouts, daily_metrics), each row tagged with its source. When both sources cover the same real-world event (e.g. the same night's sleep), cadence doesn't silently merge them into a fake "canonical" record -- it picks explicitly and says so: WHOOP wins for sleep staging and is the only source for recovery/strain (Apple Health has no equivalent computed score); Apple Health is the source for step count and other daily activity WHOOP's API doesn't expose.

Real engineering challenges (found and handled, not glossed over)

  1. The Apple Health export can be 200MB-1GB+ of XML. apple_health.py streams it with xml.etree.ElementTree.iterparse and actively evicts every processed element from the parse tree -- elem.clear() alone isn't enough (a well-known stdlib gotcha: the element still sits in its parent's children list). Verified, not just claimed: a 300k-record/48MB synthetic file grew peak RSS by ~19MB with full eviction vs ~27MB with elem.clear() alone -- a real, measurable difference that gets far more pronounced at real-export scale (millions of records).

  2. Apple Health emits sleep as dozens of small per-stage records, not one row per night. _group_sleep_sessions() reduces them into real sessions by contiguous time (a >2h gap starts a new night), summing per-stage seconds -- tested against both a gap-split case and a same-night merge case.

  3. WHOOP rotates refresh tokens on every use -- the old one is invalidated the instant a new one is issued. auth.py's refresh path persists the new refresh token immediately, before returning; dropping that step "to simplify" would break sync after the very next token rotation.

  4. A real off-by-one bug in the trend calculation, caught by its own test: comparing "this week" vs "last week" with naive inclusive date bounds put the shared boundary day in both windows, inflating the recent average (measured: 8625 instead of the correct 9000 in a 9000-vs-6000 synthetic scenario). Fixed by making the two windows properly disjoint.

  5. A real test-isolation bug, caught by its own test: store.connect()'s default argument originally bound DEFAULT_DB_PATH at function-definition time, so reassigning store.DEFAULT_DB_PATH in a test (to isolate it from the real ~/.cadence/cadence.db) silently had no effect -- an early test run actually wrote real synthetic test rows into the real local store on the dev machine before this was caught and fixed. tests/test_tools.py::test_tools_never_touch_real_default_db_path now guards against a regression.

  6. A partial-day bias in the trend math, found by the first run against real data -- the kind of bug synthetic tests can't catch. The week-over-week trend originally included today in the recent window; a cadence brief run in the early morning counted that morning's few hundred steps as a full day, and the first live report claimed steps were "down 56% vs the week before" -- partly an artifact of when the command happened to run, not real behavior. A health brief that's subtly wrong depending on the time of day is worse than no brief. Both comparison windows now cover complete days only, ending at yesterday, with a regression test pinning today's exclusion.

  7. WHOOP's start/end params silently 404 on a bare date. Found live: ?start=2026-06-10 returns a plain 404 (not a validation error), while the identical request with ?start=2026-06-10T00:00:00.000Z returns 200 with data. Full ISO8601 datetimes only -- now enforced by _iso_datetime() with its own regression test.

  8. The same lexicographic-date-comparison bug, twice, in two different modules. sleep/workouts rows store full ISO timestamps ("2026-07-10T03:00:00Z"), which sort after a bare date string ("2026-07-10") -- so a bare date.today().isoformat() upper bound silently excludes today's own rows. This was already found and fixed once in apple_health.py's tests; it recurred in digest.py (a genuinely new bug, not a regression of the first) because the fix pattern (brief.py's +1 day buffer) wasn't applied to the new module by default. Now consistent everywhere query bounds are built. Worth remembering: a fixed bug in one module doesn't protect a sibling module written later -- the pattern has to be applied deliberately each time, not assumed.

  9. A real correlation bug, caught by its own test before it ever touched real data. late_night_cost() originally iterated over commit-days to build its two comparison groups -- which meant days with ZERO commits (exactly the "no late-night commit" group it needed) were silently excluded entirely, since they never appear in commit data at all. Fixed by iterating over health-outcome days instead and defaulting missing commit data to zero, the same pattern sleep_to_output() already used correctly. Caught by a real end-to-end test (a synthetic repo + synthetic recovery data) before this ever ran against a real account -- had it shipped as written, real correlation results would have been silently computed from a biased, commits-only subset instead of the true comparison.

Limitations, stated honestly

  • WHOOP field mapping is live-verified against a real account (a real 30-day sync against a live account pulled a full set of recovery, sleep, and workout records, and the brief rendered them correctly). Two real integration bugs surfaced during that first live run and are fixed with regression tests -- see challenges #6 and #7 above.

  • Apple Health parsing covers a curated allowlist of metric types (steps, energy, distance, exercise minutes, resting heart rate, HRV-SDNN, respiratory rate, VO2max) and the classic attribute-based <Workout> schema -- not every HealthKit type Apple can export, and units are assumed to be Apple's standard emitted units except where explicitly converted.

  • Apple Health workouts have no stable id in the export format -- cadence synthesizes one from (start time, activity type), an extremely-low-collision approximation, not a real Apple-issued identifier.

  • Apple Health's own HRV measure (SDNN) and WHOOP's (RMSSD) are genuinely different statistics -- cadence keeps them in separate fields rather than pretending they're interchangeable.

Development

pip install -e ".[dev]"
pytest -v

Real, no-mock tests throughout: a synthetic (non-personal) Apple Health export fixture exercised end-to-end, WHOOP normalization against synthetic-but-realistically-shaped JSON (no live token needed), and every MCP tool invoked through the actual mcp protocol layer, not just imported and called directly.

MIT.

Install Server
A
license - permissive license
A
quality
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rajanshxrma/cadence'

If you have feedback or need assistance with the MCP directory API, please join our Discord server