suuntool

command module
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 12, 2026 License: MIT Imports: 1 Imported by: 0

README

suuntool

suuntool

Go version License Release Platform MCP ready

Fast, scriptable, agent-friendly CLI for the Suunto / Sports-Tracker API — the same backend the Suunto mobile app uses. Log in, pull your profile, pipe it into jq, automate it, hand it to an LLM. No browser, no app, no clicks.

It also doubles as an MCP server — point Claude Desktop, Claude Code, or Codex at it and they can call every Suunto endpoint directly with typed arguments.

$ suuntool login --email you@example.com
Password:
Logged in as alice (you@example.com). Session saved to ~/.config/suuntool/session.json.

$ suuntool workouts list --limit 5
Date              Act  Type           Distance  Duration  Ascent  Key
2026-05-11 07:42  1    RUNNING        8.42 km   0:44:18   62 m    wk_abc123
2026-05-10 18:05  2    CYCLING        32.10 km  1:12:04   210 m   wk_abc124
2026-05-09 06:30  11   HIKING         5.80 km   1:05:00   140 m   wk_abc125
2026-05-08 07:10  1    RUNNING        10.05 km  0:52:30   78 m    wk_abc126
2026-05-07 19:20  22   TRAIL_RUNNING  1.20 km   0:28:11   0 m     wk_abc127
5 workouts  57.57km  3:42:21

$ suuntool workouts list --since 14d --summary
workouts:  5
distance:  57.57km
time:      3:42:21
ascent:    490 m
descent:   480 m

Per activity:
Act  Type           Count  Distance  Duration  ΔWoW
1    RUNNING        2      18.47km   1:36:48   +1
2    CYCLING        1      32.10km   1:12:04   0
11   HIKING         1      5.80km    1:05:00   -1
22   TRAIL_RUNNING  1      1.20km    0:28:11   0

$ suuntool wellness sleep --since 3d | jq -c '{date:.timestamp, nap:.entryData.isNap, quality:(.entryData.quality*100|round), hrBpm:(.entryData.hrAvg*60|round)}'
{"date":"2026-05-09T18:25:00Z","nap":true,"quality":0,"hrBpm":78}
{"date":"2026-05-10T02:46:00Z","nap":false,"quality":73,"hrBpm":67}
{"date":"2026-05-10T14:50:00Z","nap":true,"quality":0,"hrBpm":84}
{"date":"2026-05-11T01:55:00Z","nap":false,"quality":77,"hrBpm":69}

Why

  • Agent-ready. Stable exit codes, JSON-on-pipe defaults, machine-readable error envelopes, and suuntool endpoints --format json so an LLM can map intents to commands without scraping help text.
  • Scriptable. One static binary. No Python, no node, no app sandbox.
  • Honest. The signing logic is locked to golden vectors, so when the contract drifts, the tool fails loudly instead of pretending.

Install

brew install tajchert/tap/suuntool

Or from source:

go install github.com/tajchert/suuntool@latest

Quickstart

suuntool login --email you@example.com              # interactive password prompt
echo 'hunter2' | suuntool login \
   --email you@example.com --password-stdin         # non-interactive (CI, agents)

# Profile
suuntool whoami                                     # current session user
suuntool profile settings                           # full user settings DTO
suuntool profile follow                             # follower/following counts
suuntool profile user alice                         # look up any user by handle

# Workouts
suuntool workouts list --limit 5                    # most recent 5 workouts
suuntool workouts list --limit 100 --summary        # totals over a window (per-activity + ΔWoW delta, colored on TTY)
suuntool workouts list --since 7d                   # last 7 days (also: 12h, 2w, last-week, 2026-01-01)
suuntool workouts list --since 2025-01-01 --stream > 2025.ndjson  # NDJSON, auto-paginates across cursors
suuntool workouts list --fields key,startTime,totalDistance      # project to a few JSON fields (forces JSON)
suuntool workouts get wk_abc123                     # one workout's metadata
suuntool workouts stats                             # aggregate stats for you
suuntool workouts count                             # workout count
suuntool workouts sml wk_abc123 -o wk.sml.json      # full ~5MB sample data
suuntool workouts fit wk_abc123 -o wk.fit           # binary .fit export
suuntool workouts export wk_abc123 --bundle ./wk    # metadata+sml+fit+ext+comments in one shot

# 24/7 wellness (gzipped NDJSON, decoded on the fly)
suuntool wellness sleep                             # pretty table on TTY, raw NDJSON when piped
suuntool wellness sleep --since 7d -o sleep.ndjson  # last 7 days (also: 2026-01-01, last-week)
suuntool wellness activity   --since last-month | jq '.entryData.stepCount'
suuntool wellness recovery   --out ./wellness
suuntool wellness sleepstages --out ./wellness

# Workout interactions
suuntool workouts comments wk_abc123                # list comments
suuntool workouts comment wk_abc123 "great run"     # post a comment (x-totp)
suuntool workouts react wk_abc123                   # like (x-totp)
suuntool workouts edit wk_abc123 --set totalAscent=120
suuntool workouts share wk_abc123 --as gpx-track    # signed GPX URL
suuntool workouts extensions wk_abc123              # Fitness/Intensity/…
suuntool workouts upload --sml ./wk.sml             # multipart upload
suuntool workouts delete wk_abc123 --yes            # destructive — needs --yes off-TTY

suuntool doctor                                     # connectivity + session check
suuntool logout

MCP server

suuntool mcp exposes every endpoint above as a Model Context Protocol tool over stdio. Point any MCP-capable client at it and an LLM can call Suunto endpoints directly with typed arguments — no shell scraping.

suuntool login                                    # one-time; the MCP server cannot prompt for a password
suuntool mcp                                      # read-only (default)
suuntool mcp --allow-write                        # + comment/react/edit/batch-update/share/extensions/upload
suuntool mcp --allow-write --allow-destructive    # + delete/uncomment/unreact
Tier Flag Tools
read (default) none whoami, profile_settings/follow/user, workouts_list/get/count/stats/sml/fit/comments, wellness_sleep/activity/recovery/sleepstages, activity_type_name, doctor
write --allow-write workouts_comment/react/edit/batch_update/share/extensions/upload (file bodies via base64)
destructive --allow-destructive (requires --allow-write) workouts_delete/uncomment/unreact

login/logout are intentionally not exposed. Workout responses are enriched with activityName next to the numeric activityId so the LLM doesn't need a second lookup. Wellness NDJSON streams are buffered into {items: [...]} arrays with an optional limit — keep windows short for long histories.

workouts_sml and the 1 MB response cap. The raw /sml body is ~5 MB on a typical ride and will exceed MCP's 1 MB response limit on anything longer than ~30 min. Pass streams (and optionally downsample) to get a filtered, structured subset instead of the base64 blob:

// power curve / HR analysis for a 2h ride
{ "key": "abc123", "streams": ["HR", "Power", "Cadence"], "downsample": 2 }
// → { key, sample_count, samples: [ { TimeISO8601, HR, Power?, Cadence? }, … ] }

Available inner-Sample fields include HR, Power, Cadence, GPSAltitude, Latitude, Longitude, UTC, EHPE, NumberOfSatellites, Events. Samples that don't contain any requested field are dropped. Omit both args to keep the legacy {key, base64} passthrough.

Claude Desktop

Edit the desktop config file (quit and reopen Claude after editing):

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
  • Linux: ~/.config/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "suuntool": {
      "command": "suuntool",
      "args": ["mcp"]
    }
  }
}

For writes/deletes, expand args:

"args": ["mcp", "--allow-write", "--allow-destructive"]

Claude Desktop does not inherit your shell's $PATH, so if which suuntool returns something like /Users/you/go/bin/suuntool (a go install build), use the absolute path in command. Homebrew installs (/opt/homebrew/bin/suuntool on Apple Silicon, /usr/local/bin/suuntool on Intel) are usually picked up automatically.

The 🔨 tool icon in the chat input should show suuntool once the app restarts.

Claude Code

One command — uses Claude Code's built-in MCP registry:

claude mcp add suuntool suuntool mcp                                          # read-only
claude mcp add suuntool -- suuntool mcp --allow-write                         # + writes
claude mcp add suuntool -- suuntool mcp --allow-write --allow-destructive     # + deletes

Note the -- separator before suuntool mcp when passing tier flags — without it, claude mcp add tries to parse --allow-write as one of its own options and errors out with unknown option '--allow-write'.

Scope flags control where the entry is stored:

  • --scope local (default): current project, this machine only
  • --scope user: every project, this machine
  • --scope project: written to .mcp.json, committed for the team

Verify inside a Claude Code session with the /mcp slash command — suuntool should appear with its tool count.

If suuntool isn't on $PATH, pass the absolute path: claude mcp add suuntool -- /Users/you/bin/suuntool mcp.

Codex (OpenAI CLI)

Edit ~/.codex/config.toml:

[mcp_servers.suuntool]
command = "suuntool"
args = ["mcp"]

For writes/deletes:

[mcp_servers.suuntool]
command = "suuntool"
args = ["mcp", "--allow-write", "--allow-destructive"]

Codex MCP servers are loaded on session start — restart any running codex process. List configured servers with codex mcp list; verify the tools at runtime with the /mcp slash command.

Use an absolute path in command if suuntool isn't on the Codex process's $PATH.

Sanity check

In any client, ask:

"Use suuntool to show my last 3 workouts."

The model should call workouts_list with limit: 3 and return rows with both activityId and activityName. If you see AUTH_EXPIRED, run suuntool login in a terminal and reconnect — the MCP server loads the on-disk session at startup and never re-auths silently.

Output

suuntool picks a sensible default and gets out of the way:

Flag Effect
--format auto (default) Pretty on a TTY, JSON when piped or redirected
--format json Force JSON (2-space indent)
--format pretty Force pretty rendering — aligned tables for list responses (workouts list, workouts stats, workouts comments, wellness sleep); key/value blocks for single records
--format tsv Tab-separated values for list responses (workouts list, workouts stats, workouts comments, workouts list --summary). Non-tabular responses fall back to JSON. Embedded tabs/newlines in cells are replaced with spaces
-o, --output <path> Write to a file instead of stdout — format inferred from extension (.json, .tsv)
--fields a,b,c Project list/object output to just these JSON keys before render (forces JSON). Skips piping through jq for trivial selection
--no-color Disable ANSI styling (also honors NO_COLOR)
--quiet, --verbose Tune log verbosity on stderr
--timeout <dur> HTTP timeout, e.g. 45s
suuntool profile settings -o settings.json          # save to file
suuntool whoami --format json | jq .username        # pipe into jq
NO_COLOR=1 suuntool whoami                          # plain-text TTY

Commands

Session

Command Endpoint Auth Notes
login --email <e> POST /v1/login2 no Reads password from a no-echo TTY prompt or --password-stdin
logout GET /v1/logout yes Invalidates server-side, clears local session
whoami GET /v1/user yes Current session user
doctor GET /v1/servertime + session check no Probe connectivity and session validity

Profile

Command Endpoint Auth Notes
profile settings GET /v1/user/settings yes Full user settings DTO (passed through)
profile follow GET /v1/user/follow yes Follower / following / blocked counts
profile user <name> GET /v1/user/name/{name} yes Look up a user by username

Workouts

Command Endpoint Auth Notes
workouts list GET /v1/workouts yes Paginated list with --since, --limit, --offset; returns cursor until. --summary collapses the window into totals + per-activity table with a ΔWoW (week-over-week count change) column, green/red on a TTY. --stream emits NDJSON and auto-paginates across every until cursor (no 100-page ceiling; --limit becomes optional cap)
workouts get <key> GET /v1/workouts/{key} yes One workout's metadata
workouts stats [user] GET /v1/workouts/{user}/stats yes Aggregate totals + per-activity breakdown
workouts count GET /v1/workouts/count yes Requires until + sharingFlags server-side (defaults handled)
workouts sml <key> GET /v1/workouts/{key}/sml yes Full ~5MB sample-by-sample JSON. Raw passthrough — use -o
workouts fit <key> GET /v1/workout/exportFit/{key} yes Binary .fit. Raw passthrough — use -o
workouts export <key> --bundle <dir> (composite) yes One-shot bundle: writes workout.json, workout.sml.json, workout.fit, extensions.json, comments.json into <dir>. --no-fit/--no-sml/--no-extensions/--no-comments skip parts; --force overwrites a non-empty dir

Wellness (24/7 health timeline)

Command Endpoint Auth Notes
wellness sleep GET 247.../v1/sleep/export yes Pretty table on TTY (dedup'd by sleepId, units converted: Hz→BPM, fractions→%); raw NDJSON when piped or with -o/--out
wellness activity GET 247.../v1/activity/export yes Per-15-min hr/steps/energy. hr is in Hz — ×60 for BPM
wellness recovery GET 247.../v1/recovery/export yes balance ∈ 0..1 = "wake-up resources"
wellness sleepstages GET 247.../v1/sleepstages/export yes Stage timeline (light/deep/REM/…)

Workout interactions & writes ⚠️

These mutate server state. The delete command requires --yes in non-TTY contexts.

Command Endpoint Auth Notes
workouts comments <key> GET /v1/workouts/comments/{key} yes List comments on a workout (note plural comments/)
workouts comment <key> [text] POST /v1/workouts/comment/{key} yes + x-totp Post a comment; --stdin for multi-line
workouts uncomment <comment-key> DELETE /v1/workouts/comment/{commentKey} yes Delete a comment by comment-key (NOT workout-key)
workouts react <key> POST /v1/workouts/reaction/{key} yes + x-totp --reaction like (only supported value)
workouts unreact <key> DELETE /v1/workouts/reaction/{key} yes Remove your reaction
workouts edit <key> --set field=<json> PUT /v1/workouts/{key}/attributes yes Partial attribute update; values parsed as JSON literals
workouts batch-update --file <json> POST /v1/workouts/batchUpdate yes Bulk updates from a JSON array
workouts share <key> --as gpx-route|gpx-track PUT /v1/workouts/{user}/{key}/share/{format} yes Returns a signed GPX URL
workouts extensions <key> POST /v1/workout/extensions/{key} yes Despite POST, this is a fetch — body is the filter list
workouts upload --sml <path> [--extensions <path>] POST /v1/workout (multipart) yes Upload a pre-built SML file. Streams via io.Pipe. Does NOT generate SML from raw GPS
workouts delete <key> DELETE /v1/workouts/{key}/delete yes Destructive. TTY confirmation prompt; pass --yes in scripts/agents

x-totp writes. comment and react require a fresh 6-digit TOTP. suuntool auto-derives one from your session — you don't need to do anything. Codes rotate every 30s, so commands generate per call.

--yes and destructive operations. workouts delete refuses to run on a non-TTY without --yes (exit code 2). This prevents agents and CI scripts from accidentally bypassing the confirmation by piping nothing into stdin.

Rate limiting. Suunto's quotas are conservative (a few QPS). Don't batch-spam comments or reactions — your account can be flagged.

MCP server

suuntool mcp runs an MCP stdio server. See the dedicated MCP server section above for full config snippets for Claude Desktop, Claude Code, and Codex.

Discovery / meta

Command Endpoint Auth Notes
endpoints no Stable command → method/path table for agents (--format json)
version no Build version

Run suuntool <command> --help for the full reference of any command. The mapping above is also available as a JSON document via suuntool endpoints --format json.

Raw passthrough. workouts sml, workouts fit, and the four wellness subcommands stream their response body straight to stdout (or -o) without going through the --format pretty-printer. The body is already in its final shape (large JSON / binary .fit / NDJSON) and reformatting it would waste memory and lose fidelity.

Exit codes

Code Meaning
0 OK
1 Generic error
2 Bad usage (flags/args)
3 Network (timeout, DNS, connection)
4 Auth — session missing or expired (run suuntool login)
5 Server error (5xx or malformed response)
6 Not found
7 Forbidden

In --format json mode, errors are emitted to stderr as:

{ "error": { "code": "AUTH_EXPIRED", "message": "...", "hint": "Run: suuntool login" } }

Environment

Variable Purpose
SUUNTOOL_SESSION_FILE Override session storage path (default $XDG_CONFIG_HOME/suuntool/session.json)
SUUNTOOL_FORMAT Default output format
SUUNTOOL_TIMEOUT Default HTTP timeout
SUUNTOOL_BASE_URL Override API base URL (for testing)
NO_COLOR Disable ANSI styling

Security & privacy

  • Your session key is persisted to $XDG_CONFIG_HOME/suuntool/session.json with mode 0600. Don't put it in your dotfiles repo.
  • Passwords are read from a no-echo TTY prompt or stdin. suuntool never accepts --password on the command line.
  • No telemetry, no analytics, no phone-home. The binary talks to Suunto's servers and nowhere else.

⚠️ Disclaimers

This is an unofficial, experimental tool. Use at your own risk.

  • suuntool is not affiliated with, endorsed by, or supported by Suunto Oy, Amer Sports, or Sports-Tracker. All trademarks belong to their respective owners.
  • The Suunto API is not public. The wire contract can change without notice. A future app release may break this tool with no warning.
  • Using this tool may violate Suunto's Terms of Service. Read them before you run anything. Heavy or abusive usage may get your account flagged or banned. The author accepts no responsibility for account actions taken against you.
  • Provided "as is", without warranty of any kind, express or implied, including but not limited to merchantability, fitness for a particular purpose, and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability arising from your use of the software.
  • For your own data only. Do not use this tool to scrape, harvest, or aggregate other users' data — you'll get your account banned, and you may be breaking the law.
  • The author is not a lawyer. Nothing here is legal advice.

If any of the above makes you uncomfortable, use the official Suunto app instead.

License

MIT.

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
internal
api
auth
Package auth implements the reverse-engineered Suunto login signing pipeline: KeyObfuscator XOR, secret derivation, RFC 6238 TOTP, and SHA-256 signatures.
Package auth implements the reverse-engineered Suunto login signing pipeline: KeyObfuscator XOR, secret derivation, RFC 6238 TOTP, and SHA-256 signatures.
mcp
Package mcp exposes suuntool endpoints as Model Context Protocol tools.
Package mcp exposes suuntool endpoints as Model Context Protocol tools.
session
Package session manages the persisted Suunto auth session on disk.
Package session manages the persisted Suunto auth session on disk.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL