Own your Oura Ring data. Pull your sleep, readiness, activity, heart rate, SpO₂, stress and workouts from the Oura Cloud API into a SQLite file on your machine, and read them in the terminal without the mobile app.
oura-cli downloads your Oura data into a SQLite file and prints it. After one oura-cli sync every report works offline. On a terminal you get tables and a weekly digest; in a pipe you get JSON, so jq, gnuplot and your own scripts can read it. The package is MIT, runs from TypeScript source on Bun, has no native dependencies and sends nothing anywhere except to the Oura API.
curl -fsSL https://bun.sh/install | bash # if you don't have Bun yet
bun add -g @drakulavich/oura-cliYou also need a Personal Access Token from Oura. Run oura-cli login once. It hides the token while you type and saves it to ~/.oura-token with 0600 permissions.
Four commands take you from an empty cache to a weekly digest:
oura-cli login # paste your PAT; the input is hidden
oura-cli doctor # confirm the token works and the database is ready
oura-cli sync # the first sync fetches the last 30 days
oura-cli report # weekly digest in the terminalThe digest looks like this:
Oura Weekly Report
2026-08-24 — 2026-08-30
Last 7 Days:
────────────────────────────────────────────────────
Day Sleep Ready Active Steps
────────────────────────────────────────────────────
Mon 24/08 87 74 68 9,668
Tue 25/08 82 79 74 11,204
...
Each collection resumes from its own last stored day. Oura revises recent days, so the overlap is deliberate, and the summary reports rows fetched (+new) per collection.
Heart rate also re-reads the two weeks behind its watermark, because Oura publishes workout samples days after the day they belong to. That re-read happens on the first sync of the day and whenever a sync brings samples newer than the cache, which means the ring has uploaded. A repeat sync on a quiet ring costs about 17 requests instead of 37.
A re-fetched window ends up holding exactly what the API returned for it. A sample Oura reclassified, or a record it re-issued under a new id, replaces the row it supersedes instead of joining it. There is one exception. When a response drops most of what one request covered, which is what a partial or short answer looks like, sync keeps those rows and names the collection and how many it kept. Once you have judged the correction genuine, oura-cli sync --prune=hr applies them for that collection, and --prune=all does it for every collection in the run.
oura-cli sync --from 2026-08-01 [--to 2026-08-07] re-fetches an explicit window for every collection instead, for example after an interrupted sync. A 429 Too Many Requests is retried with the wait Retry-After asks for, up to a minute per wait and three minutes per command, before it becomes an error.
oura-cli db today and oura-cli db week read the local cache and make no API call.
| What you see | What to run |
|---|---|
No Oura data is available for this report yet. |
oura-cli sync |
No Oura access token at /…/.oura-token |
oura-cli login |
Oura API 401 |
oura-cli login with a fresh PAT |
Oura API 429 after the automatic retries |
Wait a few minutes. A long fetch hr window is hundreds of requests. |
db today empty right after a sync |
Normal. Oura publishes a day's summary after that night's sleep syncs from the ring. |
doctor still warns that data is stale after a sync |
The ring has not uploaded. Open the Oura app near the ring, then run oura-cli sync. doctor without --offline says which side is behind. |
Database query failed: database disk image is malformed |
The cache file is damaged. Delete it (see --db and OURA_DB_PATH) and run oura-cli sync to rebuild it. oura-cli doctor shows what is wrong with it first. |
| Anything else | oura-cli doctor |
oura-cli db todayToday's scores from the local cache. If you forgot to sync, run oura-cli sync first.
oura-cli db date 2026-05-10When the cache has nothing for the day, db date says where the day sits (before the cache begins, after it ends, or in a gap inside it) and which sync call would fill it, instead of printing a row of dashes.
oura-cli db week # local cache summary, no API hit
oura-cli fetch sleep --days 7 # fresh sleep details direct from Ouraoura-cli report # weekly (default)
oura-cli report --period month # 30-day window with weekly bucketsA report has daily scores, averages with deltas against the previous window, the days that stood out (sleep or readiness under 70, activity at 90 or above), sleep details and a short recommendation block.
A day whose activity is still accumulating, normally just today, is marked with a *. It stays out of the activity and steps averages and out of the recommendations; sleep, readiness and SpO2 averages take every day shown, and each average line says how many days it covers. In JSON the same information is days[].partial and completeThrough. db today, db date and db week carry the same partial flag and print the same * and note, so the screens agree. A day counts as closed once Oura reports a full 24 hours of five-minute activity slots for it, so a ring that stops syncing does not freeze its last day as unfinished.
oura-cli db trends 30 # score trends across the last 30 days
oura-cli db stats # row counts, date range, personal bestsoura-cli doctor # token, token accepted by Oura, database, integrity, data freshness
oura-cli doctor --offline # the same without the live token checkdoctor prints one row per check and a Next: line with the first fix to apply. The integrity row runs SQLite's quick_check. The data row warns once the newest cached day ended more than 36 hours ago; online, it also asks Oura whether it holds anything newer, so it can tell "run oura-cli sync" apart from "the ring has not uploaded".
db rows prints what the cache holds for one collection, as stored, in either output mode. It takes the same range flags and defaults as fetch (--day, --from/--to, --days; today by default; none for ring), so the two are twins: fetch reads the API, db rows reads the cache. This is how you read back the collections no summary shows: tags, sessions, resilience, VO₂ max, bedtime guidance, rest mode, ring, battery.
oura-cli db rows tags --days 30 # your own annotations for the month
oura-cli db rows battery --day 2026-09-01 # the battery curve for one day
oura-cli db rows ring --format json # every ring on the account, as JSON
oura-cli db rows hr --day 2026-09-01 --limit 20 # a day of heart rate is hundreds of rowsfetch returns one collection straight from the Oura API as JSON and does not touch the local cache.
oura-cli fetch sleep # today
oura-cli fetch hr --days 7 # last 7 days
oura-cli fetch workout --from 2026-05-01 --to 2026-05-31
oura-cli fetch sleep-periods --day 2026-06-01 | jq '.[] | {day, type, average_hrv}'Collections: sleep readiness activity hr spo2 stress workout sleep-periods cv-age resilience vo2max sleep-time session rest-mode tags ring battery.
ring is a snapshot of your ring's hardware, not a day range, so it takes no --day, --from or --days. battery is a timeseries like hr: keyed by timestamp and fetched in pieces of at most 30 days.
Output switches to JSON as soon as stdout is not a terminal:
oura-cli fetch sleep --days 7 | jq '.[] | {day, score, deep: .contributors.deep_sleep}'
oura-cli db trends 90 > trends.json| Setting | Flag | Env var | Default |
|---|---|---|---|
| Token | --token |
OURA_TOKEN |
(file) |
| Token file path | --path (login only) |
OURA_TOKEN_PATH |
~/.oura-token |
| Database path | --db |
OURA_DB_PATH |
~/.oura-cli/oura.db |
| Timezone | --tz |
OURA_TZ |
system timezone, else UTC |
| Output format | --format |
auto-detect (TTY → table) | |
| Colour | --no-color |
NO_COLOR |
on for a terminal, off when piped |
These flags are global and may appear anywhere on the command line: before the command, between a command and its subcommand, or at the end. oura-cli --format json db today, oura-cli db --format json today and oura-cli db today --format json are the same command.
This tool reads your personal health data, so handle the token with care.
~/.oura-tokenis written with0600permissions on POSIX (oura-cli logindoes it for you). On Windows the file is written but ACL hardening is left to you.OURA_TOKENas an env var is convenient for scripts and CI, but it shows up inps auxe, heap dumps and core dumps. Prefer the file for interactive use.--token <pat>is the least safe option: the value lands in shell history. Avoid it outside throw-away scripts.- Revoke a token at cloud.ouraring.com/personal-access-tokens, not through this CLI.
- API error messages truncate response bodies to 200 characters and redact
Bearertokens and"token":"…"patterns before printing.
oura-cli has no telemetry. The only outbound network traffic is your authenticated Oura Cloud API calls.
| Endpoint | Source | Cached table |
|---|---|---|
| Sleep | Oura V2 daily_sleep |
daily_sleep |
| Readiness | Oura V2 daily_readiness |
daily_readiness |
| Activity | Oura V2 daily_activity |
daily_activity |
| Heart rate | Oura V2 heartrate |
heartrate |
| SpO₂ | Oura V2 daily_spo2 |
daily_spo2 |
| Stress | Oura V2 daily_stress |
daily_stress |
| Workouts | Oura V2 workout |
workouts |
| Sleep model | Oura V2 sleep |
sleep_model |
| Cardiovascular age | Oura V2 cardiovascular_age |
cardiovascular_age |
| Resilience | Oura V2 daily_resilience |
daily_resilience |
| VO₂ max | Oura V2 vO2_max |
vo2max |
| Sleep time | Oura V2 sleep_time |
sleep_time |
| Sessions | Oura V2 session |
sessions |
| Rest mode | Oura V2 rest_mode_period |
rest_mode_periods |
| Tags | Oura V2 enhanced_tag |
enhanced_tags |
| Ring | Oura V2 ring_configuration |
ring_configuration |
| Battery | Oura V2 ring_battery_level |
ring_battery_level |
Runtime: Bun. Storage: the built-in bun:sqlite. CLI parsing: citty. Output styling: chalk. There is no build step: the CLI runs from src/ on Bun and has no native dependencies.
For a script or an LLM harness driving the CLI:
oura-cli describeprints a JSON manifest of every command, argument and output schema, so an agent can discover the surface without scraping--help.oura-cli healthcheckprints{ok, version, latencyMs}for liveness probes, pluserrorwhenokis false. It proves the database opens and answers a query; it does not inspect the contents.oura-cli doctorruns SQLite'squick_checkfor that.- Gate on
.ok, not on the exit code.doctorexits 0 withok: falsefor any warning-level check (no data yet, stale data, Oura API unreachable), andhealthcheckexits 0 withok: falsefor an unusable database, because the probe itself ran.doctor --offlineskips the token-validation call, and a skipped check still counts towardsok. - Errors go to stderr as a stable JSON envelope:
{"error":{"code":"…","message":"…","hint":"…"}}. - Exit codes:
0success,1user error,2auth,3API,4storage. - JSON Schemas under
docs/schemas/coverfetch <collection>,doctorand thedescribemanifest itself, and follow semver;describenames the schema next to each command. Each collection schema pins the identity fields (idanddayfor the daily summaries,timestampfor the heart-rate and battery series,idandstart_dayfor rest mode and tags,idalone for ring) and lets the rest of the Oura record through unchanged, so a new upstream field never breaks validation. The local-data commands (sync,db *,report) have no schema files yet; their shapes are versioned through the CHANGELOG. - Two contract quirks stay for compatibility.
report --period monthreturns its window asweekStartandweekEnd.heartrate.day(andring_battery_level.day) in the cache is the date written in Oura's timestamp, UTC in practice, while every--dayand--tzargument is local.
oura-cli manifest prints the tool-registry shape OpenClaw expects. A first-party oura-mcp companion is on the roadmap.
skills/oura-cli/SKILL.md is an Agent Skills file: which command answers which question, the JSON shapes, the exit codes, and the rules an agent should keep (always --format json, cache before API, never print the token). It ships in the npm package, so link the folder into wherever your harness looks for skills:
SKILL_SRC=~/.bun/install/global/node_modules/@drakulavich/oura-cli/skills/oura-cli
ln -s "$SKILL_SRC" ~/.openclaw/skills/oura-cli # OpenClaw
ln -s "$SKILL_SRC" ~/.hermes/skills/oura-cli # Hermes Agent
ln -s "$SKILL_SRC" ~/.agents/skills/oura-cli # any harness that reads ~/.agents/skillsA test resolves every oura-cli … line in the skill against describe, so the file cannot name a command, flag, collection or error code the CLI does not have, and CI runs the reference validator (uvx --from skills-ref agentskills validate skills/oura-cli) on the frontmatter. One deliberate deviation from the spec: metadata.openclaw and metadata.hermes are nested objects, because both harnesses read them that way (the spec types metadata as string → string); the same test confines the deviation to those two keys.
- Bun >= 1.0
- macOS, Linux, or Windows (WSL)
- An Oura Personal Access Token
See CONTRIBUTING.md. Bug reports and pull requests are welcome at drakulavich/oura-cli/issues.
Made with 💍🤖 under MIT License.
