Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,16 +111,17 @@ After installing, skills and context are injected automatically. You can also in

## Telemetry

The plugin has two separate telemetry controls:
The plugin has one active telemetry control:

- `~/.claude/vercel-plugin-telemetry-preference` controls prompt text only.
- `VERCEL_PLUGIN_TELEMETRY=off` disables all telemetry.

Behavior:

- `echo 'enabled' > ~/.claude/vercel-plugin-telemetry-preference` keeps default base telemetry on and also allows prompt text telemetry.
- `echo 'disabled' > ~/.claude/vercel-plugin-telemetry-preference` keeps prompt text off, but base telemetry remains on by default.
- `VERCEL_PLUGIN_TELEMETRY=off` disables all telemetry, including prompt text, session metadata, tool names, and skill-injection telemetry.
- Prompt text telemetry is currently disabled in the plugin regardless of any preference file.
- By default, the plugin sends base telemetry only: session metadata, tool names, and skill-injection telemetry.
- Base telemetry includes `session:device_id`, a stable anonymous UUID stored at `~/.claude/vercel-plugin-device-id`.
- `session:device_id` is used to measure things like daily active users. It is not tied to a Vercel account, login, email address, prompt text, file contents, or bash commands.
- `VERCEL_PLUGIN_TELEMETRY=off` disables all telemetry, including `session:device_id`, session metadata, tool names, and skill-injection telemetry.

Where to set `VERCEL_PLUGIN_TELEMETRY=off`:

Expand All @@ -130,7 +131,6 @@ Where to set `VERCEL_PLUGIN_TELEMETRY=off`:
Examples:

```bash
echo 'disabled' > ~/.claude/vercel-plugin-telemetry-preference
export VERCEL_PLUGIN_TELEMETRY=off
```

Expand Down
23 changes: 2 additions & 21 deletions hooks/setup-telemetry.mjs
Original file line number Diff line number Diff line change
@@ -1,39 +1,20 @@
#!/usr/bin/env node

// hooks/src/setup-telemetry.mts
import { readFileSync } from "fs";
import { homedir } from "os";
import { join } from "path";
import { getTelemetryOverride } from "./telemetry.mjs";
var PREF_PATH = join(homedir(), ".claude", "vercel-plugin-telemetry-preference");
function main() {
const telemetryOverride = getTelemetryOverride();
if (telemetryOverride === "off") {
process.stdout.write(
"Telemetry is fully disabled via VERCEL_PLUGIN_TELEMETRY=off.\nRemove or change that env var before changing the prompt-sharing preference.\n"
);
process.exit(0);
}
let existing = null;
try {
existing = readFileSync(PREF_PATH, "utf-8").trim();
} catch {
}
if (existing === "enabled" || existing === "disabled" || existing === "asked") {
process.stdout.write(
`Prompt telemetry is currently ${existing}. This setting controls prompt text sharing only.
To change prompt sharing, edit ~/.claude/vercel-plugin-telemetry-preference. To disable all telemetry, set VERCEL_PLUGIN_TELEMETRY=off in the environment that launches your agent.
`
"Telemetry is fully disabled via VERCEL_PLUGIN_TELEMETRY=off.\nPrompt text telemetry is currently disabled in the plugin, so there is no separate prompt-sharing preference to change.\n"
);
process.exit(0);
}
process.stdout.write(
[
"The Vercel plugin collects anonymous usage data such as skill injection patterns and tools used by default.",
"Ask the user if they would also like to share their prompt text to help improve skill matching. This choice only controls prompt telemetry.",
"Prompt text telemetry is currently disabled in the plugin, so there is no consent prompt to show.",
"To disable all telemetry, set VERCEL_PLUGIN_TELEMETRY=off in the environment that launches your agent.",
"If they agree, run `echo 'enabled' > ~/.claude/vercel-plugin-telemetry-preference`.",
"If they decline, run `echo 'disabled' > ~/.claude/vercel-plugin-telemetry-preference`.",
""
].join("\n")
);
Expand Down
25 changes: 2 additions & 23 deletions hooks/src/setup-telemetry.mts
Original file line number Diff line number Diff line change
@@ -1,42 +1,21 @@
#!/usr/bin/env node

import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { homedir } from "node:os";
import { join, dirname } from "node:path";
import { getTelemetryOverride } from "./telemetry.mjs";

const PREF_PATH = join(homedir(), ".claude", "vercel-plugin-telemetry-preference");

function main(): void {
const telemetryOverride = getTelemetryOverride();
if (telemetryOverride === "off") {
process.stdout.write(
"Telemetry is fully disabled via VERCEL_PLUGIN_TELEMETRY=off.\nRemove or change that env var before changing the prompt-sharing preference.\n",
);
process.exit(0);
}

let existing: string | null = null;
try {
existing = readFileSync(PREF_PATH, "utf-8").trim();
} catch {
// File doesn't exist
}

if (existing === "enabled" || existing === "disabled" || existing === "asked") {
process.stdout.write(
`Prompt telemetry is currently ${existing}. This setting controls prompt text sharing only.\nTo change prompt sharing, edit ~/.claude/vercel-plugin-telemetry-preference. To disable all telemetry, set VERCEL_PLUGIN_TELEMETRY=off in the environment that launches your agent.\n`,
"Telemetry is fully disabled via VERCEL_PLUGIN_TELEMETRY=off.\nPrompt text telemetry is currently disabled in the plugin, so there is no separate prompt-sharing preference to change.\n",
);
process.exit(0);
}

process.stdout.write(
[
"The Vercel plugin collects anonymous usage data such as skill injection patterns and tools used by default.",
"Ask the user if they would also like to share their prompt text to help improve skill matching. This choice only controls prompt telemetry.",
"Prompt text telemetry is currently disabled in the plugin, so there is no consent prompt to show.",
"To disable all telemetry, set VERCEL_PLUGIN_TELEMETRY=off in the environment that launches your agent.",
"If they agree, run `echo 'enabled' > ~/.claude/vercel-plugin-telemetry-preference`.",
"If they decline, run `echo 'disabled' > ~/.claude/vercel-plugin-telemetry-preference`.",
"",
].join("\n"),
);
Expand Down
9 changes: 7 additions & 2 deletions hooks/src/telemetry.mts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const BRIDGE_ENDPOINT = "https://telemetry.vercel.com/api/vercel-plugin/v1/event
const FLUSH_TIMEOUT_MS = 3_000;

const DEVICE_ID_PATH = join(homedir(), ".claude", "vercel-plugin-device-id");
const DISABLED_CONTENT_KEYS = new Set(["prompt:text"]);

export interface TelemetryEvent {
id: string;
Expand Down Expand Up @@ -157,10 +158,11 @@ export async function trackBaseEvents(
}

// ---------------------------------------------------------------------------
// Opt-in telemetry (raw prompt content)
// Opt-in telemetry (raw prompt content, currently disabled)
// ---------------------------------------------------------------------------

export async function trackContentEvent(sessionId: string, key: string, value: string): Promise<void> {
if (DISABLED_CONTENT_KEYS.has(key)) return;
if (!isContentTelemetryEnabled()) return;

const event: TelemetryEvent = {
Expand All @@ -179,8 +181,11 @@ export async function trackContentEvents(
): Promise<void> {
if (!isContentTelemetryEnabled() || entries.length === 0) return;

const filteredEntries = entries.filter((entry) => !DISABLED_CONTENT_KEYS.has(entry.key));
if (filteredEntries.length === 0) return;

const now = Date.now();
const events: TelemetryEvent[] = entries.map((entry) => ({
const events: TelemetryEvent[] = filteredEntries.map((entry) => ({
id: randomUUID(),
event_time: now,
key: entry.key,
Expand Down
114 changes: 18 additions & 96 deletions hooks/src/user-prompt-submit-telemetry.mts
Original file line number Diff line number Diff line change
@@ -1,34 +1,15 @@
#!/usr/bin/env node
/**
* UserPromptSubmit hook: prompt telemetry opt-in + prompt text tracking.
* UserPromptSubmit hook: prompt telemetry is currently disabled.
*
* Fires on every user message. Two responsibilities:
*
* 1. Track prompt:text telemetry (awaited) for every prompt >= 10 chars
* when prompt telemetry is enabled. This runs independently of skill
* matching so prompts are never silently dropped.
*
* 2. On the first message of a session where the user hasn't recorded a
* prompt telemetry preference, return additionalContext asking the model
* to prompt the user for opt-in. Writes "asked" immediately so the user
* is never re-prompted. session-end-cleanup converts "asked" → "disabled".
*
* Note: Base telemetry is enabled by default, but users can disable all
* telemetry with VERCEL_PLUGIN_TELEMETRY=off. This hook only gates prompt
* text collection when telemetry is otherwise enabled.
* Prompt text collection is intentionally disabled regardless of preference.
* The hook remains in place as a no-op for compatibility with hooks.json.
*
* Input: JSON on stdin with { session_id, prompt }
* Output: JSON on stdout with { hookSpecificOutput: { hookEventName, additionalContext } } or {}
* Output: JSON on stdout with {}
*/

import type { SyncHookJSONOutput } from "@anthropic-ai/claude-agent-sdk";
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import { join, dirname } from "node:path";
import { getTelemetryOverride, isContentTelemetryEnabled, trackContentEvents } from "./telemetry.mjs";

const PREF_PATH = join(homedir(), ".claude", "vercel-plugin-telemetry-preference");
const MIN_PROMPT_LENGTH = 10;
import { readFileSync } from "node:fs";

function parseStdin(): Record<string, unknown> | null {
try {
Expand All @@ -52,78 +33,19 @@ async function main(): Promise<void> {
const input = parseStdin();
const sessionId = input ? resolveSessionId(input) : "";
const prompt = input ? resolvePrompt(input) : "";
const telemetryOverride = getTelemetryOverride();

// Prompt text tracking — opt-in only
if (isContentTelemetryEnabled() && sessionId && prompt.length >= MIN_PROMPT_LENGTH) {
await trackContentEvents(sessionId, [
{ key: "prompt:text", value: prompt },
]).catch(() => {});
}

// A global kill switch means the user has already disabled telemetry.
if (telemetryOverride === "off") {
process.stdout.write("{}");
process.exit(0);
}

// Already opted in, out, or asked — nothing more to do
try {
const pref = readFileSync(PREF_PATH, "utf-8").trim();
if (pref === "enabled" || pref === "disabled" || pref === "asked") {
process.stdout.write("{}");
process.exit(0);
}
} catch {
// File doesn't exist — user hasn't decided yet
}

// Once-per-session guard: don't ask again after the first message
if (sessionId) {
const flagPath = join(tmpdir(), `vercel-plugin-telemetry-asked-${sessionId}`);
if (existsSync(flagPath)) {
process.stdout.write("{}");
process.exit(0);
}
try {
mkdirSync(dirname(flagPath), { recursive: true });
writeFileSync(flagPath, "1", { flag: "wx" });
} catch {
// Race or permission issue — skip this time
process.stdout.write("{}");
process.exit(0);
}
}

// Write "asked" immediately so we never re-prompt across sessions
try {
mkdirSync(dirname(PREF_PATH), { recursive: true });
writeFileSync(PREF_PATH, "asked");
} catch {
// If we can't write, fall through — tmpdir flag still guards this session
}

const message = [
"After responding to the user's message, use the AskUserQuestion tool to ask about telemetry.",
"Use this exact question configuration:",
'- question: "The Vercel plugin collects anonymous usage data such as skill injection patterns and tools used by default. Would you like to also share your prompt text to help improve skill matching? This choice only controls prompt telemetry."',
'- header: "Telemetry"',
'- options: [{"label": "Share prompts", "description": "Share prompt text in addition to default anonymous usage data"}, {"label": "No thanks", "description": "Keep prompt text private. This only disables prompt telemetry"}]',
"- multiSelect: false",
"",
"After the user responds:",
"- If they chose \"Share prompts\", run: `echo 'enabled' > ~/.claude/vercel-plugin-telemetry-preference`",
"- If they chose \"No thanks\" or anything else, run: `echo 'disabled' > ~/.claude/vercel-plugin-telemetry-preference`",
].join("\n");

const output: SyncHookJSONOutput = {
hookSpecificOutput: {
hookEventName: "UserPromptSubmit" as const,
additionalContext: message,
},
};

process.stdout.write(JSON.stringify(output));
void sessionId;
void prompt;

// Prompt text tracking is intentionally disabled for now, regardless of
// the user's preference file or VERCEL_PLUGIN_TELEMETRY value.
//
// if (isContentTelemetryEnabled() && sessionId && prompt.length >= MIN_PROMPT_LENGTH) {
// await trackContentEvents(sessionId, [
// { key: "prompt:text", value: prompt },
// ]).catch(() => {});
// }

process.stdout.write("{}");
process.exit(0);
}

Expand Down
6 changes: 5 additions & 1 deletion hooks/telemetry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ var TRUNCATION_SUFFIX = "[TRUNCATED]";
var BRIDGE_ENDPOINT = "https://telemetry.vercel.com/api/vercel-plugin/v1/events";
var FLUSH_TIMEOUT_MS = 3e3;
var DEVICE_ID_PATH = join(homedir(), ".claude", "vercel-plugin-device-id");
var DISABLED_CONTENT_KEYS = /* @__PURE__ */ new Set(["prompt:text"]);
function truncateValue(value) {
if (Buffer.byteLength(value, "utf-8") <= MAX_VALUE_BYTES) {
return value;
Expand Down Expand Up @@ -93,6 +94,7 @@ async function trackBaseEvents(sessionId, entries) {
await send(sessionId, events);
}
async function trackContentEvent(sessionId, key, value) {
if (DISABLED_CONTENT_KEYS.has(key)) return;
if (!isContentTelemetryEnabled()) return;
const event = {
id: randomUUID(),
Expand All @@ -104,8 +106,10 @@ async function trackContentEvent(sessionId, key, value) {
}
async function trackContentEvents(sessionId, entries) {
if (!isContentTelemetryEnabled() || entries.length === 0) return;
const filteredEntries = entries.filter((entry) => !DISABLED_CONTENT_KEYS.has(entry.key));
if (filteredEntries.length === 0) return;
const now = Date.now();
const events = entries.map((entry) => ({
const events = filteredEntries.map((entry) => ({
id: randomUUID(),
event_time: now,
key: entry.key,
Expand Down
67 changes: 4 additions & 63 deletions hooks/user-prompt-submit-telemetry.mjs
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
#!/usr/bin/env node

// hooks/src/user-prompt-submit-telemetry.mts
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
import { homedir, tmpdir } from "os";
import { join, dirname } from "path";
import { getTelemetryOverride, isContentTelemetryEnabled, trackContentEvents } from "./telemetry.mjs";
var PREF_PATH = join(homedir(), ".claude", "vercel-plugin-telemetry-preference");
var MIN_PROMPT_LENGTH = 10;
import { readFileSync } from "fs";
function parseStdin() {
try {
const raw = readFileSync(0, "utf-8").trim();
Expand All @@ -26,63 +21,9 @@ async function main() {
const input = parseStdin();
const sessionId = input ? resolveSessionId(input) : "";
const prompt = input ? resolvePrompt(input) : "";
const telemetryOverride = getTelemetryOverride();
if (isContentTelemetryEnabled() && sessionId && prompt.length >= MIN_PROMPT_LENGTH) {
await trackContentEvents(sessionId, [
{ key: "prompt:text", value: prompt }
]).catch(() => {
});
}
if (telemetryOverride === "off") {
process.stdout.write("{}");
process.exit(0);
}
try {
const pref = readFileSync(PREF_PATH, "utf-8").trim();
if (pref === "enabled" || pref === "disabled" || pref === "asked") {
process.stdout.write("{}");
process.exit(0);
}
} catch {
}
if (sessionId) {
const flagPath = join(tmpdir(), `vercel-plugin-telemetry-asked-${sessionId}`);
if (existsSync(flagPath)) {
process.stdout.write("{}");
process.exit(0);
}
try {
mkdirSync(dirname(flagPath), { recursive: true });
writeFileSync(flagPath, "1", { flag: "wx" });
} catch {
process.stdout.write("{}");
process.exit(0);
}
}
try {
mkdirSync(dirname(PREF_PATH), { recursive: true });
writeFileSync(PREF_PATH, "asked");
} catch {
}
const message = [
"After responding to the user's message, use the AskUserQuestion tool to ask about telemetry.",
"Use this exact question configuration:",
'- question: "The Vercel plugin collects anonymous usage data such as skill injection patterns and tools used by default. Would you like to also share your prompt text to help improve skill matching? This choice only controls prompt telemetry."',
'- header: "Telemetry"',
'- options: [{"label": "Share prompts", "description": "Share prompt text in addition to default anonymous usage data"}, {"label": "No thanks", "description": "Keep prompt text private. This only disables prompt telemetry"}]',
"- multiSelect: false",
"",
"After the user responds:",
"- If they chose \"Share prompts\", run: `echo 'enabled' > ~/.claude/vercel-plugin-telemetry-preference`",
"- If they chose \"No thanks\" or anything else, run: `echo 'disabled' > ~/.claude/vercel-plugin-telemetry-preference`"
].join("\n");
const output = {
hookSpecificOutput: {
hookEventName: "UserPromptSubmit",
additionalContext: message
}
};
process.stdout.write(JSON.stringify(output));
void sessionId;
void prompt;
process.stdout.write("{}");
process.exit(0);
}
main();
Loading
Loading