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
5 changes: 5 additions & 0 deletions .changeset/loose-yaks-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": minor
---

fix cli ephemeral mode config leak
117 changes: 117 additions & 0 deletions cli/src/config/__tests__/persistence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ vi.mock("../../services/logs.js", () => ({
},
}))

// Mock env-config to control ephemeral mode behavior
vi.mock("../env-config.js", async () => {
const actual = await vi.importActual<typeof import("../env-config.js")>("../env-config.js")
return {
...actual,
isEphemeralMode: vi.fn(() => false), // Default to false, tests can override
}
})

// Mock fs/promises to handle schema.json reads
vi.mock("fs/promises", async () => {
const actual = await vi.importActual<typeof import("fs/promises")>("fs/promises")
Expand Down Expand Up @@ -358,4 +367,112 @@ describe("Config Persistence", () => {
expect(token).toBeNull()
})
})

describe("ephemeral mode", () => {
it("should not write config file when in ephemeral mode", async () => {
// Import the mocked module to control ephemeral mode
const envConfig = await import("../env-config.js")
vi.mocked(envConfig.isEphemeralMode).mockReturnValue(true)

const testConfig: CLIConfig = {
version: "1.0.0",
mode: "code",
telemetry: false,
provider: "test",
providers: [
{
id: "test",
provider: "kilocode",
kilocodeToken: "env-token-should-not-be-saved",
kilocodeModel: "test-model",
},
],
}

// This should NOT create a file because we're in ephemeral mode
await saveConfig(testConfig)

// Verify file was NOT created
const exists = await configExists()
expect(exists).toBe(false)

// Reset mock
vi.mocked(envConfig.isEphemeralMode).mockReturnValue(false)
})

it("should write config file when not in ephemeral mode", async () => {
// Import the mocked module to control ephemeral mode
const envConfig = await import("../env-config.js")
vi.mocked(envConfig.isEphemeralMode).mockReturnValue(false)

const testConfig: CLIConfig = {
version: "1.0.0",
mode: "code",
telemetry: false,
provider: "test",
providers: [
{
id: "test",
provider: "kilocode",
kilocodeToken: "real-token-should-be-saved",
kilocodeModel: "test-model",
},
],
}

// This should create a file because we're NOT in ephemeral mode
await saveConfig(testConfig)

// Verify file WAS created
const exists = await configExists()
expect(exists).toBe(true)

// Verify content was written correctly
const content = await fs.readFile(TEST_CONFIG_FILE, "utf-8")
const parsed = JSON.parse(content)
expect(parsed.providers[0].kilocodeToken).toBe("real-token-should-be-saved")
})

it("should not persist merged config during loadConfig when in ephemeral mode", async () => {
// Import the mocked module to control ephemeral mode
const envConfig = await import("../env-config.js")

// First, create a config file while NOT in ephemeral mode
vi.mocked(envConfig.isEphemeralMode).mockReturnValue(false)

const initialConfig: CLIConfig = {
version: "1.0.0",
mode: "code",
telemetry: true,
provider: "test",
providers: [
{
id: "test",
provider: "kilocode",
kilocodeToken: "original-token-1234567890",
kilocodeModel: "test-model",
},
],
autoApproval: DEFAULT_CONFIG.autoApproval,
theme: "dark",
customThemes: {},
}
await saveConfig(initialConfig)

// Now switch to ephemeral mode
vi.mocked(envConfig.isEphemeralMode).mockReturnValue(true)

// Load the config - this would normally trigger a save after merging
const result = await loadConfig()
expect(result.config.providers[0]).toHaveProperty("kilocodeToken", "original-token-1234567890")

// Verify the file still has the original content (not re-saved in ephemeral mode)
const content = await fs.readFile(TEST_CONFIG_FILE, "utf-8")
const parsed = JSON.parse(content)
expect(parsed.providers[0].kilocodeToken).toBe("original-token-1234567890")

// Reset mock
vi.mocked(envConfig.isEphemeralMode).mockReturnValue(false)
})
})
})
8 changes: 8 additions & 0 deletions cli/src/config/persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,14 @@ export async function loadConfig(): Promise<ConfigLoadResult> {
}

export async function saveConfig(config: CLIConfig, skipValidation: boolean = false): Promise<void> {
// Don't write to disk in ephemeral mode - this prevents environment variable
// values from being persisted to config.json during integration tests or
// when running in ephemeral/Docker environments
if (isEphemeralMode()) {
logs.debug("Skipping config save in ephemeral mode", "ConfigPersistence")
return
}

try {
await ensureConfigDir()

Expand Down