hotfix(browser): avoid aborting launch on missing managed profile

- skip managed Claude browser attach when the default CCS browser profile
  directory has not been created yet
- keep explicit env override flows failing fast so real misconfiguration still
  surfaces clearly
- add regression coverage for default and settings-profile launches
This commit is contained in:
Tam Nhu Tran
2026-04-16 22:59:43 -04:00
parent a0183073b3
commit 41bf5075e0
7 changed files with 161 additions and 40 deletions
+21 -26
View File
@@ -40,7 +40,7 @@ import {
appendBrowserToolArgs,
ensureBrowserMcpOrThrow,
getEffectiveClaudeBrowserAttachConfig,
resolveBrowserRuntimeEnv,
resolveOptionalBrowserAttachRuntime,
syncBrowserMcpToConfigDir,
} from './utils/browser';
import {
@@ -1057,14 +1057,21 @@ async function main(): Promise<void> {
// Settings-based profiles (glm, glmt) are third-party providers
const imageAnalysisMcpReady =
resolvedTarget === 'claude' ? ensureImageAnalysisMcpOrThrow() : true;
let browserRuntimeEnv: TargetCredentials['browserRuntimeEnv'];
const browserAttachConfig =
resolvedTarget === 'claude'
? getEffectiveClaudeBrowserAttachConfig(getBrowserConfig())
: undefined;
const browserAttachRuntime =
resolvedTarget === 'claude' && browserAttachConfig?.enabled
? await resolveOptionalBrowserAttachRuntime(browserAttachConfig)
: undefined;
const browserRuntimeEnv = browserAttachRuntime?.runtimeEnv;
if (browserAttachRuntime?.warning) {
console.error(warn(browserAttachRuntime.warning));
}
if (resolvedTarget === 'claude') {
ensureWebSearchMcpOrThrow();
if (browserAttachConfig?.enabled) {
if (browserRuntimeEnv) {
ensureBrowserMcpOrThrow();
}
}
@@ -1091,7 +1098,7 @@ async function main(): Promise<void> {
syncWebSearchMcpToConfigDir(inheritedClaudeConfigDir);
syncImageAnalysisMcpToConfigDir(inheritedClaudeConfigDir);
if (
browserAttachConfig?.enabled &&
browserRuntimeEnv &&
inheritedClaudeConfigDir &&
!syncBrowserMcpToConfigDir(inheritedClaudeConfigDir)
) {
@@ -1301,17 +1308,6 @@ async function main(): Promise<void> {
// Explicitly inject effective settings env vars so stale ANTHROPIC_*
// values from prior sessions cannot leak into the active profile.
if (browserAttachConfig?.enabled) {
browserRuntimeEnv = {
...(await resolveBrowserRuntimeEnv({
profileDir: browserAttachConfig.userDataDir,
devtoolsPort: browserAttachConfig.hasExplicitDevtoolsPort
? String(browserAttachConfig.devtoolsPort)
: undefined,
})),
};
}
const envVars: NodeJS.ProcessEnv = {
...globalEnv,
...settingsEnv,
@@ -1471,23 +1467,22 @@ async function main(): Promise<void> {
CCS_WEBSEARCH_SKIP: '1',
CCS_IMAGE_ANALYSIS_SKIP: '1',
};
let browserRuntimeEnv: TargetCredentials['browserRuntimeEnv'];
const browserAttachConfig =
resolvedTarget === 'claude'
? getEffectiveClaudeBrowserAttachConfig(getBrowserConfig())
: undefined;
const browserAttachRuntime =
resolvedTarget === 'claude' && browserAttachConfig?.enabled
? await resolveOptionalBrowserAttachRuntime(browserAttachConfig)
: undefined;
const browserRuntimeEnv = browserAttachRuntime?.runtimeEnv;
if (browserAttachRuntime?.warning) {
console.error(warn(browserAttachRuntime.warning));
}
if (resolvedTarget === 'claude') {
if (browserAttachConfig?.enabled) {
if (browserRuntimeEnv) {
ensureBrowserMcpOrThrow();
browserRuntimeEnv = {
...(await resolveBrowserRuntimeEnv({
profileDir: browserAttachConfig.userDataDir,
devtoolsPort: browserAttachConfig.hasExplicitDevtoolsPort
? String(browserAttachConfig.devtoolsPort)
: undefined,
})),
};
Object.assign(envVars, browserRuntimeEnv);
}
const defaultContinuityInheritance = await resolveProfileContinuityInheritance({
@@ -1505,7 +1500,7 @@ async function main(): Promise<void> {
if (defaultContinuityInheritance.claudeConfigDir) {
envVars.CLAUDE_CONFIG_DIR = defaultContinuityInheritance.claudeConfigDir;
if (
browserAttachConfig?.enabled &&
browserRuntimeEnv &&
!syncBrowserMcpToConfigDir(defaultContinuityInheritance.claudeConfigDir)
) {
throw new Error(
+10 -14
View File
@@ -67,7 +67,7 @@ import {
appendBrowserToolArgs,
ensureBrowserMcpOrThrow,
getEffectiveClaudeBrowserAttachConfig,
resolveBrowserRuntimeEnv,
resolveOptionalBrowserAttachRuntime,
syncBrowserMcpToConfigDir,
} from '../../utils/browser';
import {
@@ -265,7 +265,14 @@ export async function execClaudeWithCLIProxy(
ensureWebSearchMcpOrThrow();
const imageAnalysisMcpReady = ensureImageAnalysisMcpOrThrow();
const browserAttachConfig = getEffectiveClaudeBrowserAttachConfig(getBrowserConfig());
if (browserAttachConfig.enabled) {
const browserAttachRuntime = browserAttachConfig.enabled
? await resolveOptionalBrowserAttachRuntime(browserAttachConfig)
: undefined;
const browserRuntimeEnv = browserAttachRuntime?.runtimeEnv;
if (browserAttachRuntime?.warning) {
console.error(warn(browserAttachRuntime.warning));
}
if (browserRuntimeEnv) {
ensureBrowserMcpOrThrow();
}
displayWebSearchStatus();
@@ -1054,7 +1061,7 @@ export async function execClaudeWithCLIProxy(
syncImageAnalysisMcpToConfigDir(inheritedClaudeConfigDir);
if (
browserAttachConfig.enabled &&
browserRuntimeEnv &&
inheritedClaudeConfigDir &&
!syncBrowserMcpToConfigDir(inheritedClaudeConfigDir)
) {
@@ -1157,17 +1164,6 @@ export async function execClaudeWithCLIProxy(
}
// 11. Build final environment with all proxy chains
const browserRuntimeEnv = browserAttachConfig.enabled
? {
...(await resolveBrowserRuntimeEnv({
profileDir: browserAttachConfig.userDataDir,
devtoolsPort: browserAttachConfig.hasExplicitDevtoolsPort
? String(browserAttachConfig.devtoolsPort)
: undefined,
})),
}
: undefined;
const env = buildClaudeEnvironment({
provider,
useRemoteProxy,
+36
View File
@@ -2,6 +2,7 @@ import * as path from 'path';
import type { BrowserConfig } from '../../config/unified-config-types';
import { getCcsDir } from '../config-manager';
import { expandPath } from '../helpers';
import { type BrowserRuntimeEnv, resolveBrowserRuntimeEnv } from './chrome-reuse';
export type BrowserOverrideSource = 'CCS_BROWSER_USER_DATA_DIR' | 'CCS_BROWSER_PROFILE_DIR';
@@ -18,6 +19,11 @@ export function getRecommendedBrowserUserDataDir(): string {
return path.join(getCcsDir(), 'browser', 'chrome-user-data');
}
export interface BrowserAttachRuntimeResolution {
runtimeEnv?: BrowserRuntimeEnv;
warning?: string;
}
export function resolveBrowserUserDataDir(value?: string): string | undefined {
return value?.trim() ? expandPath(value) : undefined;
}
@@ -81,6 +87,36 @@ export function getEffectiveClaudeBrowserAttachConfig(
};
}
export async function resolveOptionalBrowserAttachRuntime(
config: EffectiveClaudeBrowserAttachConfig
): Promise<BrowserAttachRuntimeResolution> {
if (!config.enabled) {
return {};
}
try {
return {
runtimeEnv: await resolveBrowserRuntimeEnv({
profileDir: config.userDataDir,
devtoolsPort: config.hasExplicitDevtoolsPort ? String(config.devtoolsPort) : undefined,
}),
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const usesManagedDefaultDir =
config.source === 'config' &&
path.resolve(config.userDataDir) === path.resolve(getRecommendedBrowserUserDataDir());
if (usesManagedDefaultDir && message.includes('Chrome profile directory is invalid')) {
return {
warning: `Claude Browser Attach is enabled, but the managed browser profile does not exist yet (${config.userDataDir}). Launching without browser tools. Run \`ccs browser doctor\` to finish setup.`,
};
}
throw error;
}
}
function parseDevtoolsPort(value?: string): number | undefined {
if (!value?.trim() || !/^\d+$/.test(value.trim())) {
return undefined;
+1
View File
@@ -9,6 +9,7 @@ export interface BrowserReuseOptions {
}
export interface BrowserRuntimeEnv {
[key: string]: string;
CCS_BROWSER_USER_DATA_DIR: string;
CCS_BROWSER_DEVTOOLS_HOST: string;
CCS_BROWSER_DEVTOOLS_PORT: string;
+5
View File
@@ -21,6 +21,11 @@ export {
getRecommendedBrowserUserDataDir,
getBrowserAttachOverride,
getEffectiveClaudeBrowserAttachConfig,
resolveOptionalBrowserAttachRuntime,
} from './browser-settings';
export type {
BrowserAttachRuntimeResolution,
EffectiveClaudeBrowserAttachConfig,
} from './browser-settings';
export {
@@ -247,6 +247,50 @@ server.listen(0, '127.0.0.1', () => {
expect(launchedEnv).toContain('wsUrl=ws://127.0.0.1/devtools/browser/default-target');
});
it('skips managed browser attach when the default CCS browser profile directory is missing', () => {
if (process.platform === 'win32') return;
const originalCcsHome = process.env.CCS_HOME;
process.env.CCS_HOME = tmpHome;
try {
mutateUnifiedConfig((config) => {
config.browser = {
claude: {
enabled: true,
user_data_dir: '',
devtools_port: 9222,
},
codex: {
enabled: true,
},
};
});
const result = runCcs(['default', 'smoke'], {
...baseEnv,
});
expect(result.status).toBe(0);
expect(result.stderr).toContain('Launching without browser tools');
expect(result.stderr).toContain('ccs browser doctor');
const launchedArgs = fs.readFileSync(claudeArgsLogPath, 'utf8');
expect(launchedArgs).not.toContain(BROWSER_PROMPT_SNIPPET);
const launchedEnv = fs.readFileSync(claudeEnvLogPath, 'utf8');
expect(launchedEnv).toContain('userDataDir=');
expect(launchedEnv).not.toContain('.ccs/browser/chrome-user-data');
expect(launchedEnv).not.toContain('ws://127.0.0.1/devtools/browser/');
} finally {
if (originalCcsHome !== undefined) {
process.env.CCS_HOME = originalCcsHome;
} else {
delete process.env.CCS_HOME;
}
}
});
it('uses config-backed browser attach settings when env overrides are absent', async () => {
if (process.platform === 'win32') return;
@@ -198,6 +198,50 @@ server.listen(0, '127.0.0.1', () => {
expect(launchedEnv).toContain('wsUrl=ws://127.0.0.1/devtools/browser/browser-target');
});
it('skips managed browser attach for settings-profile launches when the default CCS browser profile directory is missing', () => {
if (process.platform === 'win32') return;
const originalCcsHome = process.env.CCS_HOME;
process.env.CCS_HOME = tmpHome;
try {
mutateUnifiedConfig((config) => {
config.browser = {
claude: {
enabled: true,
user_data_dir: '',
devtools_port: 9222,
},
codex: {
enabled: true,
},
};
});
const result = runCcs(['glm', 'smoke'], {
...baseEnv,
});
expect(result.status).toBe(0);
expect(result.stderr).toContain('Launching without browser tools');
expect(result.stderr).toContain('ccs browser doctor');
const launchedArgs = fs.readFileSync(claudeArgsLogPath, 'utf8');
expect(launchedArgs).not.toContain(BROWSER_PROMPT_SNIPPET);
const launchedEnv = fs.readFileSync(claudeEnvLogPath, 'utf8');
expect(launchedEnv).toContain('userDataDir=');
expect(launchedEnv).not.toContain('.ccs/browser/chrome-user-data');
expect(launchedEnv).not.toContain('ws://127.0.0.1/devtools/browser/');
} finally {
if (originalCcsHome !== undefined) {
process.env.CCS_HOME = originalCcsHome;
} else {
delete process.env.CCS_HOME;
}
}
});
it('uses config-backed browser attach settings for settings-profile launches', async () => {
if (process.platform === 'win32') return;