Local-first, read-only Google Health CLI — archive your Google Health data locally.
gohealthcli connects to the Google Health API, stores raw provider JSON in a
local SQLite archive, and provides scriptable commands for sync, status, query,
raw API exploration, and CSV/JSONL exports.
It is for local inspection and personal data archiving. It does not write health data, delete health data, run a server, upload archives, or share exports.
The full command surface is live: setup (init, doctor), OAuth and
identity snapshots (connect through irn-profile), archiving (sync,
with heartbeat-backed sync --status observability and auto-fencing of
abandoned runs), raw provider exploration (raw), and a stable read
surface (status,
query, export, describe-schema) with predictable --plain /
--json contracts for scripted and LLM consumers (PRD #144). The Tier 1 daily + hydration catalog slice is
sealed, and CI runs gofmt, golangci-lint (make lint), build, tests, and the
command-reference drift
guard (make docs-check) on every pull request and push to main. The Command Registry in
cmd/gohealthcli/commands.go is the single source of truth for the user-facing
surface; the list below mirrors each entry's Short description and stays in
sync with gohealthcli --help.
init: Create local config and an empty Health Archive.doctor: Validate local setup and provider reachability.connect: Run the browser OAuth flow and anchor one Google Identity.identity: Refresh the archived Google Identity metadata.profile: Archive a Profile Snapshot from the provider.settings: Archive a Settings Snapshot from the provider.devices: Archive a Paired Devices Snapshot from the provider.irn-profile: Archive an IRN Profile Snapshot from the provider.sync: Archive Google Health Data Points and supported Rollups.status: Summarise archive counts and newest synced timestamps.query: Run guarded read-only SQL over the Health Archive.export: Write a normalised dataset to CSV or JSONL.raw: Print raw provider JSON for endpoint exploration.describe-schema: Self-describe the Health Archive for LLM consumption.completion: Generate a shell completion script.
The discoverability verbs added by PRD #143 cover the rest of the surface:
gohealthcliwith no arguments prints the same Subcommands block asgohealthcli --helpto stdout and exits 0 — the binary never errors on a bare invocation.gohealthcli helpandgohealthcli help <command>are alias verbs for--help/<command> --help, prepending the registry's long-form prose to the flag block on stderr.gohealthcli --versionandgohealthcli --version --jsonprint the build-stampedversion,commit, andbuiltidentifiers; see docs/commands/version.md for the shape.- An unknown command prints
unknown command: <typo>on stderr, a Levenshtein-2 "Did you mean" hint (at most two suggestions), and the canonicalRun 'gohealthcli --help' for a list of commands.discovery line — see docs/commands/help.md.
Supported Data Point sync types (grouped by domain):
- Activity and fitness:
steps,distance,floors,altitude,active-energy-burned,active-minutes,active-zone-minutes,activity-level,sedentary-period,time-in-heart-rate-zone,vo2-max,run-vo2-max,daily-vo2-max,swim-lengths-data. - Heart rate:
heart-rate,heart-rate-variability,daily-resting-heart-rate,daily-heart-rate-variability,daily-heart-rate-zones. - Heart rhythm (Tier 2 opt-in scopes):
electrocardiogram,irregular-rhythm-notification. - Sleep and respiration:
sleep,oxygen-saturation,daily-oxygen-saturation,daily-respiratory-rate,respiratory-rate-sleep-summary,daily-sleep-temperature-derivations. - Exercise:
exercise. - Body measurements:
weight,body-fat,height. - Other biomarkers:
blood-glucose,core-body-temperature. - Hydration (nutrition.readonly scope):
hydration-log.
sync --source-family wearable is available for Data Types backed by the
Google Health reconcile path. sync --types steps --rollup daily archives
steps daily Rollups, and sync --types heart-rate --rollup daily archives
daily heart-rate summary Rollups without replacing raw heart-rate samples.
total-calories is known to the catalog but is not
supported by raw Data Point sync because Google exposes it as Rollup data;
calories-in-heart-rate-zone is also catalog-known but not yet implemented
because Google exposes it only through Rollup operations whose payload shape
is not pinned in gohealthcli yet.
With the Tier 2 tcx scope granted (gohealthcli connect --add-scopes tcx), exercise sync also archives each session's TCX route file as a
tcx-kind Attachment under <archive>.attachments/ (ADR-0009). Without
the scope, exercise Data Points still sync and the TCX step is skipped —
no failed provider call, no partial archive.
The drift guard in internal/googlehealth/readme_sync_types_test.go
(TestREADMEListsEverySyncableDataType and
TestREADMECaveatListsCatalogTypesSyncRejects) fails if a Data Type is
added to the Google Health catalog without a matching entry in the list
above or the caveat sentence.
For a plain-language description of each Data Type — what it captures, the upstream record shape, required scope, and the normalized view it projects into — see docs/data-types.md.
Normalized export datasets. gohealthcli export accepts any of the
names below as its positional argument. The list is auto-generated from
exportDatasetCatalogSingleton.Names() by make docs-export-datasets;
the markers around the block are stable so the regenerator can rewrite
just the bullets without touching the surrounding prose.
active-minutes-intervalsactive-zone-minutes-intervalsactivity-level-intervalsaltitude-intervalsblood-glucose-samplesbody-fat-samplescore-body-temperature-samplescurrent-heightcurrent-irn-profilecurrent-settingsdaily-heart-rate-zonesdaily-sleep-temperature-derivationsdaily-stepsdaily-vo2-maxelectrocardiogram-sessionsexercise-sessionsexercise-splitsfloors-intervalsheart-rate-samplesheight-sampleshydration-log-sessionsirregular-rhythm-notificationspaired-devicesrespiratory-rate-sleep-summaryresting-heart-rate-by-dayrun-vo2-max-samplessearchable-textsedentary-period-intervalssleep-sessionssleep-stagesswim-lengths-data-intervalstime-in-heart-rate-zone-intervalsvo2-max-samplesweight-samples
The drift guard in cmd/gohealthcli/docs_export_datasets_test.go
(TestREADMEExportDatasetsBlockMatchesCatalog) fails if the committed
block does not match a fresh regeneration; the companion
TestREADMEListsEveryExportDataset keeps the wider section honest.
With Homebrew:
brew install BramVR/tap/gohealthcli
gohealthcli --versionWith Go:
go install github.com/BramVR/gohealthcli/cmd/gohealthcli@latest
gohealthcli --versionInstall the Agent Skill:
npx skills add BramVR/gohealthcli --skill gohealthcliOn Windows amd64, install the checksummed release ZIP with the PowerShell instructions.
For local development:
git clone https://github.com/BramVR/gohealthcli.git
cd gohealthcli
go test ./...
go run ./cmd/gohealthcli --helpGoogle Health API access requires a Google Cloud project and OAuth setup.
In Google Cloud:
- Enable the Google Health API.
- Configure Google Auth Platform branding, audience, and data access.
- While unverified, keep the app in Testing and add your Google account as a test user.
- Add these Data Access scopes:
https://www.googleapis.com/auth/googlehealth.profile.readonlyhttps://www.googleapis.com/auth/googlehealth.activity_and_fitness.readonlyhttps://www.googleapis.com/auth/googlehealth.health_metrics_and_measurements.readonlyhttps://www.googleapis.com/auth/googlehealth.sleep.readonly
- Create an OAuth client with application type
Desktop app. - Download the client JSON.
The four scopes above cover the default Tier 1 surface. The Tier 2
features — the settings, devices, and irn-profile commands, the
electrocardiogram, irregular-rhythm-notification, and
hydration-log sync types, and TCX route archiving — each need an
extra opt-in scope granted with gohealthcli connect --add-scopes
(keywords: ecg, irn, nutrition, settings, tcx); without it
the provider returns HTTP 403. Add the matching optional scopes in
Google Cloud as well — the full scope-to-keyword table is in
docs/google-auth-setup.md.
Do not use a Web application client. gohealthcli uses an installed-app
localhost callback flow and rejects web-client JSON.
Keep the downloaded OAuth client JSON owner-only:
chmod 600 ~/Downloads/client_secret_*.jsonInitialize local config and archive:
gohealthcli init --oauth-client-file ~/Downloads/client_secret_*.json
gohealthcli doctor --plainConnect in the browser and verify the connection:
gohealthcli connect --plain
gohealthcli doctor --online --plain
gohealthcli identity --plain
gohealthcli profile --plainSync a small window first:
gohealthcli sync --types steps --from 2026-01-01 --to 2026-01-02 --plain
gohealthcli status --plainWatch a long sync from another terminal (or an agent) while it runs:
gohealthcli sync --status
gohealthcli sync --status --window 2h --jsonsync --status reads the local sync_runs audit table — no provider calls.
Every Sync Run heartbeats before each page fetch (counts so far plus
last_progress_at), so in-flight rows show live progress; finished runs are
listed inside the --window (default 15m, max 24h) while running rows never
age out of view. On entry, sync, sync --status, and status fence
abandoned runs: a sync_running row with no heartbeat for 5 minutes flips to
sync_failed with error_summary='abandoned (no heartbeat for 5m)', and the
Sync Cursor stays put so the next run re-reads the same window.
How long does a sync take? Cursor-resumed incremental syncs finish in seconds. Explicit backfills cost time in proportion to Data Point count — sustained throughput measures roughly 2,000–5,000 Data Points/minute on real runs (plan with ~2,000/min) — so the Data Type's density decides the wall-clock. Densities measured 2026-06-10 from a real archive backed by a Pixel Watch 4 (continuous heart-rate sampling), and what two weeks of data costs. A Data Point is the upstream record unit, which is why the counts differ so wildly per type: a heart-rate point is a single reading (every ~3 seconds on the watch), a steps point is a one-minute bucket, and a sleep point is an entire night with its stage breakdown.
| Data Type | Density (points/day) | Two weeks ≈ | Sync time ≈ |
|---|---|---|---|
heart-rate |
~27,500 | ~385,000 pts | 1.5–3 h |
time-in-heart-rate-zone |
~960 | ~13,400 pts | ~5 min |
active-energy-burned |
~630 | ~8,800 pts | ~4 min |
oxygen-saturation |
~480 | ~6,700 pts | ~3 min |
steps |
~260 | ~3,600 pts | ~2 min |
sleep, daily-* types |
~1 | ~14 pts | seconds |
Density is account-specific — a phone-only account with no
continuously-sampling wearable runs far lower. Long runs survive OAuth
token expiry: a mid-run 401 triggers a single token refresh and a retry
of the failed page, so a multi-hour heart-rate backfill can run as one
--from/--to window in the standard init --oauth-client-file setup.
Watch long runs from another terminal with sync --status. The full
per-type table covering every measured Data Type is in
docs/data-types.md.
Archive daily Rollups or wearable-filtered Data Points when needed:
gohealthcli sync --types steps --rollup daily --from 2026-01-01 --to 2026-01-31 --plain
gohealthcli sync --types heart-rate --rollup daily --from 2026-01-01 --to 2026-01-31 --plain
gohealthcli sync --types heart-rate --source-family wearable --from 2026-01-01 --to 2026-01-02 --plainExport normalized daily steps:
gohealthcli export daily-steps --format jsonl --stdout
gohealthcli export daily-steps --format csv --output steps.csvExplore raw provider JSON:
gohealthcli raw endpoint getIdentity
gohealthcli raw data-type steps --from 2026-01-01 --to 2026-01-02Query the local archive:
gohealthcli query --plain 'SELECT data_type, COUNT(*) FROM data_points GROUP BY data_type'Command flags must appear before the SQL argument because Go flag parsing stops at the first positional argument.
Use gohealthcli <command> --help or gohealthcli help <command> for
command-specific flags.
These flags apply to the top-level invocation and (where the subcommand
accepts them) to the per-subcommand parse. The shared set is the contract
captured by the Common Flag Set module in
cmd/gohealthcli/common_flags.go:
--config <path>: config file path.--db <path>: SQLite Health Archive path.--json: write stable JSON to stdout.--plain: write plain key/value output to stdout.--no-input: never prompt, never wait for browser input.--version: print the build-stamped version line and exit (top level only).
--plain and --json are mutually exclusive — passing both exits non-zero
with a flag_invalid failure envelope ("--plain and --json are mutually exclusive"). The check fires for --version too, so
gohealthcli --plain --json --version is rejected before any output is
written.
A few subcommands deviate from the standard --plain / --json
contract:
describe-schemaalways emits the curated JSON catalog (or live DDL when--sqlis passed). Its own--jsonflag is on by default; the global--json/--plainare accepted and parsed but have no effect on the schema bytes. Its failure envelopes do route through the Failure Reporter, sogohealthcli --json describe-schema boguslands a JSON failure on stdout like every other subcommand.exportalways writes CSV (default) or JSONL according to its--formatflag, and treats--plain/--jsonas format synonyms rather than output-mode switches:--jsonmeans--format jsonland--plainmeans--format csv. Passing a synonym alongside a contradictory--formatvalue (--json --format csv) fails with a "--json conflicts with --format csv" error. Failure envelopes do honour the requested mode —export <dataset> --jsonreports failures as a JSON envelope,--plainas plain key/value lines. See docs/commands/export.md.rawwrites the provider's raw bytes to stdout and ignores--plain,--json, and--no-input; passing any of them directly onrawis rejected at parse time with a targeted "not supported by raw" message.
The four read commands — status, query, export, describe-schema —
are the primary interface for LLM consumers and scripted users. PRD #144
made the contract predictable across them; the notes below summarise the
behaviour that is now stable. See each command's reference page under
docs/commands/ for the full prose.
--db <path>works on its own for every read command. Passinggohealthcli --db /tmp/scratch.sqlite statusopens that archive directly without requiring a matching--configfile. When only--dbis explicit it wins without an agreement check; when both--configand--dbare explicit and disagree, the error names--dband--configrather than the internalarchive_pathfield.describe-schema --dbis honoured the same way (PRD #144 slice 1).status --plainandstatus --jsoncarry the same information. The plainknown_data_types: a,b,cline maps to a top-levelknown_data_typesJSON array;paired_device_countis a top-level JSON key as well as the back-compat nestedidentity_snapshots_freshness.paired_device_count. A consumer who picks one mode never loses fields the other mode carries (PRD #144 slice 9).querywith no flags emits the samerow.<row>.<column>: <value>shape as--plain— the legacyRow N: column=value column=valueoutput (which silently broke on values containing spaces or=) was removed in PRD #144 slice 7. Scripted and LLM consumers get a parseable shape by default.query --jsonreturns JSON-typed columns (raw_json,data_source_json,timezone_metadata,token_metadata_json,google_identity_json, and any column whose name ends in_json) as nested JSON objects so downstream consumers parse once instead of twice. Pass--raw-textto opt out. BLOB columns are wrapped in a{"__blob_base64__": "<base64>"}marker object so raw bytes survive the JSON path without UTF-8 corruption (PRD #144 slices 5–6).export --helplists every supported dataset alphabetically (PRD #144 slice 3). The full list is auto-generated above between the<!-- export-datasets:start -->/<!-- export-datasets:end -->markers from the same registry (PRD #144 slice 4).export <typo>surfaces the closest matches (Levenshtein ≤ 3, top 3) and a pointer back toexport --help.describe-schema --jsonreports view column types as the literal"unknown"rather than a misleadingBLOBor empty string when the underlying expression's affinity does not carry a declared type. Table columns still report their declared types — the fallback is view-only (PRD #144 slice 8).
Default local paths:
- config:
~/.config/gohealthcli/config.toml - archive:
~/.local/share/gohealthcli/gohealthcli.sqlite
Default runtime token storage is OS-native:
- macOS: Keychain
- Windows: Windows Credential Manager
- Linux: Secret Service/libsecret
For local testing, an explicit file Credential Store is acceptable if it stays
owner-only. There is no default file path — a type = "file" store must set
path explicitly (a conventional location is
~/.config/gohealthcli/tokens.json):
[credential_store]
type = "file"
path = "/absolute/path/to/gohealthcli/tokens.json"Use doctor --plain to check local setup without provider calls. Use
doctor --online --plain only when you want token refresh and Google Health
reachability checks.
- Read-only provider behavior: no health writes or deletes.
- Local-first archive: no cloud service and no background upload.
- OAuth token values are not printed in normal command output.
- OAuth endpoints from the client JSON are pinned to https Google hosts,
and the client file must stay owner-only — enforced both at
connectand on the token auto-refresh path. - Exports can reveal health history; commands require explicit
--stdoutor--output, andexportrefuses to write through a symlinked--output. queryandstatusplain output escapes control characters, so archived provider data cannot inject terminal escape sequences.- Data Point Attachment paths are validated against path traversal before they are joined to the attachments root.
- Keep the SQLite archive, token files, and exported CSV/JSONL files private.
Tagged releases publish GitHub Release archives and update the Homebrew tap. Install with:
brew install BramVR/tap/gohealthcliRelease operators: see docs/release.md.
- Project Site: rendered install, quickstart, Data Types, and command reference pages.
- CONTEXT.md: project glossary only, used by grill-style review.
- docs/google-auth-setup.md: local Google Health OAuth setup checklist.
- docs/commands.md: CLI surface and output behavior.
- docs/data-model.md: archive model sketch.
- docs/security.md: local credentials and health data safety.
- docs/research.md: source-backed Google Health API notes.
- docs/plan.md: product and implementation plan.
- docs/adr/: short architectural decision records.
