The best-benchmarked open-source AI memory system. And it's free.
Google Health API CLI with agent skills
ghealth is a terminal client for Google Health API v4. It gives you commands for setup, OAuth login, data reads and writes, schema lookup, user info, and webhooks, with simplified JSON output and predictable exit codes for agent use. The repo also ships two Agent Skills so builders can add the shared auth/setup flow and the full data-type reference to their agent workflows.
Builders who want their agent or terminal workflow to work with Google Health API data.
You can query, update, and export health data without hand-coding API calls or re-explaining the auth flow.
What it does
OAuth setup and login
Guides you through Google Cloud project setup, client secret import, and browser or headless login, then stores tokens under `~/.config/ghealth/`.
Health data commands
Reads, creates, updates, deletes, rollups, and reconciles supported health data types such as steps, heart rate, sleep, weight, ECG, and glucose.
Schema discovery
Shows available data types, fields, scopes, and endpoints with `ghealth schema types`, `ghealth schema type <name>`, and related commands.
Agent-friendly output
Uses simplified JSON, deterministic exit codes, `--dry-run`, `--raw`, and CSV/table output so agents can inspect requests and parse results reliably.
Webhook management
Manages project-level push notification subscribers and subscriptions with the `webhooks` commands.
Agent skills
Ships reusable skill files in `skills/ghealth` and `skills/ghealth-shared` for installing the CLI workflow into agent setups.
How to get it
- 1Run
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
- 2Run
git clone https://github.com/Google-Health-API/google-health-cli.git cd google-health-cli go build -o ghealth .
- 3Run
ghealth setup
- 4Run
ghealth setup \ --project-id my-project \ --client-secret ~/Downloads/client_secret_123.json \ --scopes-preset readonly \ --skip-enable-api \ --no-prompt
- 5list returns up to --limit rows (default 500). When more exist it includes a…
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 - 6Some types (steps, distance) return time intervals without values from list. Use…
# 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"}, ...]}
README
ghealth
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.
Files in the repo
- cmd
- internal
- pkg
- skills
- .gitignore
- AGENTS.md
- CODE_OF_CONDUCT.md
- CONTRIBUTING.md
- go.mod
- go.sum
- LICENSE.md
- main.go
- README.md
Discussion (0)
Ask about usage, or say what you built with itSign in to join the discussion.
No comments yet. Be the first to say what this is good for.
More tools
Never stop coding. Free MIT AI gateway: one endpoint, 352 providers (150+ free), 1200+ models Kimi, Claude, GPT, Gemini, GLM, DeepSeek, MiniMax. Works with Claude Code, Codex, Cursor, OpenCode, Cline & Copilot. Quota-aware auto-fallback, RTK+Caveman compression saves 15-95% tokens, MCP/A2A, Desktop/PWA. Built by 550+ contributors
Compress tool outputs, logs, files, and RAG chunks before they reach the LLM. 20% fewer tokens for coding agents, 60-95% fewer tokens for JSON, same answers. Library, proxy, MCP server.
Turn any technical book PDF into a Claude Code skill — ready to study, reference, and use while you work.
OfficeCLI is the first and best Office suite purpose-built for AI agents to read, edit, and automate Word, Excel, and PowerPoint files. Free, open-source, single binary, no Office installation required.
Use ChatGPT Web (including Pro) as a native model in Codex — with context, tools, streaming and images, without using Codex quota.