loki-mode

Loki Mode Privacy and Telemetry

This document is an honest, complete disclosure of what Loki Mode collects, what it never collects, and how to turn collection off. If anything here does not match the code, the code is the bug; please open an issue.

Summary

Two collection paths exist

1. Crash reporting (Phase 0: local-only, no network)

When Loki Mode hits an unexpected error (an uncaught exception, an unhandled promise rejection, a nonzero process exit, or an explicit friction signal such as a retry loop, a rate-limit loop, or a quality-gate failure), it captures a scrubbed diagnostic report.

Phase 0 behavior:

2. Usage telemetry (anonymous, on by default for individuals)

Loki Mode sends anonymous usage telemetry via PostHog. It is ON by default for an ordinary individual install, and AUTO-OFF in enterprise, CI, air-gapped, and non-interactive contexts (see the precedence below). It is gated by the same switch as crash reporting, so a single opt-out (loki telemetry off / DO_NOT_TRACK=1) disables everything, and it never sends your code, prompts, paths, keys, or repo names – only anonymous diagnostics.

3. Welcome page form (anonymous, explicit submit, opt-in)

The loki welcome page (assets/welcome/welcome.html) shows an optional form. It NEVER sends anything on page load and is rendered inert unless you have opted in. If you have opted in AND you choose to fill in and submit the form, it sends these additional self-reported fields to PostHog: your role, company size, and the tools you use, plus the same anonymous distinct_id. It still never sends your name, email, or IP. In headless / Docker / CI environments there is no browser, so this path never runs.

4. Build-outcome analytics (anonymous, STRICT opt-in, default OFF)

A build_verified event that reports how a build turned out. Unlike crash reporting and usage telemetry (which default ON for individuals), this path is OFF by default even for users who have telemetry enabled – it requires a SECOND, explicit opt-in:

It fires once when a build finishes, reading a FIXED allowlist of already- computed scalars from the run’s Evidence Receipt (proof.json). The allowlist (autonomy/lib/proof-analytics-props.py) is the single source of truth; any field not named there is never read, so a schema change cannot silently start sending something new. Fields sent:

It NEVER sends your spec/PRD text, file paths or names, code, or diffs. Because it sits below the base telemetry gate, every telemetry opt-out (loki telemetry off / LOKI_TELEMETRY=off / DO_NOT_TRACK=1) also disables it – opt-out always wins.

5. First-run blocker (anonymous, STRICT opt-in, default OFF)

A first_run_blocked event naming which CLASS of dependency stopped a first run, sent at most ONCE per install. It sits behind the same strict second opt-in as build-outcome analytics above (LOKI_ANALYTICS=on), so it is off by default even with telemetry enabled.

It exists because we could see that a first run was ATTEMPTED and nothing about whether it succeeded, which made “why does a trial not convert” unanswerable. A real example we found and fixed: on a machine with no AI provider CLI, one route ended with a dead end instead of pointing at loki tour (which needs no provider, no key and no spend). Nobody could see that happening.

The ONLY field is blocker, clamped to this fixed enum:

no_provider | node | python3 | jq | git | curl | disk | skill_symlink | other

Anything not on that list becomes other. It is deliberately coarse: node is enough to act on, and a version string or an install path would be a leak. It NEVER sends paths, versions, hostnames, spec text, or command lines – a test (tests/test-first-run-blocked-signal.sh) feeds a filesystem path through the real emitter and fails the build if anything but other reaches the payload.

This document and the first-run notice describe ALL paths. The model is unified: opt-out always wins and disables everything; crash reporting and usage telemetry opt in together (default ON for individuals); build-outcome analytics and the first-run blocker each need an explicit second opt-in on top (default OFF).

What is collected (the whitelist)

Crash reports contain ONLY the following fields. Anything not on this list is dropped, not merely redacted:

What is NEVER collected

Because the report is whitelist-only (deny by default), free-text fields such as prompts, briefs, and diffs can never reach the payload even if a redaction rule were to miss something. Secrets are additionally scrubbed by the shared redactor before whitelisting.

How to turn it off (and force it on)

Anonymous diagnostics are ON by default for an ordinary individual install. To turn them off at any time, use ANY one of the following. Opt-out always wins, so setting one of these guarantees nothing is collected or sent:

To FORCE it on (for example to send us diagnostics from a context that would otherwise auto-disable, like CI), use ANY one of:

Precedence (exact)

  1. If any opt-out flag is set, collection is OFF (hard kill, always wins).
  2. Else if an explicit opt-in (loki telemetry on / LOKI_TELEMETRY=on / TELEMETRY_ENABLED=true) is set, collection is ON (this overrides the enterprise/CI/air-gapped auto-off below).
  3. Else if the context is enterprise / CI / air-gapped / non-interactive, collection is OFF (auto-off, safe out of the box). Detected via: CI / GITHUB_ACTIONS / GITLAB_CI / BUILDKITE / JENKINS_URL / TEAMCITY_VERSION / CONTINUOUS_INTEGRATION, or LOKI_ENTERPRISE=true / LOKI_AIRGAP=true, or no TTY.
  4. Otherwise (an ordinary individual install), collection is ON.

In all enabled cases, if curl is unavailable there is no egress.

Air-gapped and enterprise deployments

A default install in an air-gapped, CI, or non-interactive environment sends us no telemetry or diagnostics: collection AUTO-DISABLES there (step 3 above), so there is nothing to turn off. To make collection impossible across a fleet regardless of context, bake LOKI_TELEMETRY_DISABLED=true (or DO_NOT_TRACK=1) into your base image or CI environment; opt-out always wins over everything, including a stray loki telemetry on.

This same gate covers ALL paths: the npm install event, CLI session and command events, the dashboard event, the welcome form, and local crash capture.

OpenTelemetry (separate, self-hosted)

loki telemetry enable [endpoint] and LOKI_OTEL_ENDPOINT configure optional OpenTelemetry tracing to an endpoint YOU run. There is no default endpoint, so this never egresses to us; it is opt-in by definition and points only where you tell it to.

Where reports are stored locally

Scrubbed crash reports live in .loki/crash/ inside your project directory. You can open these files in any text editor or use loki crash show <id>. In Phase 0 this directory is the only place crash data exists; it is yours to read or delete at any time.

The unsalted project-id tradeoff (plain language)

The project_id_hash is a SHA-256 hash of your git remote origin URL, after normalizing it (scheme removed, .git suffix removed, trailing slash removed, host lowercased). It does NOT hash your local filesystem path, so it carries no /Users/<name>/ style information.

The hash is unsalted on purpose. An unsalted hash lets two users who hit the same bug in the same public repository collapse to a single triage entry, which is the entire point of deduplication and occurrence counting. A per-user salt would defeat that. The cost of leaving it unsalted is that, for a known PUBLIC repository, someone could hash candidate repo URLs and check for a match. But the only thing that would reveal is which public repository was involved, which is already public information, so the privacy cost is acceptable. For a PRIVATE repository, the origin still hashes to an opaque value that leaks no path or name. We chose cross-user dedup over per-user unlinkability, and we are stating that choice plainly so you can decide whether to opt out.

Compliance posture

Questions

Open an issue at https://github.com/asklokesh/loki-mode/issues and we will clarify or correct this document.