skill
google-health-cli
Google Health CLI — one command-line tool for the Google Health API. Includes AI agent skills.
Set up
Skills are installed by placing their folder where your agent loads skills.
The download comes from github.com/Google-Health-API/google-health-cli itself, not from our server, so what you get is exactly what its author published.
Install instructions come from the README
README
synced Oct 17, 2026ghealth
CLI for the Google Health API v4 — built for AI agents and developers.
- 40 verified data types: steps, heart rate, exercise, sleep, weight, SpO2, HRV, ECG, blood glucose, nutrition, and more
- Agent-first: simplified JSON output, deterministic exit codes,
--dry-run,--raw - Single binary:
go build -o ghealth .
Quick Start
ghealth setup # One-time: GCP project + OAuth
ghealth data steps daily-rollup --from 2026-03-22 --to 2026-03-29 # Weekly step totals
ghealth data heart-rate list --from today --limit 10 # Recent heart rate readings
ghealth schema types # See all available data types
Requirements
Download and Install Go
Installation
git clone https://github.com/Google-Health-API/google-health-cli.git
cd google-health-cli
go build -o ghealth .
Setup
ghealth setup
Walks you through: GCP project ID, OAuth credentials (download from Console — Desktop application type), Health API enablement, scope selection, and browser-based OAuth login.
Files written under ~/.config/ghealth/ (override with GHEALTH_CONFIG_DIR):
client_secret.json— your OAuth client (mode 0600)credentials.json— access + refresh tokens (mode 0600, plaintext JSON)config.toml— active profile (project, scopes)
Tokens refresh automatically.
Non-interactive setup (for agents / CI)
ghealth setup \
--project-id my-project \
--client-secret ~/Downloads/client_secret_123.json \
--scopes-preset readonly \
--skip-enable-api \
--no-prompt
Add --non-interactive-auth to skip the browser step too — complete later with ghealth auth login --complete <code> (see below).
Authentication
| Scenario | Method |
|---|---|
| Interactive | ghealth setup or ghealth auth login |
| Headless / no browser | ghealth auth login --non-interactive → click URL on any device → ghealth auth login --complete <code> |
| Move tokens between machines | ghealth auth export → ghealth auth import |
| Pre-configured token | export GHEALTH_ACCESS_TOKEN=ya29... |
| Credential file | export GHEALTH_CREDENTIALS_FILE=/path/to/creds.json |
| GCP environment | Application Default Credentials (automatic) |
Precedence: GHEALTH_ACCESS_TOKEN > GHEALTH_CREDENTIALS_FILE > stored credentials > ADC.
Headless OAuth flow
# 1. On the host running ghealth:
ghealth auth login --non-interactive --scopes-preset readonly
# → JSON with auth_url (PKCE S256 challenge + random state baked in)
# and a complete_command. pending_auth.json holds the verifier locally.
# 2. Open auth_url in any browser, click "Allow".
# The browser will redirect to a localhost URL that fails to load — expected.
# Copy either the full redirected URL or just the 'code' query parameter.
# 3. Back on the ghealth host (both forms work):
ghealth auth login --complete 'http://localhost/?code=4/0AX4XfWh...&state=cQq...'
ghealth auth login --complete 4/0AX4XfWh...
# → state validated, PKCE verifier sent on exchange, tokens persisted.
State mismatch (URL paste with the wrong state parameter) clears the pending flow and returns exit 2. The bare-code form skips state validation but still consumes the pending file, so a stale flow can't be replayed.
Move tokens between machines
# source (already authenticated):
ghealth auth export > /tmp/ghealth-creds.json
scp /tmp/ghealth-creds.json target:
# target (also needs client_secret.json — either run 'ghealth setup --non-interactive-auth' or copy it):
ghealth auth import --file /tmp/ghealth-creds.json
Bootstrap from a fresh machine (no client_secret yet)
When no OAuth client_secret.json is configured, every auth command returns a
structured error with a next_steps array — the same six steps every time —
so an agent can relay it to a user verbatim without scraping prose:
ghealth auth login
# → exit 5, JSON on stderr:
# {
# "error": {
# "type": "config", "code": 5,
# "message": "No OAuth client_secret.json configured",
# "hint": "Run 'ghealth setup' to create or import OAuth credentials",
# "next_steps": [
# "Open https://console.cloud.google.com/apis/credentials",
# "Create or select a Google Cloud project",
# "Enable the Google Health API (...)",
# "Create OAuth client ID with Application type: Desktop app",
# "Download the client_secret JSON",
# "Run: ghealth setup --client-secret /path/to/client_secret.json"
# ]
# }
# }
Same next_steps are emitted by:
ghealth auth login(interactive /--non-interactive/--complete)ghealth auth statuswhen neither stored creds nor env creds are presentghealth auth refresh,ghealth auth export(when nothing to refresh/export)ghealth setup --no-promptwhen--client-secretis missing
To fetch the checklist without triggering an error (e.g. so an agent can display the bootstrap steps before calling auth at all):
ghealth setup --instructions
# → exit 0, JSON on stdout with status: "instructions" and the next_steps array
Data Types
40 types verified against the live API. Run ghealth schema types for the full list.
| Type | Key Values | Operations |
|---|---|---|
steps | (use daily-rollup for countSum) | list, rollup, daily-rollup, reconcile |
heart-rate | beatsPerMinute | list, rollup, daily-rollup, reconcile |
exercise | type, duration, calories, avgHeartRate, notes | list, get, create, update, delete, reconcile, export-tcx |
sleep | minutesAsleep, minutesAwake, stageMinutes | list, get, create, update, delete, reconcile |
weight | weightGrams | list, get, create, update, delete, rollup, daily-rollup, reconcile |
body-fat | percentage | list, get, create, update, delete, rollup, daily-rollup, reconcile |
height | heightMillimeters | list, get, create, update, delete, reconcile |
distance | (use daily-rollup for millimetersSum) | list, rollup, daily-rollup, reconcile |
heart-rate-variability | RMSSD | list, reconcile |
oxygen-saturation | percentage (SpO2) | list, reconcile |
altitude | altitude value | list, rollup, daily-rollup, reconcile |
active-zone-minutes | activeZoneMinutes, heartRateZone | list, daily-rollup, reconcile |
activity-level | SEDENTARY, LIGHT, MODERATE, VIGOROUS | list, reconcile |
basal-energy-burned | kcal per interval (BMR) | list, reconcile |
active-energy-burned | kcal per interval (activity) | list, rollup, daily-rollup, reconcile |
vo2-max | VO2 max value | list, reconcile |
total-calories | (use daily-rollup for kcalSum) | daily-rollup |
sedentary-period | sedentary intervals | list, daily-rollup, reconcile |
swim-lengths-data | swimStrokeType, strokeCount (use daily-rollup for strokeCountSum) | list, rollup, daily-rollup, reconcile |
hydration-log | milliliters consumed | list, get, daily-rollup, reconcile |
nutrition-log | nutrients, energy, mealType, food | list, get, rollup, daily-rollup, reconcile |
food | nutrient profiles, servings (catalog — no time filter) | list, get |
food-measurement-unit | displayName (catalog — no time filter) | list, get |
blood-glucose | mg/dL, mealType, measurementTiming | list, get, rollup, daily-rollup, reconcile |
core-body-temperature | temperatureCelsius | list, get, rollup, daily-rollup, reconcile |
electrocardiogram | waveform, resultClassification (requires ecg.readonly) | list |
irregular-rhythm-notification | alert windows (requires irn.readonly) | list |
daily-resting-heart-rate | beatsPerMinute per day | list, reconcile |
daily-heart-rate-variability | daily HRV summary | list, reconcile |
daily-oxygen-saturation | daily SpO2 summary | list, reconcile |
daily-respiratory-rate | daily respiratory rate | list, reconcile |
daily-vo2-max | daily VO2 max | list, reconcile |
daily-sleep-temperature-derivations | temp deviation from baseline | list, reconcile |
respiratory-rate-sleep-summary | per-stage respiratory rate | list, reconcile |
run-vo2-max | VO2 max from running | list, daily-rollup, reconcile |
floors | (rollup only — countSum) | rollup, daily-rollup, reconcile |
active-minutes | (rollup only) | rollup, daily-rollup, reconcile |
time-in-heart-rate-zone | (rollup only) | daily-rollup, reconcile |
calories-in-heart-rate-zone | (rollup only — caloriesInHeartRateZones array per bucket) | rollup, daily-rollup, reconcile |
daily-heart-rate-zones | (reconcile only) | reconcile |
Exercise track export
export-tcx writes the raw Google TCX, or — with --as csv — flattens it to one row per trackpoint (time, activity, lap, sport, latitude_deg, longitude_deg, altitude_m, distance_m, heart_rate_bpm, cadence_rpm, speed_mps, watts) for direct pd.read_csv consumption. Indoor activities have no track and yield a header-only CSV; their summary/notes live in data exercise list. Pass --output - to stream to stdout instead of a file.
ghealth data exercise export-tcx --id <id> --output ride.csv --as csv
ghealth data exercise export-tcx --id <id> --output - --as csv | head # stream to stdout
Usage
Reading data
# Recent heart rate (sample-type: returns individual readings)
ghealth data heart-rate list --from today --limit 10
# Daily step totals for a week (rollup: returns aggregated counts)
ghealth data steps daily-rollup --from 2026-03-22 --to 2026-03-29
# Exercises this month
ghealth data exercise list --from 2026-03-01
# Weight history
ghealth data weight list --limit 20
# Sleep (summary by default, --detail for stage-by-stage breakdown)
ghealth data sleep list --limit 5
ghealth data sleep list --limit 5 --detail
Every read (list, get, rollup, daily-rollup, reconcile) returns the same JSON shape — an object {"dataPoints": [...]} with optional _hints and nextPageToken — so the rows are always under dataPoints.
list returns up to --limit rows (default 500). When more exist it includes a nextPageToken; pass it back with --page-token to fetch the next page losslessly (no rows skipped or repeated):
ghealth data heart-rate list --from 2026-06-15 --limit 500 # → {"dataPoints":[…], "nextPageToken":"ABC"}
ghealth data heart-rate list --from 2026-06-15 --limit 500 --page-token ABC
Important: list vs daily-rollup
Some types (steps, distance) return time intervals without values from list. Use daily-rollup to get totals:
# This returns minute-by-minute intervals (no step count):
ghealth data steps list --from today --limit 5
# This returns daily totals with actual counts:
ghealth data steps daily-rollup --from 2026-03-22 --to 2026-03-29
# → {"dataPoints": [{"date": "2026-03-28", "countSum": "9037"}, ...]}
Gotcha: missing days are NOT zeros
For the presence-aware types — altitude, distance, floors, steps, total-calories — a date that is absent from rollup output means the device was not worn (or did not sync) that day, not that the value was zero. A bucket with countSum: "0" is a true zero: the device was worn and genuinely recorded no activity.
- Missing date → render as "no data", never coalesce to 0
countSum: "0"→ true zero (worn, no activity)- Never average over absent days as if they were zeros — that silently deflates weekly/monthly stats
Filtering
ghealth data heart-rate list --from 2026-03-28 # From date
ghealth data heart-rate list --from 2026-03-28 --to 2026-03-29 # Date range
ghealth data heart-rate list --from today --limit 50 # Today, max 50
ghealth data heart-rate list --from yesterday # Since yesterday
--filter passes a raw expression to the API (overrides --from/--to). Filter syntax follows AIP-160 — interval types use {type}.interval.civil_start_time (ISO 8601, no Z), sleep uses sleep.interval.civil_end_time (only end-time is filterable), sample types use {type}.sample_time.physical_time (RFC-3339, with Z). Only >= and < comparators are supported.
Writing data
Writable types: exercise, sleep, weight, body-fat, height.
Write operations are asynchronous — the API returns an Operation object. Use list to verify the data was persisted.
To discover the correct JSON format, inspect a real response: ghealth data weight list --raw --limit 1
# Create (use --raw list output to model the payload structure)
ghealth data weight create --json '{"weight": {"weightGrams": 75500, "sampleTime": {"physicalTime": "2026-03-29T10:00:00Z", "utcOffset": "3600s"}}}'
# Update (use --update-mask to specify which fields to change)
ghealth data weight update --id <id> --json '{"weight": {"weightGrams": 76000}}'
# Delete (accepts bare IDs or full resource names)
ghealth data exercise delete --ids 7649353586249326520
Output
Responses are simplified by default — redundant timestamps, empty fields, and repeated metadata are stripped. Timestamps include the user's UTC offset (e.g., +01:00).
ghealth data heart-rate list --from today --limit 2
{
"dataPoints": [
{"time": "2026-03-29T16:33:07+01:00", "beatsPerMinute": "80", "source": "Google Pixel Watch 4 (41mm)"},
{"time": "2026-03-29T16:33:04+01:00", "beatsPerMinute": "80", "source": "Google Pixel Watch 4 (41mm)"}
]
}
ghealth data steps daily-rollup --from 2026-03-26 --to 2026-03-29
{
"dataPoints": [
{"date": "2026-03-28", "countSum": "9037"},
{"date": "2026-03-27", "countSum": "2408"},
{"date": "2026-03-26", "countSum": "6474"}
]
}
| Flag | Effect |
|---|---|
--raw | Return the original API response with no simplification |
--format table | Aligned columns |
--format csv | CSV output (nested objects flatten to dot-separated columns) |
-o, --output <file> | Write data to the file; print only a column schema + 3-row preview to stdout. Prefer this over > file (which gives the file but no schema) |
--dry-run | Show the HTTP request without executing |
In --format csv and --format table, the data stream stays pure: _hints and a leftover nextPageToken are written to stderr rather than mixed into the rows, and an empty result emits an empty CSV (never a JSON object). Use -o <file> and the stderr signals together to page through a large export without polluting the CSV.
AI Agent Skills
The repo ships 2 Agent Skills (SKILL.md files) — one for shared prerequisites (auth, setup, global flags) and one covering all 40 data types, operations, patterns, and gotchas.
# Install all skills at once
npx skills add https://github.com/Google-Health-API/google-health-cli
# Or pick only what you need
npx skills add https://github.com/Google-Health-API/google-health-cli/tree/main/skills/ghealth
npx skills add https://github.com/Google-Health-API/google-health-cli/tree/main/skills/ghealth-shared
Agents don't need to read the full skill file upfront. The CLI supports progressive self-discovery:
ghealth schema types # What types exist? What operations?
ghealth schema type heart-rate # Fields, parameters, scope for one type
ghealth data <type> --help # What operations does this type support?
ghealth data <type> list --help # What flags does this operation take?
ghealth --dry-run ... # What HTTP request would this send?
Other Commands
ghealth user identity # User identity
ghealth user profile get # Profile (age, stride length)
ghealth user settings get # Settings (timezone, units)
ghealth auth status # Auth state (scopes, expiry)
ghealth schema types # All data types + operations
ghealth schema type heart-rate # Detail for one type
ghealth schema scopes # OAuth scopes
ghealth schema endpoints # All API endpoints
ghealth config show # Show active configuration (project, scopes, format)
ghealth config set timezone <IANA zone> # Set a config value (keys: project_id, format, timezone)
ghealth webhooks subscribers list # Manage push-notification subscribers / subscriptions
webhooks (subscribers, subscriptions, verify) manages project-level push notifications and requires the cloud-platform scope plus a configured project ID — see the skill docs for details.
Because a cloud-platform token is rejected by the data-plane endpoints, keep webhook credentials in a separate config dir from your health-data credentials. auth login always writes tokens to the active config dir, so use a dedicated GHEALTH_CONFIG_DIR for webhooks (not GHEALTH_CREDENTIALS_FILE, which only changes where tokens are read from):
export GHEALTH_CONFIG_DIR=~/.config/ghealth-webhooks
ghealth setup # its own client_secret + project
ghealth auth login --scopes cloud-platform
ghealth webhooks subscribers list
Timezones
Date arguments (--from/--to, today, yesterday) resolve against exactly one timezone, chosen in this order:
- the active profile's configured zone —
ghealth config set timezone <IANA zone> - otherwise, the machine-local timezone
Verifying tokens
ghealth auth status is a fast local check by default — it reports what's configured without making any network calls, and the authenticated field reflects local state only. For env-token / credentials-file modes it is omitted entirely (presence of a token doesn't prove validity).
ghealth auth status --validate
Verifies the access token against Google's tokeninfo endpoint. authenticated then reflects actual validity, and the response includes expires_in and scope from Google.
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | API error (4xx/5xx) |
| 2 | Auth error (have client_secret, missing or invalid tokens — run ghealth auth login) |
| 3 | Validation error |
| 4 | Network error |
| 5 | Config error (often: no client_secret.json — error carries next_steps for bootstrap) |
Errors are always JSON on stderr and may include a next_steps: []string array for multi-step recovery (currently only emitted when no OAuth client is configured).
Environment Variables
| Variable | Purpose |
|---|---|
GHEALTH_ACCESS_TOKEN | Direct access token |
GHEALTH_CREDENTIALS_FILE | Path to credential JSON |
GHEALTH_CONFIG_DIR | Config directory override |
GHEALTH_PROFILE | Active profile name |
GHEALTH_FORMAT | Default output format (json/table/csv) |
GHEALTH_BASE_URL | Override the API base URL |
License
Apache 2.0 — see LICENSE.