Skip to main content

Telemetry

Claude-mem includes anonymous usage analytics (via PostHog) to help prioritize fixes and features. It is on by default (opt-out). Events are anonymous, identified only by a random install UUID, and every analytics property passes a strict whitelist — see What is collected and What is NEVER collected below. Turning it off is one command:
The standard DO_NOT_TRACK environment variable is also honored and overrides everything. The installer asks once at the end of npx claude-mem install so the default is never silent for new installs — your answer (either way) is remembered and never re-asked, and the prompt is skipped entirely when DO_NOT_TRACK is set or in CI/non-interactive installs.

How instrumentation works

Claude-mem has a single instrumentation path (instrument() in src/services/telemetry/instrument.ts). Every observable event is described once and fans out to two sinks:
  • The local logger — always, at full fidelity. Logging keeps working with telemetry off, and the local log never goes through the scrubber. This is where the complete, unredacted detail lives — on your machine.
  • Telemetry — only when consent passes. The telemetry copy is scrubbed (the whitelist for structured properties; allow-then-redact for error text) and, for high-volume events, rolled up into per-session/per-window aggregates before anything is sent.
So a single source of truth produces both the rich local log and the minimal, privacy-preserving telemetry — they never drift, and the scrubbing only ever happens on the telemetry branch. Note on session replay: PostHog session replay is not applicable to claude-mem. Replay records a browser DOM session; claude-mem is a Node background worker with no browser surface, so there is nothing to replay and it is never enabled.

What is collected

When enabled, events are anonymous and identified only by a random install UUID (crypto.randomUUID(), generated locally on first use). Low-volume lifecycle events (install_*, uninstall_completed, worker_started) build an analytics profile keyed to that random UUID so aggregate retention and cohort statistics are computable — the profile contains nothing beyond the whitelisted fields below (platform, version, IDE/provider choice). It is not, and cannot be, connected to you: there is no name, email, IP, hardware ID, or any other identifier. All high-volume activity is sent with $process_person_profile: false and builds no profile at all. About the observed IDE session’s identity, claude-mem collects only its model id (observed_model) and a billing tier (observed_billing) — never account ids, emails, or tokens. High-volume events are rolled up, not streamed. Rather than emit one event per compression or context injection, claude-mem aggregates them locally and sends one summary:
  • observer_turn_rollup — a per-session accumulator. Every compression in a session folds into one running rollup that is emitted once, at session end (instead of one session_compressed event per turn). It carries a rollup_reason explaining why it flushed (session_end | worker_shutdown | safety_flush) and a window_seq partial-flush counter (0 for a normal one-shot session; 0,1,2,… only when a long-lived session trips the periodic safety sweep).
  • context_injected_rollup — a 5-minute time-window accumulator for context injections.
This rollup model is what cut the high-volume event stream by roughly 99.9%. There is no longer any code path that emits raw session_compressed or context_injected events directly — the only path to PostHog for that activity is the rollup. Every event property passes through a strict whitelist scrubber — any key not in this table is silently dropped before sending: One value is derived server-side rather than sent by the client: PostHog resolves the request’s sender IP to a coarse location (country / region / city) at ingestion, before the IP itself is discarded. The client never attaches an IP to any event, and the raw IP is never stored — see What is NEVER collected.

Events

Work-state usage and diagnostic limits

Work-state rollups contain counts only. They never include task titles, fields, prompts, project names, paths, raw session or agent IDs, or inferred models. platform is a fixed supported-host name, other, or unknown; agent_role is main, subagent, or unknown (using the same subagent predicate as capture). Both captured_ and mcp_ read/write counters also have outcome counters ending in _ok_count, _error_count, _excluded_count, _disabled_count, or _unknown_count. Only an explicit saved receipt establishes a successful write. The two sources can observe the same call: never add them as distinct usage. instruction_state describes the last known hook handoff preceding the event in that exact local session/agent scope. Delayed activity older than the latest handoff has instruction_state=unknown; later instructions are never applied retroactively. Captured prompts and tools without original timestamps are omitted. Before a handoff is observed, it is unknown. Direct MCP calls, server-runtime-only capture, missing hooks, and an unavailable worker can leave coverage unknown. Live Pi, OpenCode, and OMP prompt callbacks carry their original timing; Pi uses the persisted user-message timestamp even on replay. Generic DSH client calls and OpenClaw synthetic init paths lack verified prompt timing and remain outside captured prompt/tool counts. OMP has its own bounded platform label; missing stable tool IDs contribute only unidentified-arrival diagnostics. Older adapters that already supplied a default host cannot be distinguished from hosts that explicitly reported it. Missing counters or events are not evidence of zero usage or zero adherence; an ordinary question may correctly require no write. No compliance percentage or task classification is computed. Local hashed receipt keys suppress duplicate prompts, tool replay, and repeated spool delivery across worker restarts for 30 days. Cleanup on activity, at most once an hour, removes older receipts; older events are dropped rather than counted again. A local, content-free public/private turn bit is maintained even while analytics is off, so opting back in during private work cannot reuse a previous public prompt. Only a strictly newer, timestamped public submission can reopen public coverage, including when duplicate text reuses a prompt row; private turns win timestamp ties. Neither the keys nor this bit are sent to PostHog. The worker uses the existing 30-second consent cache. Telemetry remains best effort: opt-out disables analytics, and a crash before a rollup flush can lose counts. This feature does not backfill past releases.

Error tracking

Claude-mem captures real errors to PostHog Error Tracking as $exception events. This is a deliberate change from the old strictly-whitelist-only posture: error messages and stack traces are free-form text, so the property whitelist (which only passes known closed-set keys) would drop them entirely. Instead, error text takes a separate allow-then-redact path (src/services/telemetry/error-scrub.ts): we keep the diagnostic text and aggressively strip anything that could leak PII or secrets. What is kept (redacted):
  • The error type (constructor name, e.g. TypeError), capped to 100 chars.
  • The error message, redacted and capped to 500 chars.
  • The stack trace — only the top 10 frames, each redacted, capped to ~2KB total.
  • An occurrence_count (how many times this error fingerprint fired in the current window).
What is redacted out of that text (replaced with [REDACTED], in this order):
  • Home directory (/Users/you → ~) — first, so a username embedded in the home path never survives.
  • Absolute filesystem paths → collapsed to basename (POSIX, Windows drive, and UNC paths) — keeps “which file” without the directory tree.
  • URL / connection-string credentials and query strings — userinfo (user:pass@) and ?…/#… are stripped from any scheme://… (http, ws, postgres, redis, mongodb+srv, amqp, …), so DB connection-string creds and signed-URL tokens die.
  • Emails.
  • API tokens and keys: provider-prefixed keys (sk-, phc_, ghp_, xoxb-, …), Bearer tokens, AWS access key IDs (AKIA…), JWTs, UUIDs, long hex blobs (24+ chars), and generic high-entropy tokens.
  • IPv4 addresses (internal IPs/hostnames that leak in network errors).
The redaction pipeline is pure and never throws — hostile input (null, circular, objects with throwing getters, 200KB blobs) always yields a safe, bounded result, because telemetry must never break or block the worker. Raw input is hard-capped at 8KB before any regex runs (ReDoS defense). Rate-limiting. At most one $exception per error fingerprint per 60 seconds. Errors are fingerprinted by type + a normalized message template + top stack frame, so a storm of the “same” error with varying ids/numbers dedupes to a single send with an occurrence count attached. This applies to both our manual captures and any SDK autocapture. (Autocapture is additionally re-scrubbed before send — raw source-context lines that posthog-node reads off disk are deleted, and filenames are redacted to basenames.) Consent-gated, with an independent kill-switch. Error capture is gated by the normal telemetry consent chain (opting out of telemetry disables errors too) and by a separate CLAUDE_MEM_TELEMETRY_ERRORS switch — see How to opt out. No person profile is built for $exception events ($process_person_profile: false).
One-way door. Unlike the whitelisted analytics events — every field of which is a number, boolean, or value from a closed set — $exception events carry real (redacted) message text. Once an error message is ingested into PostHog, it cannot be selectively deleted after the fact. This is a deliberate trade-off made to get actionable crash diagnostics, mitigated by aggressive redaction, rate-limiting, consent-gating, and the CLAUDE_MEM_TELEMETRY_ERRORS=0 kill-switch. If you would rather send nothing free-form, set that variable.

Historical backfill

Telemetry shipped later than claude-mem itself, so installs that predate it have activity the live events never saw. On the first worker start after upgrading, claude-mem performs a one-time backfill of that pre-telemetry history — anonymized counts only, passed through the same whitelist scrubber as everything else: Like everything else, these are counts and closed-set values only — never titles, prompts, file contents, or project names. The same anonymous install UUID identifies them, and every property passes the whitelist scrubber. A few things worth knowing:
  • It runs once. A completion marker (backfill.json in the claude-mem data directory) is written after a successful send and prevents the backfill from ever running again. Until a run succeeds, no marker is written, so a failed attempt simply retries on the next worker start.
  • It honors the exact same consent gates as live telemetry — DO_NOT_TRACK, CLAUDE_MEM_TELEMETRY=0, and enabled: false in telemetry.json all block it, and debug mode prints the would-be payload without sending.
  • Opting out before the first worker start after upgrading prevents it entirely. Nothing is sent and no marker is written while you are opted out — though if you opt back in later, the backfill will then run.
  • Location is upload-time, not historical. The coarse location PostHog derives at ingestion (see above) reflects where the events were uploaded from, not where you were on the historical dates they describe.

What is NEVER collected

One honest exception: error messages. Since the addition of error tracking, redacted error messages and stack traces ARE collected (as $exception events) — that is a deliberate change from the previous coarse-category-only posture, and it is consent-gated with its own kill-switch. Raw paths, prompts, project names, source code, and model output are still never collected — they are stripped from the error text before it leaves your machine. Analytics properties are enforced in code: they go through a whitelist (only the fields in the What is collected table survive), not a blocklist. Every whitelisted field is either a number, a boolean, or a value from a closed set we define — there is no analytics field that could carry free-form user content. Error text is the one free-form path, and it goes through the separate allow-then-redact scrubber instead.

How to opt out (four ways)

Any one of these keeps telemetry off — they are checked in this order, first match wins:
  1. DO_NOT_TRACK — the universal opt-out. Set DO_NOT_TRACK=1 and telemetry is forced off, overriding everything else.
  2. CLAUDE_MEM_TELEMETRY=0 (also false / off) — environment override. (CLAUDE_MEM_TELEMETRY=1 conversely forces it on.)
  3. Telemetry config file — enabled: false in telemetry.json (see below).
  4. CLI command:

Error tracking opt-out (independent)

Error tracking ($exception events with redacted message/stack) can be disabled on its own, without turning off anonymous analytics:
This is the one telemetry path that carries free-form (redacted) text and is a one-way door once ingested, so it has its own kill-switch for operators who are fine with anonymous counters but not error text. It defaults on whenever telemetry consent is on; any of the four opt-outs above also disables it implicitly (no consent ⇒ no errors). Check the current state — and which of the four layers decided it — anytime:

Debug mode

Want to see exactly what would be sent? Set:
With debug mode on (and telemetry enabled), every would-be event payload is printed to stderr and nothing is sent over the network.

Where the config lives

Consent and the anonymous install ID are stored in telemetry.json inside the claude-mem data directory:
  • Default: ~/.claude-mem/telemetry.json
  • Or $CLAUDE_MEM_DATA_DIR/telemetry.json if you’ve overridden the data dir
The enabled field is only present once you’ve made an explicit choice (installer prompt, telemetry enable, or telemetry disable). A file with just an installId means no decision was recorded and the default (on) applies. Delete the file to reset completely — a fresh install ID is generated on next use.