Skip to content

Latest AI audit result

Two AI models, Claude Opus 5.5 and GPT-6.1 Sol, checked the Tokrate companion’s source code against 16 privacy and security promises. Their results are published here unedited, so you can see what they found before you install or turn on sharing.

Tokrate Mac 0.1.20, Windows and Linux 0.1.20-desktop-alpha.1commit cf4b743…reviewed 8 October 2026

Released as 0.1.20 from commit cd39455: the reviewed code plus one fix for a finding below and a test-only change for Windows (see every changed line).

Git ref
v0.1.20
Prompt
On GitHub

An AI audit of the client source code. It is not a certification and not a professional audit; see what it does not cover below.

What each reviewer concluded

Claude Opus 5.5

Claude Code · claude-opus-5-5

Overall verdict: Pass with notes

“Both clients upload only the documented numbers after opt-in and stay read-only; a minor upload-timing edge case can reveal when a delayed sample became ready.”

The reviewer’s own summary
Verified
15
Partial
1
Contradicted
0
Not verifiable
0
Critical
0
High
0
Medium
0
Low
1
Info
6

GPT-6.1 Sol

Codex CLI 0.160.1 · reasoning effort high · gpt-6.1-sol

Overall verdict: Pass with notes

“Both clients protect upload privacy and consent, but the Mac client has a service-triggered crash and an updater-content caveat.”

The reviewer’s own summary
Verified
14
Partial
0
Contradicted
0
Not verifiable
2
Critical
0
High
0
Medium
1
Low
1
Info
0

Model, tool and reasoning effort are the run details we recorded ourselves. Each raw file also holds what the model reported about itself.

The 16 promises they checked

Each reviewer gave a separate verdict for the Mac app and for the Windows and Linux app. Open a row for the full promise, the reasoning and the code locations the reviewer cited.

  • Verified. The code does what the promise says, on every path the reviewer traced.
  • Partial. The promise holds in general, with a gap that a finding describes.
  • Contradicted. The code does what the promise says it does not do.
  • Not verifiable. The source code cannot settle the question.
  • Not applicable. The promise does not apply to this client.
C01 Upload allowlistClaudeMac: VerifiedWindows and Linux: VerifiedCodexMac: VerifiedWindows and Linux: Verified

Each community upload is built from an explicit field allowlist and contains only the fields documented in README.md (section “Optional community sharing”) and docs/metrics-contract.md. It never contains prompts, responses, code, file or folder paths, session, turn or account identifiers, the local deduplication digest, raw originator or entrypoint strings, user names, host names or hardware identifiers.

Claude

Mac: Verified · Windows and Linux: Verified · confidence high · findings F06

Mac SharedSample encodes 24 fixed keys through a hand-written encode(to:) and SampleEnvelope adds schemaVersion/sentAt/samples; Rust SharedSample is a serde struct with the same 24 keys and a single envelope builder. Every value comes from allowlisted tuples (client/parser/metric), regex-checked model/clientVersion, allowlisted provider/effort/sourceKind, the surface enum, numbers and a fresh UUID; the local digest id is never copied. All keys are described in README/metrics contract. Desktop sends Grok's clientVersion where the Mac never does (F06, info).

Codex

Mac: Verified · Windows and Linux: Verified · confidence high

Both serializers explicitly select 24 sample fields plus the three envelope keys. Traced tool metadata and numeric usage through normalized metrics, sanitization, serialization and signing. No raw content, paths, source identifiers or deduplication digest enters the payload. All transmitted body keys are documented; sampleId is a fresh upload UUID, not a source turn identifier.

C02 Content is not retainedClaudeMac: VerifiedWindows and Linux: VerifiedCodexMac: VerifiedWindows and Linux: Verified

Parsers keep only numeric usage, timestamps, model and provider identifiers, reasoning effort and the documented category fields. Prompt and response text, tool output, code and paths are not stored beyond parsing. Sources the documentation says are never read are really never opened or queried: Grok chat_history.jsonl and updates.jsonl; the OpenCode part table and full message.data; Antigravity step_payload, trajectory_metadata_blob, render_info, task_details, permissions, error_details and battle_mode_infos.

Claude

Mac: Verified · Windows and Linux: Verified · confidence high

JSONL parsers parse each line transiently and keep only ids (hashed into digests), timestamps, token counts, regex-checked model/version, allowlisted effort and the surface category; text is only inspected for tool_result/interruption markers. Grok monitors only open events.jsonl, usage.json and summary.json. OpenCode SQL selects session(id,parent_id,version) and json_extract paths of message.data; part is never queried. Antigravity SQL selects only steps(idx,has_subtrajectory,metadata), executor_metadata.data, gen_metadata.data and parent_references existence.

Codex

Mac: Verified · Windows and Linux: Verified · confidence high

Parsers discard parsed prompt/response/tool bodies, retaining measurements and allowed metadata. Grok opens only its three metadata/usage files. OpenCode SQL selects JSON properties, never the part table or full message.data. Antigravity queries omit every prohibited column. Incremental JSONL input may remain buffered until a line completes, with a 1 MiB cap.

C03 Minimal local storageClaudeMac: VerifiedWindows and Linux: VerifiedCodexMac: VerifiedWindows and Linux: Verified

Local history stores only normalized metric records and checkpoints (with SHA-256 path digests, never raw paths or raw session or turn identifiers). Records are kept for 7 days and at most 50,000 turns while the app runs; expired records are removed within about an hour, also while monitoring is paused (nothing runs while the app is not running). Live per-response values are memory only.

Claude

Mac: Verified · Windows and Linux: Verified · confidence high · findings F04

History files hold schemaVersion, TurnMetric records (digest id, no paths or raw ids) and checkpoints with SHA-256 path digests, inode/identity, size, mtime, version key. Both cap at 50,000 and 7 days. Mac prunes every poll (saved within 10 s) and hourly while paused; desktop prunes at most every 10 min on a poll loop that keeps running while paused. Live responses stay in memory. Source-folder paths are kept in settings/preferences, not history (F04, info).

Codex

Mac: Verified · Windows and Linux: Verified · confidence high

History contains normalized records and digest-based checkpoints, capped at seven days and 50,000 turns. Swift has hourly paused maintenance; Rust checks pruning every ten minutes through the still-running polling task. Live response values stay in memory. History replacement is atomic and owner-only on Unix; Windows uses the user's application-data directory and inherited ACLs.

C04 Read-only access to coding-tool dataClaudeMac: VerifiedWindows and Linux: VerifiedCodexMac: VerifiedWindows and Linux: Verified

The app never writes, modifies, deletes or renames other tools’ session files or databases, and never takes an exclusive lock on them. SQLite databases are opened read-only (mode=ro, never immutable=1); SQLite’s normal shared lock for the duration of a short read transaction is expected and does not break this claim.

Claude

Mac: Verified · Windows and Linux: Verified · confidence medium · findings F02

Source files are opened O_RDONLY|O_NONBLOCK (Mac) or OpenOptions read-only (Rust; Windows std share mode allows other writers) and never written, renamed or deleted; the only writes go to the app's own folder, and the Mac CLI export refuses destinations inside source roots. SQLite is opened with SQLITE_OPEN_READONLY and a file: URI with mode=ro, never immutable, inside one short read transaction. SQLite's own WAL handling may create -wal/-shm sidecars (F02, info).

Codex

Mac: Verified · Windows and Linux: Verified · confidence high

Coding-tool files are opened for reading only. SQLite uses mode=ro and read-only connection flags with short normal transactions and a 500 ms busy timeout; neither implementation sets immutable=1. Writes are confined to Tokrate state and explicit exports, with export destinations checked against coding-tool source roots.

C05 Affirmative consentClaudeMac: VerifiedWindows and Linux: VerifiedCodexMac: VerifiedWindows and Linux: Verified

No sample is uploaded and no community request is made until the user explicitly opts in on the current notice version. “Only for local use” and a saved OFF stay OFF across launches and upgrades. Raising the notice version requires a new opt-in.

Claude

Mac: Verified · Windows and Linux: Verified · confidence high

Mac: sharing is requested only when the saved choice is true AND the consent record is the current notice (4) with action contribute; otherwise activate() calls disable() and the Keychain is never touched. The toggle can only show the notice. Desktop: sharing_authorized requires sharing=true plus an Accepted consent with the exact SHARING_NOTICE_VERSION; a stale or corrupt setting is forced off, update() refuses sharing=true, and record_sharing_consent rejects a different notice version. Saved OFF/declined never prompts or enables.

Codex

Mac: Verified · Windows and Linux: Verified · confidence high

Saved OFF remains OFF. ON requires affirmative acceptance of the current versioned notice; old or invalid consent cannot enable requests. The Tauri settings patch cannot silently turn sharing on. Both implementations persist consent before enabling credentials and the upload loop, and fail closed on missing or invalid settings.

C06 Consent screen is accurateClaudeMac: VerifiedWindows and Linux: VerifiedCodexMac: VerifiedWindows and Linux: Verified

The sample payload and field description the consent screen shows match the structure and fields that are actually uploaded.

Claude

Mac: Verified · Windows and Linux: Verified · confidence high

Both consent screens render the example by running fake values through the real allowlist and envelope encoder (Mac SamplePayload uses SharedSample+SampleEnvelope; desktop sent_example calls example_request_json, which uses from_metric and envelope_value, the same path as signed_request). Field descriptions name the same categories, the 5-minute rounding, the random sample ID and the public-key pseudonym. The coding-tool version is shown in the example though only implicitly in the prose.

Codex

Mac: Verified · Windows and Linux: Verified · confidence high

Both consent examples use the production sample and envelope serializers, rather than a separately invented structure. The disclosures describe the numeric fields, categories, five-minute timestamps and pseudonymous signing identity that are actually sent.

C07 Sharing OFF stops network activityClaudeMac: VerifiedWindows and Linux: VerifiedCodexMac: VerifiedWindows and Linux: Verified

Switching sharing off cancels the upload loop, clears pending uploads and stops community statistics and alert fetches. While sharing is off, no request is made to the community endpoints.

Claude

Mac: Verified · Windows and Linux: Verified · confidence high

Mac disable() bumps the generation, cancels the loop task, clears queue/seen/board/key and every await in refresh() is followed by an isEnabled+generation check before upload or board fetch. Desktop withdraw_sharing/stop_sharing run before any fallible save, abort the network task, disable the queue and clear the board; every request in sharing_loop is created only under s.valid(generation). Board fetches exist only inside these sharing loops.

Codex

Mac: Verified · Windows and Linux: Verified · confidence high

OFF cancels the active network task, invalidates its generation, clears pending uploads and board state, and disables future community polling. Every upload and board path checks current authorization. A request already delivered while ON cannot be recalled; neither client starts a new community request while OFF.

C08 No backfillClaudeMac: VerifiedWindows and Linux: VerifiedCodexMac: VerifiedWindows and Linux: Verified

Only turns completed after the current launch or the latest switch-on are eligible for upload; historical turns are never uploaded.

Claude

Mac: Verified · Windows and Linux: Verified · confidence high

Mac enable() sets consentStartedAt=now at launch activation or switch-on, and enqueue() drops any metric with completedAt before it (or in the future); HistoryStore only passes records new to history and final. Desktop SharingQueue.enable records enabled_since when the sharing loop starts (after launch/consent/retry), disable() clears it, and enqueue() skips completed_at < enabled_since; ingest() enqueues only while sharing_active. Replayed history therefore never qualifies.

Codex

Mac: Verified · Windows and Linux: Verified · confidence high

Eligibility begins at activation in the current process and resets on every re-enable. Queue insertion requires completion at or after that boundary and rejects future timestamps. Loaded historical records are not fed into sharing; newly discovered records still undergo the boundary check.

C09 Pseudonymous identityClaudeMac: VerifiedWindows and Linux: VerifiedCodexMac: VerifiedWindows and Linux: Verified

The installation identity is a random Ed25519 key stored in the OS credential store (macOS Keychain, Windows Credential Manager, Linux Secret Service). Only the public key and signatures are sent; the private key never leaves the store or the process. No user name, host name, hardware or device identifier, MAC address or serial number is collected or sent.

Claude

Mac: Verified · Windows and Linux: Verified · confidence high

Mac creates a CryptoKit Curve25519 signing key and stores it as a generic password (AfterFirstUnlockThisDeviceOnly). Desktop draws 32 bytes from OsRng and stores them via keyring (Windows native, macOS native, Linux Secret Service), held in Zeroizing memory. Requests carry only the base64 public key and signature headers; no code reads user, host, hardware or MAC identifiers, and the ephemeral/cookieless clients add none. Sparkle system profiling is off.

Codex

Mac: Verified · Windows and Linux: Verified · confidence high

Mac generates an Ed25519 key and stores it in Keychain; Rust uses OS randomness and native keyring backends for macOS, Windows or Linux. Only base64 public keys and signatures enter HTTP headers. Private key material is held inside native process memory for signing and never exposed through IPC. No username, hostname, hardware identifier, MAC address or serial collection path was found.

C10 Known network destinations onlyClaudeMac: VerifiedWindows and Linux: VerifiedCodexMac: Not verifiableWindows and Linux: Verified

All network requests go to tokrate.dev (sample upload, community statistics, update feeds) or GitHub (update packages), over HTTPS. Update packages are accepted only from this repository’s GitHub release assets; update requests may follow ordinary HTTP redirects (GitHub serves release assets from its own download hosts), which is documented and expected, provided packages are installed only after signature verification. Sharing requests reject redirects and use no cookies or persistent cache. No other host is contacted, and local-only features (history export, tokrate inspect) make no network requests.

Claude

Mac: Verified · Windows and Linux: Verified · confidence medium · findings F03

The only request code targets https://tokrate.dev/api/public/v1/{samples,board}, the fixed update feeds on tokrate.dev, and package URLs that both clients pin to https://github.com/mattivilola/tokrate-clients/releases/download/. Sharing clients reject redirects and use no cookies or cache (Mac ephemeral session with nil cookie storage/cache; reqwest without cookie feature, https_only, redirect none). tokrate inspect and export-history make no requests. Sparkle release-notes links are not pinned (F03, info).

Codex

Mac: Not verifiable · Windows and Linux: Verified · confidence medium

Community requests use fixed HTTPS endpoints, no redirects, cookies or persistent cache. Update feeds are fixed; package URLs are checked against this repository's GitHub release assets. Inspect/export are local. Mac package restrictions do not constrain Sparkle release-note URLs/content: the standard driver is used and the published appcast is unavailable offline, leaving possible additional destinations unsettled.

C11 No tracking SDKsClaudeMac: VerifiedWindows and Linux: VerifiedCodexMac: VerifiedWindows and Linux: Verified

No analytics, advertising, crash-reporting or telemetry SDK is included in any dependency set, and no separate install, launch or usage event is sent.

Claude

Mac: Verified · Windows and Linux: Verified · confidence high

Package.resolved pins only Sparkle 2.10.0. The Rust lock files contain Tauri, reqwest, rusqlite, keyring, notify, rfd and their transitive crates; log/tracing are facades with no subscriber or exporter crate present. package-lock.json holds React, lucide-react, @tauri-apps/api and build/test tooling. No analytics, advertising, crash-reporting or telemetry package was found, and no install/launch/usage event is sent by app code.

Codex

Mac: Verified · Windows and Linux: Verified · confidence high

Read every lock file and direct dependency manifest. No analytics, advertising, crash-reporting or telemetry SDK, or independent install/launch/usage beacon, was found. Networking dependencies serve sharing and signed updates. log/tracing dependencies provide local logging infrastructure, with no telemetry exporter configured.

C12 Update integrity and privacyClaudeMac: VerifiedWindows and Linux: VerifiedCodexMac: VerifiedWindows and Linux: Verified

Updates are verified against public keys embedded in the app before installation (Sparkle EdDSA on Mac, Tauri updater minisign on Windows and Linux), and installation requires a user action. Update checks send no contribution key, measurements or coding-tool content. Sparkle system-profile reporting is disabled. The automatic-check switch is respected.

Claude

Mac: Verified · Windows and Linux: Verified · confidence high

Mac release bundles embed SUPublicEDKey, require a signed feed, verify before extraction, disable automatic install and system profiling (also set in code), and the Settings switch drives automaticallyChecksForUpdates. Desktop embeds a minisign pubkey (build.rs asserts it matches the repo key), checks only for NSIS/AppImage, gates automatic checks on the saved switch with a 24 h throttle, and installs only from the install_update command bound to a button; the download URL is checked before storing and before installing. Neither path sends the contribution key.

Codex

Mac: Verified · Windows and Linux: Verified · confidence high

Release builds embed updater public keys. Sparkle requires a signed feed and signature verification before extraction; Tauri verifies minisign through its updater plugin. Both download/install paths require user action. Automatic-check preferences are respected, and Mac system-profile reporting is disabled. Update requests use a separate updater client with no contribution identity, measurements or coding-tool content.

C13 Bounded memory-only queueClaudeMac: VerifiedWindows and Linux: VerifiedCodexMac: VerifiedWindows and Linux: Verified

Unsent uploads are held only in memory, capped at 1,000 samples and 24 hours, and are lost on quit. Nothing pending is written to disk.

Claude

Mac: Verified · Windows and Linux: Verified · confidence high

Mac SharingSession keeps Pending samples in an in-memory array, drops the oldest at 1,000 on every insert and prunes samples whose observedAt is over 24 h old; nothing in it is persisted. Desktop SharingQueue is a VecDeque capped at 1,000 with the same 24 h prune; Runtime snapshots expose only the count and history saves only records/checkpoints.

Codex

Mac: Verified · Windows and Linux: Verified · confidence high

Both upload queues are process-memory collections, capped at 1,000 samples and pruned after 24 hours. OFF clears them and quit loses them. Persistence paths write normalized local history/preferences/checkpoints, never pending upload envelopes or retry state.

C14 No sensitive loggingClaudeMac: VerifiedWindows and Linux: VerifiedCodexMac: VerifiedWindows and Linux: Verified

No prompts, responses, paths, session identifiers, keys or signatures are written to logs, the console, the OS log or crash output.

Claude

Mac: Verified · Windows and Linux: Verified · confidence high

No print/NSLog/os_log/Logger in Swift app or core; the CLI writes only usage, fixed errors and export counts by client/model to stderr. Rust has a single eprintln naming the source id and an error kind, no path; no console.* in the UI; no log subscriber is installed. Panic/expect messages are static strings.

Codex

Mac: Verified · Windows and Linux: Verified · confidence high

Searched all tracked production Swift/Rust/frontend sources for console, file and OS logging sinks and traced their arguments. Native watcher messages contain fixed source labels and categorized errors; CLI output is normalized metrics and generic errors. No coding content, source paths, source identifiers, private/public keys or signatures are passed to logging sinks.

C15 No remote code or contentClaudeMac: VerifiedWindows and Linux: VerifiedCodexMac: Not verifiableWindows and Linux: Verified

The app UI loads no remote web content and executes no downloaded code other than signed updates. On Windows and Linux the Tauri content security policy and capabilities limit the frontend to what it needs, and the IPC commands it exposes cannot read arbitrary files or reach arbitrary network destinations (fixed-purpose commands such as an update check or switching sharing on, which reach only the endpoints of C10, are expected).

Claude

Mac: Verified · Windows and Linux: Verified · confidence high

The Mac UI is SwiftUI with no web view in app code; links open the system browser. Desktop windows load the bundled index.html; the CSP limits scripts to 'self' and connections to IPC; capabilities grant only the 19 app commands (no core/plugin permissions), enforced by a unit test. Commands take enumerated strings or booleans; folder roots come only from the native picker or defaults, snapshot returns parsed records, and network commands reach only the C10 endpoints.

Codex

Mac: Not verifiable · Windows and Linux: Verified · confidence medium

Tauri ships a local frontend, restrictive CSP and explicit capabilities; IPC exposes fixed-purpose actions, normalized snapshots and a native folder picker, not arbitrary file/network primitives. Main Mac UI is native SwiftUI. Sparkle's standard update UI can display appcast-supplied release notes, and repository configuration does not constrain or disable that remote content, so the absolute Mac promise cannot be settled.

C16 Upload timingClaudeMac: PartialWindows and Linux: PartialCodexMac: VerifiedWindows and Linux: Verified

A sample is uploaded only after its five-minute observedAt period has ended, after a random delay, so neither the request’s sentAt nor its sending time places a turn more precisely than its five-minute period. Community statistics fetches do not reveal when turns completed.

Claude

Mac: Partial · Windows and Linux: Partial · confidence medium · findings F01

Both assign each sample the first 5-minute boundary at or after max(queue time, observedAt+600 s) plus one OsRng/SystemRandom jitter per slot, so normally settled turns leave at slot+jitter and sentAt is the send time; board fetches follow their own 30 s cadence. But enqueue never wakes the sharing loop: a sample queued less than ~30 s before its slot time leaves at the next board wake, together with the board fetch, which reveals its queue time to about 30 s (F01). Turn completion stays within its period.

Codex

Mac: Verified · Windows and Linux: Verified · confidence high

Uploads wait until at least observedAt+600 seconds and a slot-wide random delay, rather than a delay tied to precise completion time. Later queue additions are rounded to a five-minute sending slot. Retries preserve eligibility. Community board polling uses its independent cadence and is not triggered by turn completion.

What leaves your computer

This is what the reviewers found in the code, listed by each of them and merged here. Uploads happen only after you opt in to sharing. See one contribution, field by field, and the privacy policy for how the server treats them.

Fields in an upload (27)

Documented means the reviewer found the field named in the README or the metrics contract. “Can hold user content” is the reviewers’ answer to whether the value could contain something the user wrote or a file path.

FieldTypeWhere the value comes fromDocumentedCan hold user content
schemaVersionintegerEnvelope constant 1YesNo
sentAtstringEnvelope: wall-clock time the request is built (ISO 8601, seconds)YesNo
samplesarrayEnvelope: 1-50 SharedSample objectsYesNo
sampleIdstringFresh random UUID per sample; kept across retriesYesNo
observedAtstringcompletedAt floored to a 5-minute UTC bucketYesNo
clientstringCoding-tool id from the supported client/parser/metric tuple allowlistYesNo
clientVersionstringCoding tool's own version if it matches ^[A-Za-z0-9.+_-]{1,40}$, else unknownYesNo
appVersionstringConstant 0.1.20YesNo
parserVersionstringFrom the allowlisted tupleYesNo
metricVersionstringFrom the allowlisted tupleYesNo
modelstringModel id as logged if ^[A-Za-z0-9._-]{1,80}$, else unknown (Claude Bedrock/Vertex ids normalized)YesNo
providerstringAllowlisted provider (openai, anthropic, xai, google, amazon-bedrock, google-vertex) or unknownYesNo
reasoningEffortstringAllowlisted effort value or unknownYesNo
sourceKindstringprimary, subagent or unknownYesNo
outputTokensintegerTurn output token count (0-10,000,000)YesNo
reasoningOutputTokensinteger|nullReasoning tokens if within 0..outputTokensYesNo
durationMsnumberWhole-turn duration in ms (1 ms - 24 h)YesNo
ttftMsnumber|nullCodex-reported time to first token, Codex onlyYesNo
responseOutputTokensinteger|nullSum over qualifying API responses, null unless plausibleYesNo
responseDurationMsnumber|nullSummed response durations in msYesNo
responseCountinteger|nullNumber of qualifying responsesYesNo
providerRegionstring|nullBedrock inference-profile region from allowlist, null for other providersYesNo
delegatedOutputTokensinteger|nullSubagent output attributed to a primary turn; null for subagent recordsYesNo
surfacestring|nullCategory cli/desktop/ide/sdk/other derived from originator/entrypoint/folderYesNo
inputTokensinteger|nullTurn input tokens incl. cached, null when not reportedYesNo
cacheReadInputTokensinteger|nullCached input tokens read, <= inputTokensYesNo
cacheWriteInputTokensinteger|nullCache-write tokens (Claude Code, OpenCode anthropic) else nullYesNo

Where reviewers worded a value’s source differently, the table shows the first reviewer’s wording. Every wording is in the raw files.

Network requests (12)

  • POSThttps://tokrate.dev/api/public/v1/samples

    Mac, Windows and Linux · listed by Claude, Codex

    Purpose
    Upload signed batches of allowlisted turn measurements
    When
    Only while sharing is authorized on the current notice; at 5-minute slots plus jitter, retries at most every 30 s
    Sends
    Envelope body; X-Tokrate-Key (stable public key), X-Tokrate-Signature, Content-Type, User-Agent Tokrate/0.1.20 (Mac also Accept, Accept-Language: en); source IP
    What each reviewer wroteTheir own words, with the code they cited

    Claude

    Purpose. Upload signed batches of allowlisted turn measurements

    When. Only while sharing is authorized on the current notice; at 5-minute slots plus jitter, retries at most every 30 s

    Sends. Envelope body; X-Tokrate-Key (stable public key), X-Tokrate-Signature, Content-Type, User-Agent Tokrate/0.1.20 (Mac also Accept, Accept-Language: en); source IP

    Codex

    Purpose. Upload signed community metric batches.

    When. Only with current affirmative consent, after bucket-based eligibility and random delay; cancelled on OFF.

    Sends. JSON envelope and 24 allowlisted sample keys; X-Tokrate-Key, X-Tokrate-Signature, Content-Type, fixed app-version User-Agent and Accept headers.

  • GEThttps://tokrate.dev/api/public/v1/board

    Mac, Windows and Linux · listed by Claude, Codex

    Purpose
    Fetch community statistics and alerts (max 1 MiB)
    When
    Only while sharing is authorized; about every 30 s
    Sends
    No key or body; User-Agent Tokrate/0.1.20 (Mac also Accept, Accept-Language: en); source IP and request cadence
    What each reviewer wroteTheir own words, with the code they cited

    Claude

    Purpose. Fetch community statistics and alerts (max 1 MiB)

    When. Only while sharing is authorized; about every 30 s

    Sends. No key or body; User-Agent Tokrate/0.1.20 (Mac also Accept, Accept-Language: en); source IP and request cadence

    Codex

    Purpose. Fetch community statistics and alerts.

    When. Independent periodic cadence while sharing is authorized; cancelled and disabled on OFF.

    Sends. No body, contribution key or signature. Fixed app-version User-Agent and normal Accept metadata; Mac uses fixed Accept-Language en.

  • GEThttps://tokrate.dev/updates/macos/stable.xml

    Mac · listed by Claude, Codex

    Purpose
    Sparkle signed appcast check
    When
    Sparkle's schedule when automatic checks are on (default), or Check for Updates; independent of sharing
    Sends
    Sparkle default request (app name/version, Sparkle UA); system profile disabled; no contribution key
    What each reviewer wroteTheir own words, with the code they cited

    Claude

    Purpose. Sparkle signed appcast check

    When. Sparkle's schedule when automatic checks are on (default), or Check for Updates; independent of sharing

    Sends. Sparkle default request (app name/version, Sparkle UA); system profile disabled; no contribution key

    Codex

    Purpose. Check signed Mac update feed.

    When. Automatic checks when enabled, or explicit manual check; independent of community sharing.

    Sends. Standard Sparkle HTTP metadata such as app/updater version; no contribution key, metrics or coding content; system-profile reporting disabled.

  • GEThttps://tokrate.dev/updates/desktop/alpha.json

    Windows and Linux · listed by Claude, Codex

    Purpose
    Tauri updater feed check (NSIS and AppImage only)
    When
    On UI start and periodically if the automatic switch is on (24 h throttle), or a manual check; independent of sharing
    Sends
    Tauri updater plugin default request; no contribution key or measurements
    What each reviewer wroteTheir own words, with the code they cited

    Claude

    Purpose. Tauri updater feed check (NSIS and AppImage only)

    When. On UI start and periodically if the automatic switch is on (24 h throttle), or a manual check; independent of sharing

    Sends. Tauri updater plugin default request; no contribution key or measurements

    Codex

    Purpose. Check Windows/Linux update metadata.

    When. Enabled automatic checks or manual user check on installations supporting the native updater; independent of sharing.

    Sends. Standard updater HTTP metadata; no contribution key, measurements or coding-tool content.

  • GEThttps://github.com/mattivilola/tokrate-clients/releases/download/{tag}/{asset}

    Mac, Windows and Linux · listed by Claude, Codex

    Purpose
    Download a signed update package (may redirect to GitHub asset hosts)
    When
    Only after the user chooses to install an available update
    Sends
    Ordinary HTTP request from the updater; no contribution key or measurements
    What each reviewer wroteTheir own words, with the code they cited

    Claude

    Purpose. Download a signed update package (may redirect to GitHub asset hosts)

    When. Only after the user chooses to install an available update

    Sends. Ordinary HTTP request from the updater; no contribution key or measurements

    Codex

    Purpose. Download a signed update package, including a permitted Mac delta.

    When. After the user chooses an offered update; initial package URL must pass the release-repository allowlist.

    Sends. Updater download HTTP metadata, without contribution identity or coding-tool metrics/content.

  • otherhttps://tokrate.dev/{,privacy,terms,download}

    Mac, Windows and Linux · listed by Claude

    Purpose
    Website links opened in the system browser, not by the app's own HTTP stack
    When
    Only when the user clicks a link (works with sharing off)
    Sends
    Whatever the user's browser sends; nothing from the app
    What each reviewer wroteTheir own words, with the code they cited

    Claude

    Purpose. Website links opened in the system browser, not by the app's own HTTP stack

    When. Only when the user clicks a link (works with sharing off)

    Sends. Whatever the user's browser sends; nothing from the app

  • GEThttps://{github-download-host}/{redirect-path}

    Mac, Windows and Linux · listed by Codex

    Purpose
    Follow ordinary update-package download redirects.
    When
    When GitHub redirects a permitted release-asset download; signature verification precedes installation.
    Sends
    Standard updater download headers; no contribution headers or measurement body.
    What each reviewer wroteTheir own words, with the code they cited

    Codex

    Purpose. Follow ordinary update-package download redirects.

    When. When GitHub redirects a permitted release-asset download; signature verification precedes installation.

    Sends. Standard updater download headers; no contribution headers or measurement body.

  • GET{scheme}://{appcast-release-notes-host}/{path}

    Mac · listed by Codex

    Purpose
    Potential Sparkle release-note loading; actual URL is unknown.
    When
    Conditional on signed appcast release-note content and standard updater UI; no repository policy constrains this path.
    Sends
    Standard release-note/webview request metadata; no app code attaches contribution keys or metric bodies.
    What each reviewer wroteTheir own words, with the code they cited

    Codex

    Purpose. Potential Sparkle release-note loading; actual URL is unknown.

    When. Conditional on signed appcast release-note content and standard updater UI; no repository policy constrains this path.

    Sends. Standard release-note/webview request metadata; no app code attaches contribution keys or metric bodies.

  • GEThttps://tokrate.dev/

    Mac, Windows and Linux · listed by Codex

    Purpose
    Open the public website in the external browser.
    When
    Explicit click; independent of sharing and updater preferences.
    Sends
    Browser-controlled headers/cookies; the app does not attach its key, signature or metrics.
    What each reviewer wroteTheir own words, with the code they cited

    Codex

    Purpose. Open the public website in the external browser.

    When. Explicit click; independent of sharing and updater preferences.

    Sends. Browser-controlled headers/cookies; the app does not attach its key, signature or metrics.

  • GEThttps://tokrate.dev/privacy

    Mac, Windows and Linux · listed by Codex

    Purpose
    Open the privacy policy in the external browser.
    When
    Explicit click on Privacy, including during consent.
    Sends
    Browser-controlled headers/cookies; no app contribution identity or measurement payload.
    What each reviewer wroteTheir own words, with the code they cited

    Codex

    Purpose. Open the privacy policy in the external browser.

    When. Explicit click on Privacy, including during consent.

    Sends. Browser-controlled headers/cookies; no app contribution identity or measurement payload.

  • GEThttps://tokrate.dev/terms

    Mac, Windows and Linux · listed by Codex

    Purpose
    Open the terms in the external browser.
    When
    Explicit click on Terms, including during consent.
    Sends
    Browser-controlled headers/cookies; no app contribution identity or measurement payload.
    What each reviewer wroteTheir own words, with the code they cited

    Codex

    Purpose. Open the terms in the external browser.

    When. Explicit click on Terms, including during consent.

    Sends. Browser-controlled headers/cookies; no app contribution identity or measurement payload.

  • GEThttps://tokrate.dev/download

    Windows and Linux · listed by Codex

    Purpose
    Open desktop download information in the external browser.
    When
    Explicit desktop-downloads action on installations using manual updates.
    Sends
    Browser-controlled headers/cookies; no app contribution identity or measurement payload.
    What each reviewer wroteTheir own words, with the code they cited

    Codex

    Purpose. Open desktop download information in the external browser.

    When. Explicit desktop-downloads action on installations using manual updates.

    Sends. Browser-controlled headers/cookies; no app contribution identity or measurement payload.

Findings and our response

9 findings from the two reviewers: 1 fixed, 4 accepted and 4 open. A fixed finding links the change in the clients repository. An accepted finding was read and its behaviour or wording was left as it is, with the reason given. An open finding is not finished yet, and its response says what is planned.

Medium (1)

Codex F01: A community response can crash the Mac dashboardMediumMacFixed
What the reviewer found
The board decoder accepts a finite positive medianThroughput without a minimum. With a matching cohort, a 24h or 15m window, and at least three local turns, CommunityLine.make converts the computed percentage directly to Int. A service response with medianThroughput=1e-20 and a local median of 20 produces about 2e23 percent, outside Int's range, and Swift traps when the menu is rendered.
What it could mean for you
The community service can terminate the Mac app while sharing is on, interrupting monitoring and losing its memory-only upload queue.
The reviewer’s recommendation
Validate the ratio after arithmetic and use a checked integer conversion such as Int(exactly:), returning an unavailable comparison when it is non-finite or out of range.
Affects
Mac

Our response Fixed

Fixed in 0.1.20 after the review: a community median above the speed the clients upload themselves is ignored, and the percentage is never forced into an integer.

Commit 95f5c38 in the clients repository

Low (2)

Claude F01: A sample queued just before its slot leaves on the board cadence, timing its readiness to ~30 sLowMac, Windows and LinuxOpen
What the reviewer found
Both clients compute the sharing loop's sleep right after each pass: until the board is next due (30 s) or the earliest queued sample's slot+jitter. enqueue() (Mac SharingSession.enqueue; desktop SharingQueue::enqueue from Runtime::ingest) never wakes the loop. A sample whose slot is set by its queue time (a primary turn that waited for background subagents, or turns read after a resume) and that is queued less than ~30 s before slot+jitter is sent at the next board wake, in the same pass as the board GET, not at slot+jitter. Board GETs are visible every ~30 s, so a POST coinciding with one shows the sample was queued in the preceding ~30 s.
What it could mean for you
The service or a network observer can sometimes learn, to about 30 s, when a delayed sample became ready (e.g. when background subagent work finished or monitoring resumed), finer than the five-minute period the README promises. The turn's completion time is not revealed more precisely.
The reviewer’s recommendation
Wake the sharing loop when enqueue adds a sample whose eligible time is earlier than the current sleep target, or always sleep until the exact earliest slot+jitter; alternatively, if a sample is found past its eligible time at a board wake, postpone it to the next slot with fresh jitter.
Affects
Mac, Windows and Linux · promises C16

Our response Open

Planned for 0.1.21: the upload loop will wake exactly at each slot instead of at the next statistics fetch. A turn’s completion is still never placed more precisely than its five-minute period.

Codex F02: The inspect CLI buffers an unbounded number of turnsLowMacOpen
What the reviewer found
tokrate inspect repeatedly reads one-MiB batches until no bytes remain, appends every metric to one records array, then sorts and encodes the entire array into another in-memory buffer. The reader's line and per-poll byte limits do not bound this accumulated output. An arbitrarily large valid session file can exhaust memory; this requires a large source file and the user invoking inspect.
What it could mean for you
A very large local session can hang or terminate the CLI. The path does not upload content or alter the source file.
The reviewer’s recommendation
Stream inspection output or enforce a documented total record/byte limit and report truncation.
Affects
Mac

Our response Open

Planned for 0.1.21: tokrate inspect will stream records instead of holding them all. It is a local command you run yourself.

Info (6)

Claude F02: Read-only SQLite opens may still create -wal/-shm sidecar files in the tool's folderInfoMac, Windows and LinuxAccepted
What the reviewer found
Antigravity and OpenCode databases are opened with SQLITE_OPEN_READONLY and file:...?mode=ro, without immutable or readonly_shm. Per SQLite's documented WAL behaviour, a read-only connection to a WAL database opens the -shm wal-index read/write and can create missing -wal/-shm files when the directory is writable; it cannot checkpoint or delete them on close. The database contents are never modified by Tokrate's code.
What it could mean for you
Possibly empty -wal/-shm files left next to a closed tool's database. No data change; SQLite treats them as normal sidecars.
The reviewer’s recommendation
Document that SQLite may create its own coordination files, or skip a read when the database is in WAL mode and neither -wal nor -shm exists (the tool is not running and the main file is complete).
Affects
Mac, Windows and Linux · promises C04

Our response Accepted

SQLite creates its own coordination files for databases in WAL mode. Tokrate never writes the database contents.

Claude F03: Sparkle release-notes and info links are not pinned like package URLsInfoMacAccepted
What the reviewer found
UpdateDownloadPolicy restricts each appcast item's package and delta URLs to the project's GitHub release path, but not releaseNotesURL/fullReleaseNotesURL/infoURL. Sparkle fetches release notes from whatever host the item names and renders them in its own web view. The appcast must be signed (SURequireSignedFeed) and generate_appcast.sh publishes no notes link, so only the release key holder could add one.
What it could mean for you
No current defect. A future signed appcast with a notes link would make the Mac app contact a host outside C10's list when showing an update.
The reviewer’s recommendation
Ignore or reject items whose release-notes link is outside tokrate.dev or the GitHub release path, or embed notes inline in the signed appcast only.
Affects
Mac · promises C10, C15

Our response Accepted

Our signed update feed carries no release-notes link. Pinning that link as well is planned.

Claude F04: Source-folder paths are persisted in settings, outside the history fileInfoMac, Windows and LinuxAccepted
What the reviewer found
The desktop settings.json stores all five source roots, including defaults under the user's home folder (which contain the account name), and the Mac stores a chosen folder's path and security-scoped bookmark in UserDefaults. History and checkpoints contain no paths, so C03 holds, but the contract's wording that source paths are never persisted could be read more broadly.
What it could mean for you
Local configuration only (owner-only file on desktop); nothing is uploaded.
The reviewer’s recommendation
Persist only non-default roots on desktop and clarify in the docs that chosen source folders are kept in app preferences.
Affects
Mac, Windows and Linux · promises C03

Our response Accepted

Chosen source folders are kept only in the app’s own owner-only settings and are never uploaded. The documentation will say so.

Claude F05: CI exposes the updater signing key to the full Tauri buildInfoWindows and LinuxOpen
What the reviewer found
On pushes to main and manual runs, TAURI_SIGNING_PRIVATE_KEY and its password are in the environment of the step that runs `tauri build`, which compiles every third-party crate and runs their build scripts and the npm build. The Rust toolchain is 'stable' rather than a fixed version. Actions are pinned by SHA and secrets are not given to pull requests.
What it could mean for you
A compromised dependency build script in that step could read the updater signing key, which signs Windows/Linux updates.
The reviewer’s recommendation
Build unsigned artifacts first, then sign them in a separate step or job that runs only the signer with the key; pin the Rust toolchain version.
Affects
Windows and Linux · promises C12

Our response Open

Planned: build Windows and Linux installers unsigned and sign updates in a separate CI step, with a pinned Rust toolchain.

Claude F06: Desktop shares Grok Build's clientVersion that the Mac rules excludeInfoWindows and LinuxOpen
What the reviewer found
The Mac SharedSample refuses Grok Build records that carry a client version (grokClientVersion) and its Grok parser never sets one. The desktop Grok parser reads clientVersion from usage.json and from_metric sends it. The value is regex-bounded and the field is documented, so this is not a privacy defect, but the clients disagree; if the service rejects such samples, the desktop acks and drops the whole batch on 400/422.
What it could mean for you
Possible loss of other samples in the same batch; no extra personal data.
The reviewer’s recommendation
Apply one Grok clientVersion rule in both clients (and the contract), and consider rejecting single samples rather than whole batches.
Affects
Windows and Linux · promises C01

Our response Open

Planned for 0.1.21: one Grok Build client-version rule in both clients.

Claude F07: Signatures cover only the body; replay protection is left to the serverInfoMac, Windows and LinuxAccepted
What the reviewer found
Both clients sign the exact body bytes with Ed25519 and send the public key; the signature does not bind the method, path or host, and there is no nonce. Freshness and duplicate detection depend on the server checking sentAt and sampleId. Only a party that can read the TLS stream (the service or a TLS-terminating proxy) could replay a body.
What it could mean for you
No client-side defect; replays can only inflate data if the server does not deduplicate.
The reviewer’s recommendation
Include the endpoint in the signed material and document the server's sentAt window and sampleId deduplication.
Affects
Mac, Windows and Linux

Our response Accepted

The server accepts a request only within five minutes of its sentAt and deduplicates samples by installation and sample ID.

Run it yourself

  1. Get the code at the reviewed commit.

    git clone https://github.com/mattivilola/tokrate-clients
    cd tokrate-clients
    git checkout cf4b743319f665c04da574f674f7f9de8c15405b
  2. Open that folder in your coding agent in read-only mode: it may read files and run read-only commands, but not edit anything.

  3. Paste the prompt below. It asks for one JSON object in the same format as the published files.

  4. Compare the result with the published files. Models, and runs of the same model, word and weigh things differently, so check the code locations an answer cites rather than the verdict alone.

The prompt

The reviewers of this round received exactly this text. It begins with two git checks that stop the review if the code differs from the commit. The same prompt is in the repository: docs/security-review/PROMPT.md.


You are an independent security and privacy reviewer. Review the source code of the Tokrate desktop clients and decide whether the code keeps the privacy and security promises listed below. You are not the author and you have no reason to be generous: report what the code actually does.

## Target

- Repository: https://github.com/mattivilola/tokrate-clients
- Ref: Tokrate 0.1.20 (Mac `v0.1.20` and Windows/Linux `v0.1.20-desktop-alpha.1`, both from this commit)
- Commit: `cf4b743319f665c04da574f674f7f9de8c15405b`

Before starting, run these in the working directory:

```sh
git cat-file -e cf4b743319f665c04da574f674f7f9de8c15405b^{commit}
git diff --quiet cf4b743319f665c04da574f674f7f9de8c15405b -- . ':(exclude)*.md'
```

The first confirms the commit exists; the second confirms that every tracked non-Markdown file in the working directory, including uncommitted changes, is identical to the target commit. If either exits non-zero, stop and return only `{"error": "code does not match target", "head": "<output of git rev-parse HEAD>"}`.

Only files tracked at the target commit are in scope (`git ls-tree -r --name-only cf4b743319f665c04da574f674f7f9de8c15405b`). Ignore untracked and gitignored files, including build output and installed dependencies (`.build/`, `desktop/**/target/`, `node_modules/`, `dist/`, `.local/`); review dependencies through the lock files instead. For Markdown files, read the version at the target commit (`git show cf4b743319f665c04da574f674f7f9de8c15405b:README.md`).

## What Tokrate is

Tokrate is a menu-bar / tray app that reads the local session logs of AI coding tools (Codex, Claude Code, Grok Build, Antigravity, OpenCode), measures how fast the model answered, and, only if the user opts in, uploads the numbers (model, token counts, timings) to `tokrate.dev` for a public speed board.

Two implementations are in scope. Check every claim against both:

- **macos**: the Swift app. `shared/` (core, parsers, sharing, CLI `tokrate`) and `macos/` (SwiftUI app, packaging scripts).
- **windowsLinux**: the Tauri app. `desktop/core` (Rust core), `desktop/src-tauri` (native host), `desktop/ui` and `desktop/*` frontend and build config.

Also in scope: `Package.swift`, `Package.resolved`, `desktop/**/Cargo.toml`, `desktop/**/Cargo.lock`, `desktop/package*.json`, `.github/workflows/`, the updater public keys, and packaging/signing scripts (look for secrets or unsafe build steps).

Out of scope: the tokrate.dev backend, which is not in this repository. Server-side promises (retention, IP handling, continent derivation) cannot be checked here; do not mark them as failures.

## Rules

1. **Code is the only evidence.** README, `docs/`, comments, identifiers and test names describe intent; they are not proof. Use them to find what to check, then confirm in the code that runs. Tests may support a verdict but never replace reading the implementation.
2. **Trace data end to end.** For uploads, follow a value from the file or database it is read from, through parsing, storage and serialization, to the network call. For each claim, look for any code path that breaks it, not only the main path.
3. **Find every network call, file write and log call yourself.** Search for networking APIs (for example `URLSession`, `URLRequest`, `reqwest`, `ureq`, `hyper`, `fetch`, `XMLHttpRequest`, Tauri HTTP/updater plugins, Sparkle), file writes, process launches, and logging (`print`, `NSLog`, `os_log`, `Logger`, `println!`, `eprintln!`, `log::`, `tracing::`, `console.*`).
4. **Check dependencies.** Read `Package.resolved`, `Cargo.lock` files and `desktop/package-lock.json` for analytics, crash-reporting, advertising or telemetry packages, and check what any network-capable dependency is used for.
   Third-party dependency source code (Sparkle, Tauri and its updater plugin, reqwest, SQLite, notify and so on) is not in this repository. Judge how the app configures and calls a dependency, and take a well-established dependency's documented behaviour as given (for example that Sparkle verifies the EdDSA signature against `SUPublicEDKey` before installing, or that the Tauri updater verifies the minisign signature against the configured `pubkey`). Do not mark a claim `NOT_VERIFIABLE` only because that code is not in the repository; list the dependency behaviour you relied on in `scope.limitations`. A claim is `NOT_VERIFIABLE` only when this repository's own code or configuration leaves the outcome open.
5. **Cite exact locations.** Every evidence item needs a repository-relative path and the line numbers at the target commit. Do not cite files you did not open. Never invent a path or line.
6. **No speculation as findings.** A finding needs a concrete code path. Hardening ideas without a current defect are `info`.
7. **Read-only.** Do not modify, build-install, or run the apps. Running the test suites (`swift test`, `cargo test`) is allowed but optional. Do not contact tokrate.dev or any other service, and do not use web search.
8. **Be complete before concluding.** If you could not examine something, list it in `scope.notExamined`; do not guess its verdict.

## Claims to check

Check each claim, use its exact ID, and keep the order.

- **C01 Upload allowlist.** Each community upload is built from an explicit field allowlist and contains only the fields documented in `README.md` (section "Optional community sharing") and `docs/metrics-contract.md`. It never contains prompts, responses, code, file or folder paths, session/turn/account identifiers, the local deduplication digest, raw originator/entrypoint strings, user names, host names or hardware identifiers. List every key actually sent in `uploadedFields`, and report any key the documentation does not mention.
- **C02 Content is not retained.** Parsers keep only numeric usage, timestamps, model/provider identifiers, reasoning effort and the documented category fields. Prompt and response text, tool output, code and paths are not stored beyond parsing. Sources the documentation says are never read are really never opened or queried: Grok `chat_history.jsonl` and `updates.jsonl`; the OpenCode `part` table and full `message.data`; Antigravity `step_payload`, `trajectory_metadata_blob`, `render_info`, `task_details`, `permissions`, `error_details`, `battle_mode_infos`.
- **C03 Minimal local storage.** Local history stores only normalized metric records and checkpoints (with SHA-256 path digests, never raw paths or raw session/turn identifiers). Records are kept for 7 days and at most 50,000 turns while the app runs; expired records are removed within about an hour, also while monitoring is paused (nothing runs while the app is not running). Live per-response values are memory only.
- **C04 Read-only access to coding-tool data.** The app never writes, modifies, deletes or renames other tools' session files or databases, and never takes an exclusive lock on them. SQLite databases are opened read-only (`mode=ro`, never `immutable=1`); SQLite's normal shared lock for the duration of a short read transaction is expected and does not break this claim.
- **C05 Affirmative consent.** No sample is uploaded and no community request is made until the user explicitly opts in on the current notice version. "Only for local use" and a saved OFF stay OFF across launches and upgrades. Raising the notice version requires a new opt-in.
- **C06 Consent screen is accurate.** The sample payload and field description the consent screen shows match the structure and fields that are actually uploaded.
- **C07 Sharing OFF stops network activity.** Switching sharing off cancels the upload loop, clears pending uploads and stops community statistics/alert fetches. While sharing is off, no request is made to the community endpoints.
- **C08 No backfill.** Only turns completed after the current launch or the latest switch-on are eligible for upload; historical turns are never uploaded.
- **C09 Pseudonymous identity.** The installation identity is a random Ed25519 key stored in the OS credential store (macOS Keychain, Windows Credential Manager, Linux Secret Service). Only the public key and signatures are sent; the private key never leaves the store or the process. No user name, host name, hardware/device identifier, MAC address or serial number is collected or sent.
- **C10 Known network destinations only.** All network requests go to `tokrate.dev` (sample upload, community statistics, update feeds) or GitHub (update packages), over HTTPS. Update packages are accepted only from this repository's GitHub release assets; update requests may follow ordinary HTTP redirects (GitHub serves release assets from its own download hosts), which is documented and expected, provided packages are installed only after signature verification. Sharing requests reject redirects and use no cookies or persistent cache. No other host is contacted, and local-only features (history export, `tokrate inspect`) make no network requests. List every request in `networkEndpoints`.
- **C11 No tracking SDKs.** No analytics, advertising, crash-reporting or telemetry SDK is included in any dependency set, and no separate install, launch or usage event is sent.
- **C12 Update integrity and privacy.** Updates are verified against public keys embedded in the app before installation (Sparkle EdDSA on Mac, Tauri updater minisign on Windows/Linux), and installation requires a user action. Update checks send no contribution key, measurements or coding-tool content. Sparkle system-profile reporting is disabled. The automatic-check switch is respected.
- **C13 Bounded memory-only queue.** Unsent uploads are held only in memory, capped at 1,000 samples and 24 hours, and are lost on quit. Nothing pending is written to disk.
- **C14 No sensitive logging.** No prompts, responses, paths, session identifiers, keys or signatures are written to logs, the console, the OS log or crash output.
- **C15 No remote code or content.** The app UI loads no remote web content and executes no downloaded code other than signed updates. On Windows/Linux the Tauri content security policy and capabilities limit the frontend to what it needs, and the IPC commands it exposes cannot read arbitrary files or reach arbitrary network destinations (fixed-purpose commands such as an update check or switching sharing on, which reach only the endpoints of C10, are expected).
- **C16 Upload timing.** A sample is uploaded only after its five-minute `observedAt` period has ended, after a random delay, so neither the request's `sentAt` nor its sending time places a turn more precisely than its five-minute period. Community statistics fetches do not reveal when turns completed.

## Threat model

Use this to judge who can trigger a defect, and set severity accordingly:

- **The tokrate.dev service and anyone on the network path** see every request. What can they learn about the user beyond the documented fields? Could they make the app do something harmful (responses, update feeds)?
- **Content inside the coding tools' logs and databases** (prompt, response and tool-output text) can be influenced by third parties, for example a malicious repository, web page or model output. A defect triggerable by such content is realistic.
- **File structure** (file types such as FIFOs or symlinks, numeric fields, timestamps, JSON shape, file sizes, paths) is written by the coding tools themselves. Crafting it requires write access to the user's home folder; such an attacker can already do far more than disturb Tokrate.
- **Other local users** on the same machine, through file permissions.

## General security review

Beyond the claims, report concrete defects in any area, for example:

- Handling of untrusted input: the session files and databases Tokrate reads are written by other programs and could be crafted. Look for crashes, unbounded memory or CPU use, path traversal, symlink following outside source roots, SQL injection, and unsafe deserialization.
- Request signing and replay: signature construction, timestamp handling, key generation randomness.
- Local file permissions and locations of the history file and any other written file.
- Tauri IPC surface, webview configuration, CSP gaps.
- Secrets, private keys or credentials committed to the repository; unsafe CI or packaging steps.
- Places where the documentation promises something the code does not do (severity by user impact).

## Severity

- `critical`: prompts, responses, code, paths or the private key can leave the device, or remote code execution is possible.
- `high`: a claim is contradicted in normal use; data leaves without consent; update verification can be bypassed.
- `medium`: a claim fails only in an edge case or on one platform; a defect exploitable by the service, a network observer, another local user, or content inside the coding tools' logs; documentation materially misstates what is sent or stored.
- `low`: a defense-in-depth gap with limited impact; a crash, hang or resource exhaustion that needs crafted file structure (write access to the user's home folder, see Threat model); a minor documentation inaccuracy.
- `info`: an observation or hardening suggestion with no current defect.

## Verdicts

Per claim and per implementation:

- `VERIFIED`: the code implements the claim on every path you traced, with evidence cited.
- `PARTIAL`: the claim holds in general but has a gap (an edge case, one path, a documentation mismatch). Must reference at least one finding.
- `CONTRADICTED`: the code does what the claim says it does not do. Must reference a finding of severity `high` or `critical`.
- `NOT_VERIFIABLE`: the source cannot settle it; explain why in `rationale`.
- `NOT_APPLICABLE`: the claim does not apply to this implementation (explain why).

The claim's overall `verdict` is the worse of its two implementation verdicts, in this order from worst: `CONTRADICTED`, `PARTIAL`, `NOT_VERIFIABLE`, `VERIFIED`. `NOT_APPLICABLE` is ignored unless both are `NOT_APPLICABLE`.

`summary.overallVerdict`:

- `FAIL` if any claim is `CONTRADICTED` or any finding is `critical` or `high`;
- otherwise `PASS_WITH_NOTES` if any claim is `PARTIAL` or `NOT_VERIFIABLE`, or any finding is `medium`;
- otherwise `PASS`.

## Output format

Your final answer must be exactly one JSON object that matches the schema below: no Markdown fences, no text before or after it. Use only the enum values given. Keep string lengths within the stated limits. Use `[]` for empty lists and `null` for unknown optional values. Number findings `F01`, `F02`, … from most to least severe.

```jsonc
{
  "schemaVersion": "tokrate-client-review/1",
  "reviewer": {
    "model": "string — exact model name and version you are",
    "tool": "string — agent or app you ran in, e.g. Codex CLI, Claude Code, Grok",
    "reviewedAt": "YYYY-MM-DD"
  },
  "target": {
    "repository": "https://github.com/mattivilola/tokrate-clients",
    "ref": "v0.1.20",
    "commit": "the 40-character target commit SHA you verified"
  },
  "scope": {
    "filesExamined": 0,                    // integer: files you actually opened
    "testsRun": "none | swift | cargo | swift+cargo",
    "notExamined": ["string ≤ 160 chars"],
    "limitations": ["string ≤ 200 chars"]
  },
  "summary": {
    "overallVerdict": "PASS | PASS_WITH_NOTES | FAIL",
    "headline": "string ≤ 160 chars — one plain-language sentence for a website visitor",
    "claimCounts": { "VERIFIED": 0, "PARTIAL": 0, "CONTRADICTED": 0, "NOT_VERIFIABLE": 0, "NOT_APPLICABLE": 0 },
    "findingCounts": { "critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0 }
  },
  "claims": [                              // exactly 16 entries, C01 … C16 in order
    {
      "id": "C01",
      "verdict": "VERIFIED | PARTIAL | CONTRADICTED | NOT_VERIFIABLE | NOT_APPLICABLE",
      "byImplementation": {
        "macos": "VERIFIED | PARTIAL | CONTRADICTED | NOT_VERIFIABLE | NOT_APPLICABLE",
        "windowsLinux": "VERIFIED | PARTIAL | CONTRADICTED | NOT_VERIFIABLE | NOT_APPLICABLE"
      },
      "confidence": "high | medium | low",
      "rationale": "string ≤ 600 chars — what you traced and why the verdict follows",
      "evidence": [ { "file": "repo/relative/path", "lines": "120-148", "note": "string ≤ 160 chars" } ],
      "findings": ["F01"]
    }
  ],
  "uploadedFields": [                      // every JSON key in an upload request body, both implementations
    {
      "field": "string — JSON key as sent",
      "type": "string | integer | number | boolean | null-able variants, e.g. integer|null",
      "valueSource": "string ≤ 160 chars — where the value comes from",
      "implementations": ["macos", "windowsLinux"],
      "documented": true,
      "containsUserContent": false
    }
  ],
  "networkEndpoints": [                    // every outbound request either app can make
    {
      "url": "string — scheme, host and path (use {placeholder} for variable parts)",
      "method": "GET | POST | PUT | other",
      "purpose": "string ≤ 120 chars",
      "when": "string ≤ 160 chars — what triggers it and whether sharing/update switches gate it",
      "sends": "string ≤ 200 chars — headers and body contents that could identify the user or install",
      "implementations": ["macos", "windowsLinux"],
      "evidence": [ { "file": "repo/relative/path", "lines": "10-42", "note": "string ≤ 160 chars" } ]
    }
  ],
  "findings": [
    {
      "id": "F01",
      "severity": "critical | high | medium | low | info",
      "category": "privacy-leak | consent | network | local-storage | input-handling | resource-exhaustion | crypto | update-integrity | supply-chain | ipc-webview | logging | secrets | documentation | other",
      "title": "string ≤ 100 chars",
      "description": "string ≤ 800 chars — the defect and the concrete code path",
      "impact": "string ≤ 300 chars — what can actually happen to a user",
      "recommendation": "string ≤ 400 chars",
      "implementations": ["macos", "windowsLinux"],
      "claims": ["C05"],
      "evidence": [ { "file": "repo/relative/path", "lines": "55-61", "note": "string ≤ 160 chars" } ]
    }
  ]
}
```

The reviewers’ raw results

The two JSON files exactly as the reviewers returned them.

What this AI audit does not cover

  • An AI audit is not a professional audit. AI reviewers can miss problems and can be wrong. This is not a certification and not a professional security audit. It is two independent readings of the same code, published so you can judge the results yourself.
  • The server is not in the code they read. The tokrate.dev backend is not part of the open-source repository, so nobody checked what the server does with an upload. See the privacy policy for what it keeps and for how long.
  • They read source code, not the apps you download. The link between the two is the release itself: every release publishes SHA-256 checksums, the Mac app is signed with an Apple Developer ID and notarized by Apple, and in-app updates are signature-verified. The download page has the details.
  • Dependencies were taken as documented. Sparkle, Tauri, SQLite and the other libraries the apps use were not reviewed line by line, only how the apps use them and what their lock files list.
  • Each reviewer lists what it did not read. The scope and limits sections in the raw files name the files and areas a reviewer only sampled.