---
title: "Anonymous telemetry | SEO Skill"
description: "Every anonymous usage event SEO sends, everything it never collects, and every way to disable telemetry."
canonical: "https://seoskill.dev/telemetry"
language: "en"
---

# Anonymous telemetry

SEO sends small anonymous usage events so the project can count installs, completed setup and audits, retention, agent usage, and report popularity.

Your SEO data stays on your machine. Telemetry never contains an identifier that could link events to a person or machine.

See the same aggregate data as the maintainer on the [public usage stats page](https://seoskill.dev/stats).

## First run notice

This notice is shown before the first event is sent.

The notice shown before anonymous telemetry starts.

```
SEO Skill collects anonymous usage data to improve the tool (event name,
report id, version, agent, OS, architecture, Node major, and install week;
never URLs, report data, or identifiers of any kind).
Docs: https://seoskill.dev/telemetry
Disable: seo telemetry disable (or set DO_NOT_TRACK=1)
```

## Every event

| Event name | When it is sent | Frequency |
| --- | --- | --- |
| `first_run` | The first invocation that creates local telemetry state. | Once per local install. |
| `setup_complete` | A non-dry-run guided setup finishes successfully. | Once per local install. |
| `audit_start` | A recognised report starts through the CLI or MCP server. | Every report run. |
| `audit_complete` | A recognised report finishes successfully. | Every successful report run. |
| `audit_failed` | A recognised report fails. It includes only a fixed `errorCategory`. | Every failed report run. |
| `first_audit_complete` | The first recognised report finishes successfully. | Once per local install. |
| `active_d1` | An invocation happens at least one day after the first run. | Once per local install. |
| `active_d7` | An invocation happens at least seven days after the first run. | Once per local install. |
| `active_d30` | An invocation happens at least 30 days after the first run. | Once per local install. |

## Every field sent

| Field | Value |
| --- | --- |
| `event` | One event name from the table above. |
| `version` | The installed `seo` package version, such as `0.2.5`. |
| `agent` | `claude-code` | `cursor` | `codex` | `cli` | `unknown`. MCP uses the client name from its standard initialize handshake, then discards the original name. |
| `os` | `aix` | `android` | `darwin` | `freebsd` | `haiku` | `linux` | `openbsd` | `sunos` | `win32`. |
| `arch` | `arm` | `arm64` | `ia32` | `loong64` | `mips` | `mipsel` | `ppc` | `ppc64` | `riscv64` | `s390` | `s390x` | `x64`. |
| `node` | The Node.js major version as a string, such as `22`. |
| `cohort` | The ISO week of the first run, such as `2026-W29`. The exact first-run time never leaves local state. |
| `schema` | The integer wire schema version. The current value is `1`. |
| `errorCategory` | Sent only with `audit_failed`. One of `auth` | `crawl_timeout` | `network` | `config` | `unknown`. Raw errors are never sent. |
| `report` | Sent only with report lifecycle events. It is one fixed public report identifier from the list below, never a URL or user-supplied string. |

A successful report completion event.

```
{
  "event": "audit_complete",
  "version": "0.2.5",
  "agent": "codex",
  "os": "darwin",
  "arch": "arm64",
  "node": "22",
  "cohort": "2026-W29",
  "schema": 1,
  "report": "site-crawl"
}
```

## Never collected or transmitted

-   User IDs, machine IDs, UUIDs, advertising IDs, or fingerprints of any kind.
-   Audited domains, URLs, hostnames, page content, search queries, or crawl results.
-   Report findings, recommendations, scores, exports, or other report content.
-   Google Search Console or Google Analytics responses, property IDs, account data, or metrics.
-   API keys, OAuth tokens, secrets, file paths, usernames, local hostnames, or raw error messages.
-   IP addresses in D1, Worker logs, application storage, or the public stats response.
-   Country, region, city, coordinates, or any other location data.

## How to disable it

The local command is the simplest permanent setting. Environment settings can disable every telemetry network call without changing the saved preference.

Change or check the saved telemetry setting.

```
seo telemetry disable
seo telemetry status
seo telemetry enable
```

Disable telemetry for the current environment.

```
DO_NOT_TRACK=1
SEOSKILL_TELEMETRY_DISABLED=1
SEO_TELEMETRY_DISABLED=1
```

Telemetry is also disabled when `CI=true` or a common CI environment variable is present, including GitHub Actions, GitLab CI, CircleCI, Travis CI, Buildkite, Jenkins, TeamCity, Azure Pipelines, Bitbucket Pipelines, and AWS CodeBuild. An environment override wins over `seo telemetry enable`.

## Local state

The memory needed for once-only events stays in `telemetry.json` inside the existing SEO config directory. Run `seo telemetry status --json` to see the exact path on your platform. The file contains only these fields.

The complete local telemetry state file.

```
{
  "telemetryEnabled": true,
  "firstRunAt": "2026-07-16T09:12:00.000Z",
  "cohort": "2026-W29",
  "sentMilestones": ["first_run", "first_audit_complete", "active_d1"]
}
```

`firstRunAt` is used only on your machine to decide when a retention milestone is due. Only its week-level `cohort` is sent. Clearing all local SEO state means a later run is counted as a new install because the server has no identity to recognise it.

## Transport and storage

Each event is one JSON `POST` to `https://seoskill.dev/api/t`. The sender has a two-second timeout, does not batch or retry, silently drops failures, and never changes command output or exit status.

The Cloudflare Worker accepts only the fields and fixed values documented above. Unknown fields or values are rejected. It writes those values to a dedicated D1 table through a Worker binding. The server adds only the UTC receipt month, such as `2026-07`, for monthly totals. It does not store the exact request time. There is no account or identity column, and no telemetry KV, R2, or Analytics Engine dataset. Worker invocation logs are disabled, and the code never reads`request.cf`, IP headers, or location fields. Cloudflare handles the ordinary network request at the edge, but no IP address or geography is written to D1.

D1 keeps the accepted anonymous events until they are deliberately deleted. Public totals cover all events recorded since collection began. Only aggregate counts are returned by the public stats endpoint.

## Read the source

-   [Client state, checks, agent detection, and sender](https://github.com/iannuttall/seo/blob/main/packages/core/src/telemetry.ts)
-   [Strict ingest validation, D1 write, and aggregate query](https://github.com/iannuttall/seo/blob/main/apps/web/worker/index.ts)
-   [Exact D1 columns and database constraints](https://github.com/iannuttall/seo/blob/main/apps/web/migrations/0001_create_telemetry_events.sql)
-   [Public aggregate usage stats](https://seoskill.dev/stats)
