Skip to content
Merged
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
13 changes: 13 additions & 0 deletions .changeset/hardening-egress-defaults.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@moonshot-ai/kimi-code': minor
---

Telemetry is opt-in in this fork. It previously ran unless the config said
`telemetry = false`, including when the config could not be read at all, so an
install that never made a choice still reported. It now runs only when the
config opts in; an unreadable config is treated as no consent rather than as
consent. `KIMI_DISABLE_TELEMETRY` continues to work as a hard override.

`KIMI_CODE_NO_TIPS=1` disables the startup tips fetch, which was the one
network call at launch with no way to turn it off — so an install configured
for no outbound traffic still beaconed on every run.
6 changes: 4 additions & 2 deletions apps/kimi-code/src/cli/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ export function initializeCliTelemetry(options: InitializeCliTelemetryOptions):
initializeTelemetry({
homeDir: options.harness.homeDir,
deviceId: options.bootstrap.deviceId,
enabled: options.config.telemetry !== false,
// Opt-in in this fork: telemetry only runs when the user turns it on.
enabled: options.config.telemetry === true,
appName: CLI_USER_AGENT_PRODUCT,
version: options.version,
uiMode: options.uiMode,
Expand Down Expand Up @@ -100,7 +101,8 @@ export function initializeServerTelemetry(
initializeTelemetry({
homeDir: bootstrap.homeDir,
deviceId: bootstrap.deviceId,
enabled: config.telemetry !== false,
// Opt-in in this fork: telemetry only runs when the user turns it on.
enabled: config.telemetry === true,
appName: CLI_USER_AGENT_PRODUCT,
version: options.version,
uiMode: WEB_UI_MODE,
Expand Down
11 changes: 11 additions & 0 deletions apps/kimi-code/src/tui/banner/banner-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,14 @@ export function selectDisplayableBanner({
return pickRandomCandidate(candidates, random);
}

/**
* `KIMI_CODE_NO_TIPS` disables the startup tips fetch. Same truthy values as
* the other kill switches (`1` / `true` / `yes` / `on`).
*/
export function isTipsBannerDisabled(env: NodeJS.ProcessEnv = process.env): boolean {
return ['1', 'true', 'yes', 'on'].includes((env['KIMI_CODE_NO_TIPS'] ?? '').trim().toLowerCase());
}

export class BannerProvider {
constructor(
private readonly clientVersion: string,
Expand All @@ -322,6 +330,9 @@ export class BannerProvider {
fetchImpl: typeof fetch = fetch,
options: BannerProviderLoadOptions = {},
): Promise<BannerState | null> {
// The banner is the one startup fetch with no way to turn it off, which
// makes an otherwise offline-configured install still beacon on every run.
if (isTipsBannerDisabled()) return null;
try {
const controller = new AbortController();
const timeout = setTimeout(() => {
Expand Down
25 changes: 23 additions & 2 deletions apps/kimi-code/test/cli/telemetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,16 +104,37 @@ describe('initializeServerTelemetry', () => {
);
});

it('degrades to enabled with no model when config is unreadable', async () => {
it('stays disabled when the config does not opt in', async () => {
mocks.loadRuntimeConfigSafe.mockReturnValue({ config: {}, fileError: undefined });
const { initializeServerTelemetry } = await import('#/cli/telemetry');
initializeServerTelemetry({ version: '1.2.3' });

expect(mocks.initializeTelemetry).toHaveBeenCalledWith(
expect.objectContaining({ enabled: false }),
);
});

it('enables only when the config opts in', async () => {
mocks.loadRuntimeConfigSafe.mockReturnValue({ config: { telemetry: true }, fileError: undefined });
const { initializeServerTelemetry } = await import('#/cli/telemetry');
initializeServerTelemetry({ version: '1.2.3' });

expect(mocks.initializeTelemetry).toHaveBeenCalledWith(
expect.objectContaining({ enabled: true }),
);
});

it('degrades to disabled with no model when config is unreadable', async () => {
mocks.loadRuntimeConfigSafe.mockReturnValue({
config: {},
fileError: new Error('bad toml'),
});
const { initializeServerTelemetry } = await import('#/cli/telemetry');
initializeServerTelemetry({ version: '1.2.3' });

// Telemetry is opt-in, so a config we could not read is not consent.
expect(mocks.initializeTelemetry).toHaveBeenCalledWith(
expect.objectContaining({ enabled: true, model: undefined }),
expect.objectContaining({ enabled: false, model: undefined }),
);
});
});
18 changes: 17 additions & 1 deletion apps/kimi-code/test/tui/banner/banner-provider.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { describe, expect, it } from 'vitest';
import { describe, expect, it, vi } from 'vitest';

import {
selectBannerState,
selectDisplayableBanner,
shouldDisplayBanner,
} from '#/tui/banner/banner-provider';
import type { BannerState } from '#/tui/types';
import { BannerProvider } from '#/tui/banner/banner-provider';

describe('selectBannerState', () => {
const now = new Date('2026-06-15T12:00:00+08:00');
Expand Down Expand Up @@ -679,3 +680,18 @@ describe('selectDisplayableBanner', () => {
expect(result).toMatchObject({ key: 'fallback-always', display: 'always' });
});
});

describe('tips banner kill switch', () => {
it('skips the fetch entirely when KIMI_CODE_NO_TIPS is set', async () => {
vi.stubEnv('KIMI_CODE_NO_TIPS', '1');
try {
const fetchImpl = vi.fn<typeof fetch>();
const provider = new BannerProvider('1.0.0');

await expect(provider.load(fetchImpl)).resolves.toBeNull();
expect(fetchImpl).not.toHaveBeenCalled();
} finally {
vi.unstubAllEnvs();
}
});
});
Loading