fix: support Claude subcommands and background sessions

Squash merge PR #1240.\n\nValidated locally from the PR head because trusted-author GitHub CI was skipped for this fork PR:\n- bun run validate\n- bun run build:all\n- bun run test:all\n- CCS_E2E_SKIP_BUILD=1 bun run test:e2e
This commit is contained in:
walker1211
2026-05-14 10:18:23 -04:00
committed by GitHub
parent e87b627764
commit 35b6210cd2
7 changed files with 340 additions and 113 deletions
+20 -11
View File
@@ -26,6 +26,8 @@ import { setupCleanupHandlers } from './session-bridge';
import { resolveRuntimeQuotaMonitorProviders } from './account-resolution';
import {
isClaudeSubcommandInvocation,
stripClaudeCodeFeatureBlockingEnv,
stripClaudeSubcommandSessionArgs,
stripSubcommandBlockingEnv,
} from '../../utils/claude-subcommand-detector';
@@ -94,18 +96,22 @@ export async function launchClaude(context: ClaudeLaunchContext): Promise<ChildP
? cfg.customSettingsPath.replace(/^~/, os.homedir())
: getProviderSettingsPath(provider);
// Assemble final args: image analysis tools → browser tools → web search tools → settings
const imageAnalysisArgs = imageAnalysisMcpReady
? appendThirdPartyImageAnalysisToolArgs(claudeArgs)
: claudeArgs;
const browserArgs = browserRuntimeEnv
? appendBrowserToolArgs(imageAnalysisArgs)
: imageAnalysisArgs;
// Claude subcommands (agents, doctor, mcp, ...) don't accept `--settings` —
// its presence flips `agents` into list mode instead of opening the
// interactive agent view. Provider routing still flows via env vars.
// Issue #1218.
const isSubcommand = isClaudeSubcommandInvocation(claudeArgs);
const claudeSessionArgs = isSubcommand
? stripClaudeSubcommandSessionArgs(claudeArgs)
: claudeArgs;
// Assemble final args: image analysis tools → browser tools → web search tools → settings
const imageAnalysisArgs = imageAnalysisMcpReady
? appendThirdPartyImageAnalysisToolArgs(claudeSessionArgs)
: claudeSessionArgs;
const browserArgs = browserRuntimeEnv
? appendBrowserToolArgs(imageAnalysisArgs)
: imageAnalysisArgs;
const launchArgs = isSubcommand
? appendThirdPartyWebSearchToolArgs(browserArgs)
: ['--settings', settingsPath, ...appendThirdPartyWebSearchToolArgs(browserArgs)];
@@ -119,12 +125,11 @@ export async function launchClaude(context: ClaudeLaunchContext): Promise<ChildP
settingsPath,
claudeConfigDir: inheritedClaudeConfigDir,
});
const baseTracedEnv = stripClaudeCodeFeatureBlockingEnv({ ...env, ...traceEnv });
// Strip telemetry-disable env vars for subcommands; otherwise Claude's
// `agents`/`mcp`/... TUIs silently fall back to non-interactive list mode.
// Issue #1218.
const tracedEnv = isSubcommand
? stripSubcommandBlockingEnv({ ...env, ...traceEnv })
: { ...env, ...traceEnv };
const tracedEnv = isSubcommand ? stripSubcommandBlockingEnv(baseTracedEnv) : baseTracedEnv;
// Spawn: Windows .cmd/.bat/.ps1 need shell escaping; all others spawn directly
let claude: ChildProcess;
@@ -158,6 +163,9 @@ export async function launchClaude(context: ClaudeLaunchContext): Promise<ChildP
}
}
const hasSessionProxy = Boolean(codexReasoningProxy || toolSanitizationProxy || httpsTunnel);
const backgroundKeepAliveBaseUrl = hasSessionProxy ? tracedEnv.ANTHROPIC_BASE_URL : undefined;
// Wire cleanup handlers (process exit, SIGINT, SIGTERM, proxy teardown)
setupCleanupHandlers(
claude,
@@ -166,7 +174,8 @@ export async function launchClaude(context: ClaudeLaunchContext): Promise<ChildP
codexReasoningProxy,
toolSanitizationProxy,
httpsTunnel,
verbose
verbose,
{ backgroundKeepAliveBaseUrl }
);
return claude;
+119 -78
View File
@@ -8,7 +8,7 @@
* - Startup lock coordination
*/
import { ChildProcess } from 'child_process';
import { ChildProcess, execFileSync } from 'child_process';
import { info, warn } from '../../utils/ui';
import { getInstalledCliproxyVersion } from '../binary-manager';
import { CLIProxyBackend } from '../types';
@@ -33,6 +33,70 @@ export interface ProxySessionResult {
shouldSpawn: boolean;
}
export interface SessionProxyKeepAliveOptions {
backgroundKeepAliveBaseUrl?: string;
keepAlivePollIntervalMs?: number;
hasBackgroundWorkerUsingBaseUrl?: (baseUrl: string) => boolean;
}
export interface ShouldKeepSessionProxiesAliveOptions {
code: number | null;
signal: NodeJS.Signals | null;
backgroundKeepAliveBaseUrl?: string;
hasBackgroundWorkerUsingBaseUrl: (baseUrl: string) => boolean;
}
const CLAUDE_BG_WORKER_ARGS = ['--bg-spare', '--bg-pty-host'] as const;
export function hasClaudeBackgroundWorkerUsingBaseUrlInProcessList(
processList: string,
baseUrl: string
): boolean {
const envNeedle = `ANTHROPIC_BASE_URL=${baseUrl}`;
return processList
.split(/\r?\n/)
.some(
(line) => line.includes(envNeedle) && CLAUDE_BG_WORKER_ARGS.some((arg) => line.includes(arg))
);
}
export function hasClaudeBackgroundWorkerUsingBaseUrl(baseUrl: string): boolean {
if (process.platform === 'win32') {
return false;
}
for (const args of [['auxEww'], ['auxeww'], ['eww', '-axo', 'pid=,command=']]) {
try {
const processList = execFileSync('ps', args, {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
});
if (hasClaudeBackgroundWorkerUsingBaseUrlInProcessList(processList, baseUrl)) {
return true;
}
} catch {
continue;
}
}
return false;
}
export function shouldKeepSessionProxiesAlive(
options: ShouldKeepSessionProxiesAliveOptions
): boolean {
if (options.code !== 0 || options.signal) {
return false;
}
const baseUrl = options.backgroundKeepAliveBaseUrl;
if (!baseUrl) {
return false;
}
return options.hasBackgroundWorkerUsingBaseUrl(baseUrl);
}
/**
* Check for existing proxy and handle version mismatch, or determine if new spawn needed
*/
@@ -180,7 +244,8 @@ export function setupCleanupHandlers(
codexReasoningProxy: unknown,
toolSanitizationProxy: unknown,
httpsTunnel: unknown,
verbose: boolean
verbose: boolean,
keepAliveOptions: SessionProxyKeepAliveOptions = {}
): void {
const log = (msg: string) => {
if (verbose) {
@@ -188,66 +253,67 @@ export function setupCleanupHandlers(
}
};
const cleanup = () => {
stopQuotaMonitor();
log('Parent signal received, cleaning up');
let resourcesStopped = false;
if (
codexReasoningProxy &&
typeof codexReasoningProxy === 'object' &&
'stop' in codexReasoningProxy
) {
(codexReasoningProxy as { stop: () => void }).stop();
const stopResource = (resource: unknown) => {
if (resource && typeof resource === 'object' && 'stop' in resource) {
(resource as { stop: () => void }).stop();
}
if (
toolSanitizationProxy &&
typeof toolSanitizationProxy === 'object' &&
'stop' in toolSanitizationProxy
) {
(toolSanitizationProxy as { stop: () => void }).stop();
}
if (httpsTunnel && typeof httpsTunnel === 'object' && 'stop' in httpsTunnel) {
(httpsTunnel as { stop: () => void }).stop();
}
// Unregister session, proxy keeps running (local mode only)
if (sessionId) {
unregisterSession(sessionId, sessionPort);
}
claude.kill('SIGTERM');
};
claude.on('exit', (code, signal) => {
const stopSessionResources = () => {
if (resourcesStopped) return;
resourcesStopped = true;
stopQuotaMonitor();
log(`Claude exited: code=${code}, signal=${signal}`);
stopResource(codexReasoningProxy);
stopResource(toolSanitizationProxy);
stopResource(httpsTunnel);
if (
codexReasoningProxy &&
typeof codexReasoningProxy === 'object' &&
'stop' in codexReasoningProxy
) {
(codexReasoningProxy as { stop: () => void }).stop();
}
if (
toolSanitizationProxy &&
typeof toolSanitizationProxy === 'object' &&
'stop' in toolSanitizationProxy
) {
(toolSanitizationProxy as { stop: () => void }).stop();
}
if (httpsTunnel && typeof httpsTunnel === 'object' && 'stop' in httpsTunnel) {
(httpsTunnel as { stop: () => void }).stop();
}
// Unregister this session (proxy keeps running for persistence) - only for local mode
if (sessionId) {
unregisterSession(sessionId, sessionPort);
log(`Session ${sessionId} unregistered, proxy persists for other sessions or future use`);
}
};
const backgroundProbe =
keepAliveOptions.hasBackgroundWorkerUsingBaseUrl ?? hasClaudeBackgroundWorkerUsingBaseUrl;
const startKeepAliveWatcher = (baseUrl: string) => {
const timer = setInterval(() => {
if (backgroundProbe(baseUrl)) return;
log('No Claude bg worker is using the session proxy; cleaning up');
stopSessionResources();
process.exit(0);
}, keepAliveOptions.keepAlivePollIntervalMs ?? 30000);
timer.unref();
};
const cleanup = () => {
log('Parent signal received, cleaning up');
stopSessionResources();
claude.kill('SIGTERM');
};
claude.on('exit', (code, signal) => {
log(`Claude exited: code=${code}, signal=${signal}`);
const backgroundKeepAliveBaseUrl = keepAliveOptions.backgroundKeepAliveBaseUrl;
if (
shouldKeepSessionProxiesAlive({
code,
signal,
backgroundKeepAliveBaseUrl,
hasBackgroundWorkerUsingBaseUrl: backgroundProbe,
}) &&
backgroundKeepAliveBaseUrl
) {
stopQuotaMonitor();
log(`Keeping session proxy alive for Claude bg worker: ${backgroundKeepAliveBaseUrl}`);
startKeepAliveWatcher(backgroundKeepAliveBaseUrl);
return;
}
stopSessionResources();
if (signal) {
process.kill(process.pid, signal as NodeJS.Signals);
@@ -257,33 +323,8 @@ export function setupCleanupHandlers(
});
claude.on('error', (error) => {
stopQuotaMonitor();
console.error(require('../../utils/ui').fail(`Claude CLI error: ${error}`));
if (
codexReasoningProxy &&
typeof codexReasoningProxy === 'object' &&
'stop' in codexReasoningProxy
) {
(codexReasoningProxy as { stop: () => void }).stop();
}
if (
toolSanitizationProxy &&
typeof toolSanitizationProxy === 'object' &&
'stop' in toolSanitizationProxy
) {
(toolSanitizationProxy as { stop: () => void }).stop();
}
if (httpsTunnel && typeof httpsTunnel === 'object' && 'stop' in httpsTunnel) {
(httpsTunnel as { stop: () => void }).stop();
}
// Unregister session, proxy keeps running (local mode only)
if (sessionId) {
unregisterSession(sessionId, sessionPort);
}
stopSessionResources();
process.exit(1);
});
+10 -3
View File
@@ -44,7 +44,10 @@ import {
normalizeDeprecatedGlmtEnv,
} from '../../utils/glmt-deprecation';
import { createOpenAICompatLaunchSettings } from '../../utils/openai-compat-launch-settings';
import { isClaudeSubcommandInvocation } from '../../utils/claude-subcommand-detector';
import {
isClaudeSubcommandInvocation,
stripClaudeSubcommandSessionArgs,
} from '../../utils/claude-subcommand-detector';
import {
resolveDroidProvider,
evaluateTargetRuntimeCompatibility,
@@ -371,8 +374,11 @@ export async function runSettingsFlow(ctx: ProfileDispatchContext): Promise<void
// Claude subcommands reject `--settings` (it flips `agents` to list mode).
// Routing env vars still flow via proxyEnv. Issue #1218.
const isSubcommand = isClaudeSubcommandInvocation(browserArgs);
const subcommandArgs = isSubcommand
? stripClaudeSubcommandSessionArgs(browserArgs)
: browserArgs;
const launchArgs = isSubcommand
? appendThirdPartyWebSearchToolArgs(browserArgs)
? appendThirdPartyWebSearchToolArgs(subcommandArgs)
: [
'--settings',
launchSettings.settingsPath,
@@ -393,8 +399,9 @@ export async function runSettingsFlow(ctx: ProfileDispatchContext): Promise<void
// Skip `--settings` for Claude subcommands so the interactive agent view
// works; env vars from the settings file still flow via envVars. Issue #1218.
const isSubcommand = isClaudeSubcommandInvocation(browserArgs);
const subcommandArgs = isSubcommand ? stripClaudeSubcommandSessionArgs(browserArgs) : browserArgs;
const launchArgs = isSubcommand
? appendThirdPartyWebSearchToolArgs(browserArgs)
? appendThirdPartyWebSearchToolArgs(subcommandArgs)
: ['--settings', expandedSettingsPath, ...appendThirdPartyWebSearchToolArgs(browserArgs)];
const traceEnv = createWebSearchTraceContext({
launcher: 'ccs.settings-profile',
+57 -15
View File
@@ -80,9 +80,20 @@ const VALUE_TAKING_FLAGS = new Set<string>([
'--setting-sources',
'--settings',
'--system-prompt',
'--teammate-mode',
'--tools',
]);
const SUBCOMMAND_SESSION_ONLY_FLAGS = new Set<string>([
'--allow-dangerously-skip-permissions',
'--dangerously-skip-permissions',
]);
const SUBCOMMAND_SESSION_ONLY_VALUE_FLAGS = new Set<string>([
'--permission-mode',
'--teammate-mode',
]);
/**
* Returns true when `args` look like a Claude subcommand invocation.
*
@@ -119,27 +130,49 @@ export function isClaudeSubcommandInvocation(args: readonly string[]): boolean {
return false;
}
/**
* Claude env vars that disable interactive subcommand TUIs (e.g. the new
* `claude agents` agent view) when set. CCS injects these as defaults to
* silence telemetry/bug-report prompts in normal sessions, but they trip
* subcommands into non-interactive list mode. Issue #1218.
*
* Strip only for confirmed Claude subcommand invocations — keep the user's
* telemetry preference intact for everything else.
*/
const SUBCOMMAND_BLOCKING_ENV_KEYS = ['DISABLE_TELEMETRY'] as const;
export function stripClaudeSubcommandSessionArgs(args: readonly string[]): string[] {
if (!isClaudeSubcommandInvocation(args)) {
return [...args];
}
const out: string[] = [];
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
if (SUBCOMMAND_SESSION_ONLY_FLAGS.has(arg)) {
continue;
}
if (arg.startsWith('--permission-mode=') || arg.startsWith('--teammate-mode=')) {
continue;
}
if (SUBCOMMAND_SESSION_ONLY_VALUE_FLAGS.has(arg)) {
const next = args[i + 1];
if (next !== undefined && !next.startsWith('-')) {
i += 1;
}
continue;
}
out.push(arg);
}
return out;
}
/**
* Return a shallow copy of `env` with subcommand-blocking telemetry vars
* removed. Caller is responsible for only invoking this when args are a
* Claude subcommand invocation.
* Claude Code treats DISABLE_TELEMETRY as a feature kill switch for agent-view
* and background-session surfaces, not only as telemetry preference.
*/
export function stripSubcommandBlockingEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
const CLAUDE_CODE_FEATURE_BLOCKING_ENV_KEYS = ['DISABLE_TELEMETRY'] as const;
export function stripClaudeCodeFeatureBlockingEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
const out: NodeJS.ProcessEnv = {};
for (const [key, value] of Object.entries(env)) {
if (
SUBCOMMAND_BLOCKING_ENV_KEYS.includes(key as (typeof SUBCOMMAND_BLOCKING_ENV_KEYS)[number])
CLAUDE_CODE_FEATURE_BLOCKING_ENV_KEYS.includes(
key as (typeof CLAUDE_CODE_FEATURE_BLOCKING_ENV_KEYS)[number]
)
) {
continue;
}
@@ -147,3 +180,12 @@ export function stripSubcommandBlockingEnv(env: NodeJS.ProcessEnv): NodeJS.Proce
}
return out;
}
/**
* Return a shallow copy of `env` with subcommand-blocking telemetry vars
* removed. Caller is responsible for only invoking this when args are a
* Claude subcommand invocation.
*/
export function stripSubcommandBlockingEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
return stripClaudeCodeFeatureBlockingEnv(env);
}
+11 -5
View File
@@ -11,6 +11,8 @@ import { getWebSearchHookEnv } from './websearch-manager';
import { wireChildProcessSignals } from './signal-forwarder';
import {
isClaudeSubcommandInvocation,
stripClaudeCodeFeatureBlockingEnv,
stripClaudeSubcommandSessionArgs,
stripSubcommandBlockingEnv,
} from './claude-subcommand-detector';
@@ -288,15 +290,19 @@ export function execClaude(
? stripAnthropicRoutingEnv(mergedEnv, envVars ?? undefined)
: mergedEnv;
const effectiveArgs = isClaudeSubcommandInvocation(args)
? stripClaudeSubcommandSessionArgs(args)
: args;
// Strip Claude Code nested session guard env var to allow CCS delegation
// (Claude Code v2.1.39+ sets CLAUDECODE to detect nested sessions)
let env = stripClaudeCodeEnv(effectiveMergedEnv);
let env = stripClaudeCodeFeatureBlockingEnv(stripClaudeCodeEnv(effectiveMergedEnv));
// For Claude subcommand invocations (`agents`, `mcp`, `doctor`, ...) strip
// telemetry-disable env vars that cause upstream Claude Code to fall back
// to non-interactive list mode instead of opening the subcommand TUI.
// Issue #1218.
if (isClaudeSubcommandInvocation(args)) {
if (isClaudeSubcommandInvocation(effectiveArgs)) {
env = stripSubcommandBlockingEnv(env);
}
@@ -316,7 +322,7 @@ export function execClaude(
if (isPowerShellScript) {
child = spawn(
'powershell.exe',
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', claudeCli, ...args],
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', claudeCli, ...effectiveArgs],
{
stdio: 'inherit',
windowsHide: true,
@@ -325,7 +331,7 @@ export function execClaude(
);
} else if (needsShell) {
// When shell needed: concatenate into string to avoid DEP0190 warning
const cmdString = [claudeCli, ...args].map(escapeShellArg).join(' ');
const cmdString = [claudeCli, ...effectiveArgs].map(escapeShellArg).join(' ');
child = spawn(cmdString, {
stdio: 'inherit',
windowsHide: true,
@@ -334,7 +340,7 @@ export function execClaude(
});
} else {
// When no shell needed: use array form (faster, no shell overhead)
child = spawn(claudeCli, args, {
child = spawn(claudeCli, effectiveArgs, {
stdio: 'inherit',
windowsHide: true,
env,
@@ -0,0 +1,57 @@
import { describe, it, expect } from 'bun:test';
import {
hasClaudeBackgroundWorkerUsingBaseUrlInProcessList,
shouldKeepSessionProxiesAlive,
} from '../../../src/cliproxy/executor/session-bridge';
describe('session bridge background proxy keepalive', () => {
it('keeps per-session proxies alive after a clean Claude exit when a bg worker inherited the base URL', () => {
expect(
shouldKeepSessionProxiesAlive({
code: 0,
signal: null,
backgroundKeepAliveBaseUrl: 'http://127.0.0.1:64314/api/provider/codex',
hasBackgroundWorkerUsingBaseUrl: () => true,
})
).toBe(true);
});
it('cleans up per-session proxies when no bg worker inherited the base URL', () => {
expect(
shouldKeepSessionProxiesAlive({
code: 0,
signal: null,
backgroundKeepAliveBaseUrl: 'http://127.0.0.1:64314/api/provider/codex',
hasBackgroundWorkerUsingBaseUrl: () => false,
})
).toBe(false);
});
it('cleans up per-session proxies for signaled Claude exits', () => {
expect(
shouldKeepSessionProxiesAlive({
code: null,
signal: 'SIGTERM',
backgroundKeepAliveBaseUrl: 'http://127.0.0.1:64314/api/provider/codex',
hasBackgroundWorkerUsingBaseUrl: () => true,
})
).toBe(false);
});
it('detects Claude bg workers that inherited the exact base URL', () => {
const baseUrl = 'http://127.0.0.1:64314/api/provider/codex';
const processList = `
123 /opt/claude/claude.exe --bg-spare /tmp/claim.sock ANTHROPIC_BASE_URL=${baseUrl} ANTHROPIC_AUTH_TOKEN=ccs-internal-managed
456 /opt/claude/claude.exe --bg-spare /tmp/claim.sock ANTHROPIC_BASE_URL=http://127.0.0.1:62075/api/provider/codex
789 /opt/claude/claude.exe --print ANTHROPIC_BASE_URL=${baseUrl}
`;
expect(hasClaudeBackgroundWorkerUsingBaseUrlInProcessList(processList, baseUrl)).toBe(true);
expect(
hasClaudeBackgroundWorkerUsingBaseUrlInProcessList(
processList,
'http://127.0.0.1:65535/api/provider/codex'
)
).toBe(false);
});
});
@@ -1,5 +1,9 @@
import { describe, expect, it } from 'bun:test';
import { isClaudeSubcommandInvocation } from '../../../src/utils/claude-subcommand-detector';
import {
isClaudeSubcommandInvocation,
stripClaudeCodeFeatureBlockingEnv,
stripClaudeSubcommandSessionArgs,
} from '../../../src/utils/claude-subcommand-detector';
import { appendThirdPartyWebSearchToolArgs } from '../../../src/utils/websearch/claude-tool-args';
import { appendThirdPartyImageAnalysisToolArgs } from '../../../src/utils/image-analysis/claude-tool-args';
import { appendBrowserToolArgs } from '../../../src/utils/browser/claude-tool-args';
@@ -36,6 +40,14 @@ describe('isClaudeSubcommandInvocation', () => {
it('skips past value-taking flags before the positional', () => {
expect(isClaudeSubcommandInvocation(['--model', 'sonnet', 'agents'])).toBe(true);
expect(isClaudeSubcommandInvocation(['--settings', '/tmp/s.json', 'doctor'])).toBe(true);
expect(
isClaudeSubcommandInvocation([
'--dangerously-skip-permissions',
'--teammate-mode',
'in-process',
'agents',
])
).toBe(true);
});
it('does not treat a flag value matching a subcommand name as a subcommand', () => {
@@ -57,6 +69,59 @@ describe('isClaudeSubcommandInvocation', () => {
});
});
describe('stripClaudeCodeFeatureBlockingEnv', () => {
it('removes telemetry disable env that blocks Claude Code background features', () => {
const env = stripClaudeCodeFeatureBlockingEnv({
DISABLE_TELEMETRY: '1',
DISABLE_BUG_COMMAND: '1',
DISABLE_ERROR_REPORTING: '1',
ANTHROPIC_MODEL: 'gpt-5.5',
});
expect(env.DISABLE_TELEMETRY).toBeUndefined();
expect(env.DISABLE_BUG_COMMAND).toBe('1');
expect(env.DISABLE_ERROR_REPORTING).toBe('1');
expect(env.ANTHROPIC_MODEL).toBe('gpt-5.5');
});
});
describe('stripClaudeSubcommandSessionArgs', () => {
it('removes session-only flags before a Claude subcommand', () => {
expect(
stripClaudeSubcommandSessionArgs([
'--dangerously-skip-permissions',
'--teammate-mode',
'in-process',
'agents',
])
).toEqual(['agents']);
});
it('removes session-only flags after a Claude subcommand while preserving subcommand flags', () => {
expect(
stripClaudeSubcommandSessionArgs([
'agents',
'--dangerously-skip-permissions',
'--teammate-mode',
'in-process',
'--setting-sources',
'user',
])
).toEqual(['agents', '--setting-sources', 'user']);
});
it('leaves non-subcommand interactive launches unchanged', () => {
expect(
stripClaudeSubcommandSessionArgs([
'--dangerously-skip-permissions',
'--teammate-mode',
'in-process',
'fix the bug',
])
).toEqual(['--dangerously-skip-permissions', '--teammate-mode', 'in-process', 'fix the bug']);
});
});
describe('subcommand passthrough — injectors short-circuit', () => {
it('appendThirdPartyWebSearchToolArgs returns args unchanged for subcommand invocations', () => {
expect(appendThirdPartyWebSearchToolArgs(['agents'])).toEqual(['agents']);