Skip to content
Open
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
4 changes: 4 additions & 0 deletions electron/gateway/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { PORTS } from '../utils/config';
import {
getOpenClawDir,
getOpenClawEntryPath,
getOpenClawConfigDir,
isOpenClawBuilt,
isOpenClawPresent,
appendNodeRequireToNodeOptions,
} from '../utils/paths';
Expand Down Expand Up @@ -1054,6 +1056,7 @@ export class GatewayManager extends EventEmitter {
...process.env,
PATH: finalPath,
...uvEnv,
OPENCLAW_STATE_DIR: getOpenClawConfigDir(),
OPENCLAW_NO_RESPAWN: '1',
};

Expand Down Expand Up @@ -1299,6 +1302,7 @@ export class GatewayManager extends EventEmitter {
...uvEnv,
...proxyEnv,
OPENCLAW_GATEWAY_TOKEN: gatewayToken,
OPENCLAW_STATE_DIR: getOpenClawConfigDir(),
OPENCLAW_SKIP_CHANNELS: '',
CLAWDBOT_SKIP_CHANNELS: '',
// Prevent OpenClaw from respawning itself inside the utility process
Expand Down
6 changes: 3 additions & 3 deletions electron/main/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1493,7 +1493,7 @@ function registerGatewayHandlers(
*/
function registerOpenClawHandlers(gatewayManager: GatewayManager): void {
async function ensureDingTalkPluginInstalled(): Promise<{ installed: boolean; warning?: string }> {
const targetDir = join(homedir(), '.openclaw', 'extensions', 'dingtalk');
const targetDir = join(getOpenClawConfigDir(), 'extensions', 'dingtalk');
const targetManifest = join(targetDir, 'openclaw.plugin.json');

if (existsSync(targetManifest)) {
Expand Down Expand Up @@ -1523,7 +1523,7 @@ function registerOpenClawHandlers(gatewayManager: GatewayManager): void {
}

try {
mkdirSync(join(homedir(), '.openclaw', 'extensions'), { recursive: true });
mkdirSync(join(getOpenClawConfigDir(), 'extensions'), { recursive: true });
rmSync(targetDir, { recursive: true, force: true });
cpSync(sourceDir, targetDir, { recursive: true, dereference: true });

Expand Down Expand Up @@ -2782,7 +2782,7 @@ function mimeToExt(mimeType: string): string {
return '';
}

const OUTBOUND_DIR = join(homedir(), '.openclaw', 'media', 'outbound');
const OUTBOUND_DIR = join(getOpenClawConfigDir(), 'media', 'outbound');

/**
* Generate a preview data URL for image files.
Expand Down
9 changes: 4 additions & 5 deletions electron/utils/channel-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@
import { access, mkdir, readFile, writeFile, readdir, stat, rm } from 'fs/promises';
import { constants } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { getOpenClawResolvedDir } from './paths';
import { getOpenClawResolvedDir, getOpenClawConfigDir } from './paths';
import * as logger from './logger';
import { proxyAwareFetch } from './proxy-fetch';

const OPENCLAW_DIR = join(homedir(), '.openclaw');
const OPENCLAW_DIR = getOpenClawConfigDir();
const CONFIG_FILE = join(OPENCLAW_DIR, 'openclaw.json');

// Channels that are managed as plugins (config goes under plugins.entries, not channels)
Expand Down Expand Up @@ -316,7 +315,7 @@ export async function deleteChannelConfig(channelType: string): Promise<void> {
// Special handling for WhatsApp credentials
if (channelType === 'whatsapp') {
try {
const whatsappDir = join(homedir(), '.openclaw', 'credentials', 'whatsapp');
const whatsappDir = join(getOpenClawConfigDir(), 'credentials', 'whatsapp');
if (await fileExists(whatsappDir)) {
await rm(whatsappDir, { recursive: true, force: true });
console.log('Deleted WhatsApp credentials directory');
Expand All @@ -339,7 +338,7 @@ export async function listConfiguredChannels(): Promise<string[]> {

// Check for WhatsApp credentials directory
try {
const whatsappDir = join(homedir(), '.openclaw', 'credentials', 'whatsapp');
const whatsappDir = join(getOpenClawConfigDir(), 'credentials', 'whatsapp');
if (await fileExists(whatsappDir)) {
const entries = await readdir(whatsappDir);
const hasSession = await (async () => {
Expand Down
4 changes: 2 additions & 2 deletions electron/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export function getPort(key: keyof typeof PORTS): number {
* Application paths
*/
export const APP_PATHS = {
/** OpenClaw configuration directory */
OPENCLAW_CONFIG: '~/.openclaw',
/** OpenClaw configuration directory (isolated from system-wide ~/.openclaw) */
OPENCLAW_CONFIG: '~/.clawx/openclaw',

/** ClawX configuration directory */
CLAWX_CONFIG: '~/.clawx',
Expand Down
12 changes: 6 additions & 6 deletions electron/utils/openclaw-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
import { access, mkdir, readFile, writeFile, readdir } from 'fs/promises';
import { constants, Dirent } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import {
getProviderEnvVar,
getProviderDefaultModel,
getProviderConfig,
} from './provider-registry';
import { getOpenClawConfigDir } from './paths';
import {
OPENCLAW_PROVIDER_KEY_MOONSHOT,
isOAuthProviderType,
Expand Down Expand Up @@ -88,7 +88,7 @@ interface AuthProfilesStore {
// ── Auth Profiles I/O ────────────────────────────────────────────

function getAuthProfilesPath(agentId = 'main'): string {
return join(homedir(), '.openclaw', 'agents', agentId, 'agent', AUTH_PROFILE_FILENAME);
return join(getOpenClawConfigDir(), 'agents', agentId, 'agent', AUTH_PROFILE_FILENAME);
}

async function readAuthProfiles(agentId = 'main'): Promise<AuthProfilesStore> {
Expand All @@ -111,7 +111,7 @@ async function writeAuthProfiles(store: AuthProfilesStore, agentId = 'main'): Pr
// ── Agent Discovery ──────────────────────────────────────────────

async function discoverAgentIds(): Promise<string[]> {
const agentsDir = join(homedir(), '.openclaw', 'agents');
const agentsDir = join(getOpenClawConfigDir(), 'agents');
try {
if (!(await fileExists(agentsDir))) return ['main'];
const entries: Dirent[] = await readdir(agentsDir, { withFileTypes: true });
Expand All @@ -129,7 +129,7 @@ async function discoverAgentIds(): Promise<string[]> {

// ── OpenClaw Config Helpers ──────────────────────────────────────

const OPENCLAW_CONFIG_PATH = join(homedir(), '.openclaw', 'openclaw.json');
const OPENCLAW_CONFIG_PATH = join(getOpenClawConfigDir(), 'openclaw.json');

async function readOpenClawJson(): Promise<Record<string, unknown>> {
return (await readJsonFile<Record<string, unknown>>(OPENCLAW_CONFIG_PATH)) ?? {};
Expand Down Expand Up @@ -302,7 +302,7 @@ export async function removeProviderFromOpenClaw(provider: string): Promise<void

// 2. Remove from models.json (per-agent model registry used by pi-ai directly)
for (const id of agentIds) {
const modelsPath = join(homedir(), '.openclaw', 'agents', id, 'agent', 'models.json');
const modelsPath = join(getOpenClawConfigDir(), 'agents', id, 'agent', 'models.json');
try {
if (await fileExists(modelsPath)) {
const raw = await readFile(modelsPath, 'utf-8');
Expand Down Expand Up @@ -743,7 +743,7 @@ export async function updateAgentModelProvider(
): Promise<void> {
const agentIds = await discoverAgentIds();
for (const agentId of agentIds) {
const modelsPath = join(homedir(), '.openclaw', 'agents', agentId, 'agent', 'models.json');
const modelsPath = join(getOpenClawConfigDir(), 'agents', agentId, 'agent', 'models.json');
let data: Record<string, unknown> = {};
try {
data = (await readJsonFile<Record<string, unknown>>(modelsPath)) ?? {};
Expand Down
4 changes: 2 additions & 2 deletions electron/utils/openclaw-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { constants, Dirent } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { logger } from './logger';
import { getResourcesDir } from './paths';
import { getResourcesDir, getOpenClawConfigDir } from './paths';

const CLAWX_BEGIN = '<!-- clawx:begin -->';
const CLAWX_END = '<!-- clawx:end -->';
Expand Down Expand Up @@ -51,7 +51,7 @@ export function mergeClawXSection(existing: string, section: string): string {
* directories that already exist under ~/.openclaw/.
*/
async function resolveAllWorkspaceDirs(): Promise<string[]> {
const openclawDir = join(homedir(), '.openclaw');
const openclawDir = getOpenClawConfigDir();
const dirs = new Set<string>();

const configPath = join(openclawDir, 'openclaw.json');
Expand Down
8 changes: 6 additions & 2 deletions electron/utils/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@ export function expandPath(path: string): string {
}

/**
* Get OpenClaw config directory
* Get OpenClaw config directory.
*
* Uses a ClawX-specific directory (~/.clawx/openclaw) so that the
* embedded Gateway's config does not collide with a system-wide
* OpenClaw CLI installation at ~/.openclaw.
*/
export function getOpenClawConfigDir(): string {
return join(homedir(), '.openclaw');
return join(homedir(), '.clawx', 'openclaw');
}

/**
Expand Down
7 changes: 3 additions & 4 deletions electron/utils/skill-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@ import { readFile, writeFile, access, cp, mkdir } from 'fs/promises';
import { existsSync } from 'fs';
import { constants } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { getOpenClawDir } from './paths';
import { getOpenClawDir, getOpenClawConfigDir } from './paths';
import { logger } from './logger';

const OPENCLAW_CONFIG_PATH = join(homedir(), '.openclaw', 'openclaw.json');
const OPENCLAW_CONFIG_PATH = join(getOpenClawConfigDir(), 'openclaw.json');

interface SkillEntry {
enabled?: boolean;
Expand Down Expand Up @@ -155,7 +154,7 @@ const BUILTIN_SKILLS = [
* block the normal startup flow.
*/
export async function ensureBuiltinSkillsInstalled(): Promise<void> {
const skillsRoot = join(homedir(), '.openclaw', 'skills');
const skillsRoot = join(getOpenClawConfigDir(), 'skills');

for (const { slug, sourceExtension } of BUILTIN_SKILLS) {
const targetDir = join(skillsRoot, slug);
Expand Down
5 changes: 2 additions & 3 deletions electron/utils/whatsapp-login.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { dirname, join } from 'path';
import { homedir } from 'os';
import { createRequire } from 'module';
import { EventEmitter } from 'events';
import { existsSync, mkdirSync, rmSync } from 'fs';
import { deflateSync } from 'zlib';
import { getOpenClawDir, getOpenClawResolvedDir } from './paths';
import { getOpenClawDir, getOpenClawResolvedDir, getOpenClawConfigDir } from './paths';

const require = createRequire(import.meta.url);

Expand Down Expand Up @@ -232,7 +231,7 @@ export class WhatsAppLoginManager extends EventEmitter {

try {
// Path where OpenClaw expects WhatsApp credentials
const authDir = join(homedir(), '.openclaw', 'credentials', 'whatsapp', accountId);
const authDir = join(getOpenClawConfigDir(), 'credentials', 'whatsapp', accountId);

// Ensure directory exists
if (!existsSync(authDir)) {
Expand Down
54 changes: 54 additions & 0 deletions tests/unit/config-isolation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Tests for OpenClaw config path isolation.
*
* Verifies that ClawX uses ~/.clawx/openclaw instead of ~/.openclaw
* for all OpenClaw configuration, ensuring no conflict with a
* system-wide OpenClaw CLI installation.
*/
import { describe, it, expect, vi } from 'vitest';
import { join } from 'path';

vi.mock('electron', () => ({
app: {
isPackaged: false,
getPath: (name: string) => {
if (name === 'userData') return '/tmp/clawx-test-userdata';
return '/tmp/clawx-test';
},
getAppPath: () => '/tmp/clawx-test-app',
getName: () => 'ClawX',
},
}));

describe('OpenClaw config path isolation', () => {
it('getOpenClawConfigDir() returns ~/.clawx/openclaw instead of ~/.openclaw', async () => {
const { getOpenClawConfigDir } = await import('@electron/utils/paths');
const os = await import('os');
const expected = join(os.homedir(), '.clawx', 'openclaw');
expect(getOpenClawConfigDir()).toBe(expected);
});

it('getOpenClawConfigDir() does NOT return ~/.openclaw', async () => {
const { getOpenClawConfigDir } = await import('@electron/utils/paths');
const os = await import('os');
const systemPath = join(os.homedir(), '.openclaw');
expect(getOpenClawConfigDir()).not.toBe(systemPath);
});

it('getOpenClawSkillsDir() is under the isolated config dir', async () => {
const { getOpenClawConfigDir, getOpenClawSkillsDir } = await import('@electron/utils/paths');
expect(getOpenClawSkillsDir()).toBe(join(getOpenClawConfigDir(), 'skills'));
});

it('getClawXConfigDir() returns ~/.clawx (parent of isolated openclaw config)', async () => {
const { getClawXConfigDir } = await import('@electron/utils/paths');
const os = await import('os');
expect(getClawXConfigDir()).toBe(join(os.homedir(), '.clawx'));
});

it('APP_PATHS.OPENCLAW_CONFIG points to the isolated path', async () => {
const { APP_PATHS } = await import('@electron/utils/config');
expect(APP_PATHS.OPENCLAW_CONFIG).toBe('~/.clawx/openclaw');
expect(APP_PATHS.OPENCLAW_CONFIG).not.toBe('~/.openclaw');
});
});