// These side-effects must run before all other imports: // 1. profileCheckpoint marks entry before heavy module evaluation begins // 2. startMdmRawRead fires MDM subprocesses (plutil/reg query) so they run in // parallel with the remaining ~135ms of imports below // 3. startKeychainPrefetch fires both macOS keychain reads (OAuth + legacy API // key) in parallel — isRemoteManagedSettingsEligible() otherwise reads them // sequentially via sync spawn inside applySafeConfigEnvironmentVariables() // (~65ms on every macOS startup) import { profileCheckpoint, profileReport } from "./utils/startupProfiler.js"; // eslint-disable-next-line custom-rules/no-top-level-side-effects profileCheckpoint("main_tsx_entry"); import { startMdmRawRead } from "./utils/settings/mdm/rawRead.js"; // eslint-disable-next-line custom-rules/no-top-level-side-effects startMdmRawRead(); import { ensureKeychainPrefetchCompleted, startKeychainPrefetch, } from "./utils/secureStorage/keychainPrefetch.js"; // eslint-disable-next-line custom-rules/no-top-level-side-effects startKeychainPrefetch(); import { feature } from "bun:bundle"; import { Command as CommanderCommand, InvalidArgumentError, Option, } from '@commander-js/extra-typings' import chalk from 'chalk' import { readFileSync } from 'fs' import mapValues from 'lodash-es/mapValues.js' import pickBy from 'lodash-es/pickBy.js' import uniqBy from 'lodash-es/uniqBy.js' import React from 'react' import { getOauthConfig } from './constants/oauth.js' import { getRemoteSessionUrl } from './constants/product.js' import { getSystemContext, getUserContext } from './context.js' import { init, initializeTelemetryAfterTrust } from './entrypoints/init.js' import { addToHistory } from './history.js' import type { Root } from '@anthropic/ink' import { launchRepl } from './replLauncher.js' import { hasGrowthBookEnvOverride, initializeGrowthBook, refreshGrowthBookAfterAuthChange, } from "./services/analytics/growthbook.js"; import { fetchBootstrapData } from "./services/api/bootstrap.js"; import { type DownloadResult, downloadSessionFiles, type FilesApiConfig, parseFileSpecs, } from "./services/api/filesApi.js"; import { prefetchPassesEligibility } from "./services/api/referral.js"; import type { McpSdkServerConfig, McpServerConfig, ScopedMcpServerConfig, } from "./services/mcp/types.js"; import { isPolicyAllowed, loadPolicyLimits, refreshPolicyLimits, waitForPolicyLimitsToLoad, } from "./services/policyLimits/index.js"; import { loadRemoteManagedSettings, refreshRemoteManagedSettings, } from "./services/remoteManagedSettings/index.js"; import type { ToolInputJSONSchema } from "./Tool.js"; import { createSyntheticOutputTool, isSyntheticOutputToolEnabled, } from "@claude-code-best/builtin-tools/tools/SyntheticOutputTool/SyntheticOutputTool.js"; import { getTools } from "./tools.js"; import { canUserConfigureAdvisor, getInitialAdvisorSetting, isAdvisorEnabled, isValidAdvisorModel, modelSupportsAdvisor, } from "./utils/advisor.js"; import { isAgentSwarmsEnabled } from "./utils/agentSwarmsEnabled.js"; import { count, uniq } from "./utils/array.js"; import { installAsciicastRecorder } from "./utils/asciicast.js"; import { getSubscriptionType, isClaudeAISubscriber, prefetchAwsCredentialsAndBedRockInfoIfSafe, prefetchGcpCredentialsIfSafe, validateForceLoginOrg, } from "./utils/auth.js"; import { checkHasTrustDialogAccepted, getGlobalConfig, getRemoteControlAtStartup, isAutoUpdaterDisabled, saveGlobalConfig, } from "./utils/config.js"; import { seedEarlyInput, stopCapturingEarlyInput } from "./utils/earlyInput.js"; import { getInitialEffortSetting, parseEffortValue } from "./utils/effort.js"; import { getInitialFastModeSetting, isFastModeEnabled, prefetchFastModeStatus, resolveFastModeStatusFromCache, } from "./utils/fastMode.js"; import { applyConfigEnvironmentVariables } from "./utils/managedEnv.js"; import { createSystemMessage, createUserMessage } from "./utils/messages.js"; import { getPlatform } from "./utils/platform.js"; import { getBaseRenderOptions } from "./utils/renderOptions.js"; import { getSessionIngressAuthToken } from "./utils/sessionIngressAuth.js"; import { settingsChangeDetector } from "./utils/settings/changeDetector.js"; import { skillChangeDetector } from "./utils/skills/skillChangeDetector.js"; import { jsonParse, writeFileSync_DEPRECATED } from "./utils/slowOperations.js"; import { computeInitialTeamContext } from "./utils/swarm/reconnection.js"; import { initializeWarningHandler } from "./utils/warningHandler.js"; import { isWorktreeModeEnabled } from "./utils/worktreeModeEnabled.js"; // Lazy require to avoid circular dependency: teammate.ts -> AppState.tsx -> ... -> main.tsx /* eslint-disable @typescript-eslint/no-require-imports */ const getTeammateUtils = () => require("./utils/teammate.js") as typeof import("./utils/teammate.js"); const getTeammatePromptAddendum = () => require("./utils/swarm/teammatePromptAddendum.js") as typeof import("./utils/swarm/teammatePromptAddendum.js"); const getTeammateModeSnapshot = () => require("./utils/swarm/backends/teammateModeSnapshot.js") as typeof import("./utils/swarm/backends/teammateModeSnapshot.js"); /* eslint-enable @typescript-eslint/no-require-imports */ // Dead code elimination: conditional import for COORDINATOR_MODE /* eslint-disable @typescript-eslint/no-require-imports */ const coordinatorModeModule = feature("COORDINATOR_MODE") ? (require("./coordinator/coordinatorMode.js") as typeof import("./coordinator/coordinatorMode.js")) : null; /* eslint-enable @typescript-eslint/no-require-imports */ // Dead code elimination: conditional import for KAIROS (assistant mode) /* eslint-disable @typescript-eslint/no-require-imports */ const assistantModule = feature("KAIROS") ? (require("./assistant/index.js") as typeof import("./assistant/index.js")) : null; const kairosGate = feature("KAIROS") ? (require("./assistant/gate.js") as typeof import("./assistant/gate.js")) : null; import { relative, resolve } from "path"; import { isAnalyticsDisabled } from "src/services/analytics/config.js"; import { getFeatureValue_CACHED_MAY_BE_STALE } from "src/services/analytics/growthbook.js"; import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, } from 'src/services/analytics/index.js' import { initializeAnalyticsGates } from 'src/services/analytics/sink.js' import { getOriginalCwd, setAdditionalDirectoriesForClaudeMd, setIsRemoteMode, setMainLoopModelOverride, setMainThreadAgentType, setTeleportedSessionInfo, } from "./bootstrap/state.js"; import { filterCommandsForRemoteMode, getCommands } from "./commands.js"; import type { StatsStore } from "./context/stats.js"; import { launchAssistantInstallWizard, launchAssistantSessionChooser, launchInvalidSettingsDialog, launchResumeChooser, launchSnapshotUpdateDialog, launchTeleportRepoMismatchDialog, launchTeleportResumeWrapper, } from './dialogLaunchers.js' import { SHOW_CURSOR } from '@anthropic/ink' import { exitWithError, exitWithMessage, getRenderContext, renderAndRun, showSetupScreens, } from "./interactiveHelpers.js"; import { initBuiltinPlugins } from "./plugins/bundled/index.js"; /* eslint-enable @typescript-eslint/no-require-imports */ import { checkQuotaStatus } from "./services/claudeAiLimits.js"; import { getMcpToolsCommandsAndResources, prefetchAllMcpResources, } from "./services/mcp/client.js"; import { VALID_INSTALLABLE_SCOPES, VALID_UPDATE_SCOPES, } from "./services/plugins/pluginCliCommands.js"; import { initBundledSkills } from "./skills/bundled/index.js"; import type { AgentColorName } from "@claude-code-best/builtin-tools/tools/AgentTool/agentColorManager.js"; import { getActiveAgentsFromList, getAgentDefinitionsWithOverrides, isBuiltInAgent, isCustomAgent, parseAgentsFromJson, } from "@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js"; import type { LogOption } from "./types/logs.js"; import type { Message as MessageType } from "./types/message.js"; import { CLAUDE_IN_CHROME_SKILL_HINT, CLAUDE_IN_CHROME_SKILL_HINT_WITH_WEBBROWSER, } from "./utils/claudeInChrome/prompt.js"; import { setupClaudeInChrome, shouldAutoEnableClaudeInChrome, shouldEnableClaudeInChrome, } from "./utils/claudeInChrome/setup.js"; import { getContextWindowForModel } from "./utils/context.js"; import { loadConversationForResume } from "./utils/conversationRecovery.js"; import { buildDeepLinkBanner } from "./utils/deepLink/banner.js"; import { hasNodeOption, isBareMode, isEnvTruthy, isInProtectedNamespace, } from "./utils/envUtils.js"; import { refreshExampleCommands } from "./utils/exampleCommands.js"; import type { FpsMetrics } from "./utils/fpsTracker.js"; import { getWorktreePaths } from "./utils/getWorktreePaths.js"; import { findGitRoot, getBranch, getIsGit, getWorktreeCount, } from "./utils/git.js"; import { getGhAuthStatus } from "./utils/github/ghAuthStatus.js"; import { safeParseJSON } from "./utils/json.js"; import { logError } from "./utils/log.js"; import { getModelDeprecationWarning } from "./utils/model/deprecation.js"; import { getDefaultMainLoopModel, getUserSpecifiedModelSetting, normalizeModelStringForAPI, parseUserSpecifiedModel, } from "./utils/model/model.js"; import { ensureModelStringsInitialized } from "./utils/model/modelStrings.js"; import { PERMISSION_MODES } from "./utils/permissions/PermissionMode.js"; import { getAutoModeEnabledStateIfCached, initializeToolPermissionContext, initialPermissionModeFromCLI, isDefaultPermissionModeAuto, parseToolListFromCLI, removeDangerousPermissions, stripDangerousPermissionsForAutoMode, verifyAutoModeGateAccess, } from "./utils/permissions/permissionSetup.js"; import { cleanupOrphanedPluginVersionsInBackground } from "./utils/plugins/cacheUtils.js"; import { initializeVersionedPlugins } from "./utils/plugins/installedPluginsManager.js"; import { getManagedPluginNames } from "./utils/plugins/managedPlugins.js"; import { getGlobExclusionsForPluginCache } from "./utils/plugins/orphanedPluginFilter.js"; import { getPluginSeedDirs } from "./utils/plugins/pluginDirectories.js"; import { countFilesRoundedRg } from "./utils/ripgrep.js"; import { processSessionStartHooks, processSetupHooks, } from "./utils/sessionStart.js"; import { cacheSessionTitle, getSessionIdFromLog, loadTranscriptFromFile, saveAgentSetting, saveMode, searchSessionsByCustomTitle, sessionIdExists, } from "./utils/sessionStorage.js"; import { ensureMdmSettingsLoaded } from "./utils/settings/mdm/settings.js"; import { getInitialSettings, getManagedSettingsKeysForLogging, getSettingsForSource, getSettingsWithErrors, } from "./utils/settings/settings.js"; import { resetSettingsCache } from "./utils/settings/settingsCache.js"; import type { ValidationError } from "./utils/settings/validation.js"; import { DEFAULT_TASKS_MODE_TASK_LIST_ID, TASK_STATUSES, } from "./utils/tasks.js"; import { logPluginLoadErrors, logPluginsEnabledForSession, } from "./utils/telemetry/pluginTelemetry.js"; import { logSkillsLoaded } from "./utils/telemetry/skillLoadedEvent.js"; import { generateTempFilePath } from "./utils/tempfile.js"; import { validateUuid } from "./utils/uuid.js"; // Plugin startup checks are now handled non-blockingly in REPL.tsx import { registerMcpAddCommand } from "src/commands/mcp/addCommand.js"; import { registerMcpXaaIdpCommand } from "src/commands/mcp/xaaIdpCommand.js"; import { logPermissionContextForAnts } from "src/services/internalLogging.js"; import { fetchClaudeAIMcpConfigsIfEligible } from "src/services/mcp/claudeai.js"; import { clearServerCache } from "src/services/mcp/client.js"; import { areMcpConfigsAllowedWithEnterpriseMcpConfig, dedupClaudeAiMcpServers, doesEnterpriseMcpConfigExist, filterMcpServersByPolicy, getClaudeCodeMcpConfigs, getMcpServerSignature, parseMcpConfig, parseMcpConfigFromFilePath, } from "src/services/mcp/config.js"; import { excludeCommandsByServer, excludeResourcesByServer, } from "src/services/mcp/utils.js"; import { isXaaEnabled } from "src/services/mcp/xaaIdpLogin.js"; import { getRelevantTips } from "src/services/tips/tipRegistry.js"; import { logContextMetrics } from "src/utils/api.js"; import { CLAUDE_IN_CHROME_MCP_SERVER_NAME, isClaudeInChromeMCPServer, } from "src/utils/claudeInChrome/common.js"; import { registerCleanup } from "src/utils/cleanupRegistry.js"; import { eagerParseCliFlag } from "src/utils/cliArgs.js"; import { createEmptyAttributionState } from "src/utils/commitAttribution.js"; import { countConcurrentSessions, registerSession, updateSessionName, } from "src/utils/concurrentSessions.js"; import { getCwd } from "src/utils/cwd.js"; import { logForDebugging, setHasFormattedOutput } from "src/utils/debug.js"; import { errorMessage, getErrnoCode, isENOENT, TeleportOperationError, toError, } from "src/utils/errors.js"; import { getFsImplementation, safeResolvePath, } from "src/utils/fsOperations.js"; import { gracefulShutdown, gracefulShutdownSync, } from "src/utils/gracefulShutdown.js"; import { setAllHookEventsEnabled } from "src/utils/hooks/hookEvents.js"; import { refreshModelCapabilities } from "src/utils/model/modelCapabilities.js"; import { peekForStdinData, writeToStderr } from "src/utils/process.js"; import { setCwd } from "src/utils/Shell.js"; import { type ProcessedResume, processResumedConversation, } from "src/utils/sessionRestore.js"; import { parseSettingSourcesFlag } from "src/utils/settings/constants.js"; import { plural } from "src/utils/stringUtils.js"; import { type ChannelEntry, getInitialMainLoopModel, getIsNonInteractiveSession, getSdkBetas, getSessionId, getUserMsgOptIn, setAllowedChannels, setAllowedSettingSources, setChromeFlagOverride, setClientType, setCwdState, setDirectConnectServerUrl, setFlagSettingsPath, setInitialMainLoopModel, setInlinePlugins, setIsInteractive, setKairosActive, setOriginalCwd, setQuestionPreviewFormat, setSdkBetas, setSessionBypassPermissionsMode, setSessionPersistenceDisabled, setSessionSource, setUserMsgOptIn, switchSession, } from "./bootstrap/state.js"; /* eslint-disable @typescript-eslint/no-require-imports */ const autoModeStateModule = feature("TRANSCRIPT_CLASSIFIER") ? (require("./utils/permissions/autoModeState.js") as typeof import("./utils/permissions/autoModeState.js")) : null; // TeleportRepoMismatchDialog, TeleportResumeWrapper dynamically imported at call sites import { migrateBypassPermissionsAcceptedToSettings } from "./migrations/migrateBypassPermissionsAcceptedToSettings.js"; import { migrateEnableAllProjectMcpServersToSettings } from "./migrations/migrateEnableAllProjectMcpServersToSettings.js"; import { migrateFennecToOpus } from "./migrations/migrateFennecToOpus.js"; import { migrateLegacyOpusToCurrent } from "./migrations/migrateLegacyOpusToCurrent.js"; import { migrateOpusToOpus1m } from "./migrations/migrateOpusToOpus1m.js"; import { migrateReplBridgeEnabledToRemoteControlAtStartup } from "./migrations/migrateReplBridgeEnabledToRemoteControlAtStartup.js"; import { migrateSonnet1mToSonnet45 } from "./migrations/migrateSonnet1mToSonnet45.js"; import { migrateSonnet45ToSonnet46 } from "./migrations/migrateSonnet45ToSonnet46.js"; import { resetAutoModeOptInForDefaultOffer } from "./migrations/resetAutoModeOptInForDefaultOffer.js"; import { resetProToOpusDefault } from "./migrations/resetProToOpusDefault.js"; import { createRemoteSessionConfig } from "./remote/RemoteSessionManager.js"; /* eslint-enable @typescript-eslint/no-require-imports */ // teleportWithProgress dynamically imported at call site import { createDirectConnectSession, DirectConnectError, } from "./server/createDirectConnectSession.js"; import { initializeLspServerManager } from "./services/lsp/manager.js"; import { shouldEnablePromptSuggestion } from "./services/PromptSuggestion/promptSuggestion.js"; import { type AppState, getDefaultAppState, IDLE_SPECULATION_STATE, } from "./state/AppStateStore.js"; import { onChangeAppState } from "./state/onChangeAppState.js"; import { createStore } from "./state/store.js"; import { asSessionId } from "./types/ids.js"; import { filterAllowedSdkBetas } from "./utils/betas.js"; import { isInBundledMode, isRunningWithBun } from "./utils/bundledMode.js"; import { logForDiagnosticsNoPII } from "./utils/diagLogs.js"; import { filterExistingPaths, getKnownPathsForRepo, } from "./utils/githubRepoPathMapping.js"; import { clearPluginCache, loadAllPluginsCacheOnly, } from "./utils/plugins/pluginLoader.js"; import { migrateChangelogFromConfig } from "./utils/releaseNotes.js"; import { SandboxManager } from "./utils/sandbox/sandbox-adapter.js"; import { fetchSession, prepareApiRequest } from "./utils/teleport/api.js"; import { checkOutTeleportedSessionBranch, processMessagesForTeleportResume, teleportToRemoteWithErrorHandling, validateGitState, validateSessionRepository, } from "./utils/teleport.js"; import { shouldEnableThinkingByDefault, type ThinkingConfig, } from './utils/thinking.js' import { initUser, resetUserCache } from './utils/user.js' import { getTmuxInstallInstructions, isTmuxAvailable, parsePRReference, } from "./utils/worktree.js"; // eslint-disable-next-line custom-rules/no-top-level-side-effects profileCheckpoint("main_tsx_imports_loaded"); /** * Log managed settings keys to Statsig for analytics. * This is called after init() completes to ensure settings are loaded * and environment variables are applied before model resolution. */ function logManagedSettings(): void { try { const policySettings = getSettingsForSource("policySettings"); if (policySettings) { const allKeys = getManagedSettingsKeysForLogging(policySettings); logEvent("tengu_managed_settings_loaded", { keyCount: allKeys.length, keys: allKeys.join( ",", ) as unknown as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }); } } catch { // Silently ignore errors - this is just for analytics } } // Check if running in debug/inspection mode function isBeingDebugged() { const isBun = isRunningWithBun(); // Check for inspect flags in process arguments (including all variants) const hasInspectArg = process.execArgv.some((arg) => { if (isBun) { // Note: Bun has an issue with single-file executables where application arguments // from process.argv leak into process.execArgv (similar to https://github.com/oven-sh/bun/issues/11673) // This breaks use of --debug mode if we omit this branch // We're fine to skip that check, because Bun doesn't support Node.js legacy --debug or --debug-brk flags return /--inspect(-brk)?/.test(arg); } else { // In Node.js, check for both --inspect and legacy --debug flags return /--inspect(-brk)?|--debug(-brk)?/.test(arg); } }); // Check if NODE_OPTIONS contains inspect flags const hasInspectEnv = process.env.NODE_OPTIONS && /--inspect(-brk)?|--debug(-brk)?/.test(process.env.NODE_OPTIONS); // Check if inspector is available and active (indicates debugging) try { // Dynamic import would be better but is async - use global object instead // eslint-disable-next-line @typescript-eslint/no-explicit-any const inspector = (global as any).require("inspector"); const hasInspectorUrl = !!inspector.url(); return hasInspectorUrl || hasInspectArg || hasInspectEnv; } catch { // Ignore error and fall back to argument detection return hasInspectArg || hasInspectEnv; } } /** * Per-session skill/plugin telemetry. Called from both the interactive path * and the headless -p path (before runHeadless) — both go through * main.tsx but branch before the interactive startup path, so it needs two * call sites here rather than one here + one in QueryEngine. */ function logSessionTelemetry(): void { const model = parseUserSpecifiedModel( getInitialMainLoopModel() ?? getDefaultMainLoopModel(), ); void logSkillsLoaded( getCwd(), getContextWindowForModel(model, getSdkBetas()), ); void loadAllPluginsCacheOnly() .then(({ enabled, errors }) => { const managedNames = getManagedPluginNames(); logPluginsEnabledForSession( enabled, managedNames, getPluginSeedDirs(), ); logPluginLoadErrors(errors, managedNames); }) .catch((err) => logError(err)); } function getCertEnvVarTelemetry(): Record { const result: Record = {}; if (process.env.NODE_EXTRA_CA_CERTS) { result.has_node_extra_ca_certs = true; } if (process.env.CLAUDE_CODE_CLIENT_CERT) { result.has_client_cert = true; } if (hasNodeOption("--use-system-ca")) { result.has_use_system_ca = true; } if (hasNodeOption("--use-openssl-ca")) { result.has_use_openssl_ca = true; } return result; } async function logStartupTelemetry(): Promise { if (isAnalyticsDisabled()) return; const [isGit, worktreeCount, ghAuthStatus] = await Promise.all([ getIsGit(), getWorktreeCount(), getGhAuthStatus(), ]); logEvent("tengu_startup_telemetry", { is_git: isGit, worktree_count: worktreeCount, gh_auth_status: ghAuthStatus as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, sandbox_enabled: SandboxManager.isSandboxingEnabled(), are_unsandboxed_commands_allowed: SandboxManager.areUnsandboxedCommandsAllowed(), is_auto_bash_allowed_if_sandbox_enabled: SandboxManager.isAutoAllowBashIfSandboxedEnabled(), auto_updater_disabled: isAutoUpdaterDisabled(), prefers_reduced_motion: getInitialSettings().prefersReducedMotion ?? false, ...getCertEnvVarTelemetry(), }); } // @[MODEL LAUNCH]: Consider any migrations you may need for model strings. See migrateSonnet1mToSonnet45.ts for an example. // Bump this when adding a new sync migration so existing users re-run the set. const CURRENT_MIGRATION_VERSION = 11; function runMigrations(): void { if (getGlobalConfig().migrationVersion !== CURRENT_MIGRATION_VERSION) { migrateBypassPermissionsAcceptedToSettings(); migrateEnableAllProjectMcpServersToSettings(); resetProToOpusDefault(); migrateSonnet1mToSonnet45(); migrateLegacyOpusToCurrent(); migrateSonnet45ToSonnet46(); migrateOpusToOpus1m(); migrateReplBridgeEnabledToRemoteControlAtStartup(); if (feature("TRANSCRIPT_CLASSIFIER")) { resetAutoModeOptInForDefaultOffer(); } if (process.env.USER_TYPE === "ant") { migrateFennecToOpus(); } saveGlobalConfig((prev) => prev.migrationVersion === CURRENT_MIGRATION_VERSION ? prev : { ...prev, migrationVersion: CURRENT_MIGRATION_VERSION }, ); } // Async migration - fire and forget since it's non-blocking migrateChangelogFromConfig().catch(() => { // Silently ignore migration errors - will retry on next startup }); } /** * Prefetch system context (including git status) only when it's safe to do so. * Git commands can execute arbitrary code via hooks and config (e.g., core.fsmonitor, * diff.external), so we must only run them after trust is established or in * non-interactive mode where trust is implicit. */ function prefetchSystemContextIfSafe(): void { const isNonInteractiveSession = getIsNonInteractiveSession(); // In non-interactive mode (--print), trust dialog is skipped and // execution is considered trusted (as documented in help text) if (isNonInteractiveSession) { logForDiagnosticsNoPII( "info", "prefetch_system_context_non_interactive", ); void getSystemContext(); return; } // In interactive mode, only prefetch if trust has already been established const hasTrust = checkHasTrustDialogAccepted(); if (hasTrust) { logForDiagnosticsNoPII("info", "prefetch_system_context_has_trust"); void getSystemContext(); } else { logForDiagnosticsNoPII( "info", "prefetch_system_context_skipped_no_trust", ); } // Otherwise, don't prefetch - wait for trust to be established first } /** * Start background prefetches and housekeeping that are NOT needed before first render. * These are deferred from setup() to reduce event loop contention and child process * spawning during the critical startup path. * Call this after the REPL has been rendered. */ export function startDeferredPrefetches(): void { // This function runs after first render, so it doesn't block the initial paint. // However, the spawned processes and async work still contend for CPU and event // loop time, which skews startup benchmarks (CPU profiles, time-to-first-render // measurements). Skip all of it when we're only measuring startup performance. if ( isEnvTruthy(process.env.CLAUDE_CODE_EXIT_AFTER_FIRST_RENDER) || // --bare: skip ALL prefetches. These are cache-warms for the REPL's // first-turn responsiveness (initUser, getUserContext, tips, countFiles, // modelCapabilities, change detectors). Scripted -p calls don't have a // "user is typing" window to hide this work in — it's pure overhead on // the critical path. isBareMode() ) { return; } // Process-spawning prefetches (consumed at first API call, user is still typing) void initUser(); void getUserContext(); prefetchSystemContextIfSafe(); void getRelevantTips(); if ( isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) && !isEnvTruthy(process.env.CLAUDE_CODE_SKIP_BEDROCK_AUTH) ) { void prefetchAwsCredentialsAndBedRockInfoIfSafe(); } if ( isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) && !isEnvTruthy(process.env.CLAUDE_CODE_SKIP_VERTEX_AUTH) ) { void prefetchGcpCredentialsIfSafe(); } void countFilesRoundedRg(getCwd(), AbortSignal.timeout(3000), []); // Analytics and feature flag initialization void initializeAnalyticsGates(); void refreshModelCapabilities(); // File change detectors deferred from init() to unblock first render void settingsChangeDetector.initialize(); if (!isBareMode()) { void skillChangeDetector.initialize(); } // Event loop stall detector — logs when the main thread is blocked >500ms if (process.env.USER_TYPE === "ant") { void import("./utils/eventLoopStallDetector.js").then((m) => m.startEventLoopStallDetector(), ); } } function loadSettingsFromFlag(settingsFile: string): void { try { const trimmedSettings = settingsFile.trim(); const looksLikeJson = trimmedSettings.startsWith("{") && trimmedSettings.endsWith("}"); let settingsPath: string; if (looksLikeJson) { // It's a JSON string - validate and create temp file const parsedJson = safeParseJSON(trimmedSettings); if (!parsedJson) { process.stderr.write( chalk.red("Error: Invalid JSON provided to --settings\n"), ); process.exit(1); } // Create a temporary file and write the JSON to it. // Use a content-hash-based path instead of random UUID to avoid // busting the Anthropic API prompt cache. The settings path ends up // in the Bash tool's sandbox denyWithinAllow list, which is part of // the tool description sent to the API. A random UUID per subprocess // changes the tool description on every query() call, invalidating // the cache prefix and causing a 12x input token cost penalty. // The content hash ensures identical settings produce the same path // across process boundaries (each SDK query() spawns a new process). settingsPath = generateTempFilePath("claude-settings", ".json", { contentHash: trimmedSettings, }); writeFileSync_DEPRECATED(settingsPath, trimmedSettings, "utf8"); } else { // It's a file path - resolve and validate by attempting to read const { resolvedPath: resolvedSettingsPath } = safeResolvePath( getFsImplementation(), settingsFile, ); try { readFileSync(resolvedSettingsPath, "utf8"); } catch (e) { if (isENOENT(e)) { process.stderr.write( chalk.red( `Error: Settings file not found: ${resolvedSettingsPath}\n`, ), ); process.exit(1); } throw e; } settingsPath = resolvedSettingsPath; } setFlagSettingsPath(settingsPath); resetSettingsCache(); } catch (error) { if (error instanceof Error) { logError(error); } process.stderr.write( chalk.red(`Error processing settings: ${errorMessage(error)}\n`), ); process.exit(1); } } function loadSettingSourcesFromFlag(settingSourcesArg: string): void { try { const sources = parseSettingSourcesFlag(settingSourcesArg); setAllowedSettingSources(sources); resetSettingsCache(); } catch (error) { if (error instanceof Error) { logError(error); } process.stderr.write( chalk.red( `Error processing --setting-sources: ${errorMessage(error)}\n`, ), ); process.exit(1); } } /** * Parse and load settings flags early, before init() * This ensures settings are filtered from the start of initialization */ function eagerLoadSettings(): void { profileCheckpoint("eagerLoadSettings_start"); // Parse --settings flag early to ensure settings are loaded before init() const settingsFile = eagerParseCliFlag("--settings"); if (settingsFile) { loadSettingsFromFlag(settingsFile); } // Parse --setting-sources flag early to control which sources are loaded const settingSourcesArg = eagerParseCliFlag("--setting-sources"); if (settingSourcesArg !== undefined) { loadSettingSourcesFromFlag(settingSourcesArg); } profileCheckpoint("eagerLoadSettings_end"); } function initializeEntrypoint(isNonInteractive: boolean): void { // Skip if already set (e.g., by SDK or other entrypoints) if (process.env.CLAUDE_CODE_ENTRYPOINT) { return; } const cliArgs = process.argv.slice(2); // Check for MCP serve command (handle flags before mcp serve, e.g., --debug mcp serve) const mcpIndex = cliArgs.indexOf("mcp"); if (mcpIndex !== -1 && cliArgs[mcpIndex + 1] === "serve") { process.env.CLAUDE_CODE_ENTRYPOINT = "mcp"; return; } if (isEnvTruthy(process.env.CLAUDE_CODE_ACTION)) { process.env.CLAUDE_CODE_ENTRYPOINT = "claude-code-github-action"; return; } // Note: 'local-agent' entrypoint is set by the local agent mode launcher // via CLAUDE_CODE_ENTRYPOINT env var (handled by early return above) // Set based on interactive status process.env.CLAUDE_CODE_ENTRYPOINT = isNonInteractive ? "sdk-cli" : "cli"; } // Set by early argv processing when `claude open ` is detected (interactive mode only) type PendingConnect = { url: string | undefined; authToken: string | undefined; dangerouslySkipPermissions: boolean; }; const _pendingConnect: PendingConnect | undefined = feature("DIRECT_CONNECT") ? { url: undefined, authToken: undefined, dangerouslySkipPermissions: false, } : undefined; // Set by early argv processing when `claude assistant [sessionId]` is detected type PendingAssistantChat = { sessionId?: string; discover: boolean }; const _pendingAssistantChat: PendingAssistantChat | undefined = feature( "KAIROS", ) ? { sessionId: undefined, discover: false } : undefined; // `claude ssh [dir]` — parsed from argv early (same pattern as // DIRECT_CONNECT above) so the main command path can pick it up and hand // the REPL an SSH-backed session instead of a local one. type PendingSSH = { host: string | undefined; cwd: string | undefined; permissionMode: string | undefined; dangerouslySkipPermissions: boolean; /** --local: spawn the child CLI directly, skip ssh/probe/deploy. e2e test mode. */ local: boolean; /** Extra CLI args to forward to the remote CLI on initial spawn (--resume, -c). */ extraCliArgs: string[]; remoteBin: string | undefined; }; const _pendingSSH: PendingSSH | undefined = feature("SSH_REMOTE") ? { host: undefined, cwd: undefined, permissionMode: undefined, dangerouslySkipPermissions: false, local: false, extraCliArgs: [], remoteBin: undefined, } : undefined; export async function main() { profileCheckpoint("main_function_start"); // SECURITY: Prevent Windows from executing commands from current directory // This must be set before ANY command execution to prevent PATH hijacking attacks // See: https://docs.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-searchpathw process.env.NoDefaultCurrentDirectoryInExePath = "1"; // Initialize warning handler early to catch warnings initializeWarningHandler(); process.on("exit", () => { resetCursor(); }); process.on("SIGINT", () => { // In print mode, print.ts registers its own SIGINT handler that aborts // the in-flight query and calls gracefulShutdown; skip here to avoid // preempting it with a synchronous process.exit(). if (process.argv.includes("-p") || process.argv.includes("--print")) { return; } process.exit(0); }); profileCheckpoint("main_warning_handler_initialized"); // Check for cc:// or cc+unix:// URL in argv — rewrite so the main command // handles it, giving the full interactive TUI instead of a stripped-down subcommand. // For headless (-p), we rewrite to the internal `open` subcommand. if (feature("DIRECT_CONNECT")) { const rawCliArgs = process.argv.slice(2); const ccIdx = rawCliArgs.findIndex( (a) => a.startsWith("cc://") || a.startsWith("cc+unix://"), ); if (ccIdx !== -1 && _pendingConnect) { const ccUrl = rawCliArgs[ccIdx]!; const { parseConnectUrl } = await import("./server/parseConnectUrl.js"); const parsed = parseConnectUrl(ccUrl); _pendingConnect.dangerouslySkipPermissions = rawCliArgs.includes( "--dangerously-skip-permissions", ); if (rawCliArgs.includes("-p") || rawCliArgs.includes("--print")) { // Headless: rewrite to internal `open` subcommand const stripped = rawCliArgs.filter((_, i) => i !== ccIdx); const dspIdx = stripped.indexOf( "--dangerously-skip-permissions", ); if (dspIdx !== -1) { stripped.splice(dspIdx, 1); } process.argv = [ process.argv[0]!, process.argv[1]!, "open", ccUrl, ...stripped, ]; } else { // Interactive: strip cc:// URL and flags, run main command _pendingConnect.url = parsed.serverUrl; _pendingConnect.authToken = parsed.authToken; const stripped = rawCliArgs.filter((_, i) => i !== ccIdx); const dspIdx = stripped.indexOf( "--dangerously-skip-permissions", ); if (dspIdx !== -1) { stripped.splice(dspIdx, 1); } process.argv = [ process.argv[0]!, process.argv[1]!, ...stripped, ]; } } } // Handle deep link URIs early — this is invoked by the OS protocol handler // and should bail out before full init since it only needs to parse the URI // and open a terminal. if (feature("LODESTONE")) { const handleUriIdx = process.argv.indexOf("--handle-uri"); if (handleUriIdx !== -1 && process.argv[handleUriIdx + 1]) { const { enableConfigs } = await import("./utils/config.js"); enableConfigs(); const uri = process.argv[handleUriIdx + 1]!; const { handleDeepLinkUri } = await import("./utils/deepLink/protocolHandler.js"); const exitCode = await handleDeepLinkUri(uri); process.exit(exitCode); } // macOS URL handler: when LaunchServices launches our .app bundle, the // URL arrives via Apple Event (not argv). LaunchServices overwrites // __CFBundleIdentifier to the launching bundle's ID, which is a precise // positive signal — cheaper than importing and guessing with heuristics. if ( process.platform === "darwin" && process.env.__CFBundleIdentifier === "com.anthropic.claude-code-url-handler" ) { const { enableConfigs } = await import("./utils/config.js"); enableConfigs(); const { handleUrlSchemeLaunch } = await import("./utils/deepLink/protocolHandler.js"); const urlSchemeResult = await handleUrlSchemeLaunch(); process.exit(urlSchemeResult ?? 1); } } // `claude assistant [sessionId]` — stash and strip so the main // command handles it, giving the full interactive TUI. Position-0 only // (matching the ssh pattern below) — indexOf would false-positive on // `claude -p "explain assistant"`. Root-flag-before-subcommand // (e.g. `--debug assistant`) falls through to the stub, which // prints usage. if (feature("KAIROS") && _pendingAssistantChat) { const rawArgs = process.argv.slice(2); if (rawArgs[0] === "assistant") { const nextArg = rawArgs[1]; if (nextArg && !nextArg.startsWith("-")) { _pendingAssistantChat.sessionId = nextArg; rawArgs.splice(0, 2); // drop 'assistant' and sessionId process.argv = [process.argv[0]!, process.argv[1]!, ...rawArgs]; } else if (!nextArg) { _pendingAssistantChat.discover = true; rawArgs.splice(0, 1); // drop 'assistant' process.argv = [process.argv[0]!, process.argv[1]!, ...rawArgs]; } // else: `claude assistant --help` → fall through to stub } } // `claude ssh [dir]` — strip from argv so the main command handler // runs (full interactive TUI), stash the host/dir for the REPL branch at // ~line 3720 to pick up. Headless (-p) mode not supported in v1: SSH // sessions need the local REPL to drive them (interrupt, permissions). if (feature("SSH_REMOTE") && _pendingSSH) { const rawCliArgs = process.argv.slice(2); // SSH-specific flags can appear before the host positional (e.g. // `ssh --permission-mode auto host /tmp` — standard POSIX flags-before- // positionals). Pull them all out BEFORE checking whether a host was // given, so `claude ssh --permission-mode auto host` and `claude ssh host // --permission-mode auto` are equivalent. The host check below only needs // to guard against `-h`/`--help` (which commander should handle). if (rawCliArgs[0] === "ssh") { const localIdx = rawCliArgs.indexOf("--local"); if (localIdx !== -1) { _pendingSSH.local = true; rawCliArgs.splice(localIdx, 1); } const dspIdx = rawCliArgs.indexOf("--dangerously-skip-permissions"); if (dspIdx !== -1) { _pendingSSH.dangerouslySkipPermissions = true; rawCliArgs.splice(dspIdx, 1); } const pmIdx = rawCliArgs.indexOf("--permission-mode"); if ( pmIdx !== -1 && rawCliArgs[pmIdx + 1] && !rawCliArgs[pmIdx + 1]!.startsWith("-") ) { _pendingSSH.permissionMode = rawCliArgs[pmIdx + 1]; rawCliArgs.splice(pmIdx, 2); } const pmEqIdx = rawCliArgs.findIndex((a) => a.startsWith("--permission-mode="), ); if (pmEqIdx !== -1) { _pendingSSH.permissionMode = rawCliArgs[pmEqIdx]!.split("=")[1]; rawCliArgs.splice(pmEqIdx, 1); } // Forward session-resume + model flags to the remote CLI's initial spawn. // --continue/-c and --resume operate on the REMOTE session history // (which persists under the remote's ~/.claude/projects//). // --model controls which model the remote uses. const extractFlag = ( flag: string, opts: { hasValue?: boolean; as?: string } = {}, ) => { const i = rawCliArgs.indexOf(flag); if (i !== -1) { _pendingSSH.extraCliArgs.push(opts.as ?? flag); const val = rawCliArgs[i + 1]; if (opts.hasValue && val && !val.startsWith("-")) { _pendingSSH.extraCliArgs.push(val); rawCliArgs.splice(i, 2); } else { rawCliArgs.splice(i, 1); } } const eqI = rawCliArgs.findIndex((a) => a.startsWith(`${flag}=`), ); if (eqI !== -1) { _pendingSSH.extraCliArgs.push( opts.as ?? flag, rawCliArgs[eqI]!.slice(flag.length + 1), ); rawCliArgs.splice(eqI, 1); } }; const rbIdx = rawCliArgs.indexOf('--remote-bin'); if (rbIdx !== -1 && rawCliArgs[rbIdx + 1] && !rawCliArgs[rbIdx + 1]!.startsWith('-')) { _pendingSSH.remoteBin = rawCliArgs[rbIdx + 1]; rawCliArgs.splice(rbIdx, 2); } const rbEqIdx = rawCliArgs.findIndex(a => a.startsWith('--remote-bin=')); if (rbEqIdx !== -1) { _pendingSSH.remoteBin = rawCliArgs[rbEqIdx]!.split('=').slice(1).join('='); rawCliArgs.splice(rbEqIdx, 1); } extractFlag("-c", { as: "--continue" }); extractFlag("--continue"); extractFlag("--resume", { hasValue: true }); extractFlag("--model", { hasValue: true }); } // After pre-extraction, any remaining dash-arg at [1] is either -h/--help // (commander handles) or an unknown-to-ssh flag (fall through to commander // so it surfaces a proper error). Only a non-dash arg is the host. if ( rawCliArgs[0] === "ssh" && rawCliArgs[1] && !rawCliArgs[1].startsWith("-") ) { _pendingSSH.host = rawCliArgs[1]; // Optional positional cwd. let consumed = 2; if (rawCliArgs[2] && !rawCliArgs[2].startsWith("-")) { _pendingSSH.cwd = rawCliArgs[2]; consumed = 3; } const rest = rawCliArgs.slice(consumed); // Headless (-p) mode is not supported with SSH in v1 — reject early // so the flag doesn't silently cause local execution. if (rest.includes("-p") || rest.includes("--print")) { process.stderr.write( "Error: headless (-p/--print) mode is not supported with claude ssh\n", ); gracefulShutdownSync(1); return; } // Rewrite argv so the main command sees remaining flags but not `ssh`. process.argv = [process.argv[0]!, process.argv[1]!, ...rest]; } } // Check for -p/--print and --init-only flags early to set isInteractiveSession before init() // This is needed because telemetry initialization calls auth functions that need this flag const cliArgs = process.argv.slice(2); const hasPrintFlag = cliArgs.includes("-p") || cliArgs.includes("--print"); const hasInitOnlyFlag = cliArgs.includes("--init-only"); const hasSdkUrl = cliArgs.some((arg) => arg.startsWith("--sdk-url")); const forceInteractive = isEnvTruthy(process.env.CLAUDE_CODE_FORCE_INTERACTIVE); const isNonInteractive = hasPrintFlag || hasInitOnlyFlag || hasSdkUrl || (!forceInteractive && !process.stdout.isTTY); // Stop capturing early input for non-interactive modes if (isNonInteractive) { stopCapturingEarlyInput(); } // Set simplified tracking fields const isInteractive = !isNonInteractive; setIsInteractive(isInteractive); // Initialize entrypoint based on mode - needs to be set before any event is logged initializeEntrypoint(isNonInteractive); // Determine client type const clientType = (() => { if (isEnvTruthy(process.env.GITHUB_ACTIONS)) return "github-action"; if (process.env.CLAUDE_CODE_ENTRYPOINT === "sdk-ts") return "sdk-typescript"; if (process.env.CLAUDE_CODE_ENTRYPOINT === "sdk-py") return "sdk-python"; if (process.env.CLAUDE_CODE_ENTRYPOINT === "sdk-cli") return "sdk-cli"; if (process.env.CLAUDE_CODE_ENTRYPOINT === "claude-vscode") return "claude-vscode"; if (process.env.CLAUDE_CODE_ENTRYPOINT === "local-agent") return "local-agent"; if (process.env.CLAUDE_CODE_ENTRYPOINT === "claude-desktop") return "claude-desktop"; // Check if session-ingress token is provided (indicates remote session) const hasSessionIngressToken = process.env.CLAUDE_CODE_SESSION_ACCESS_TOKEN || process.env.CLAUDE_CODE_WEBSOCKET_AUTH_FILE_DESCRIPTOR; if ( process.env.CLAUDE_CODE_ENTRYPOINT === "remote" || hasSessionIngressToken ) { return "remote"; } return "cli"; })(); setClientType(clientType); const previewFormat = process.env.CLAUDE_CODE_QUESTION_PREVIEW_FORMAT; if (previewFormat === "markdown" || previewFormat === "html") { setQuestionPreviewFormat(previewFormat); } else if ( !clientType.startsWith("sdk-") && // Desktop and CCR pass previewFormat via toolConfig; when the feature is // gated off they pass undefined — don't override that with markdown. clientType !== "claude-desktop" && clientType !== "local-agent" && clientType !== "remote" ) { setQuestionPreviewFormat("markdown"); } // Tag sessions created via `claude remote-control` so the backend can identify them if (process.env.CLAUDE_CODE_ENVIRONMENT_KIND === "bridge") { setSessionSource("remote-control"); } profileCheckpoint("main_client_type_determined"); // Parse and load settings flags early, before init() eagerLoadSettings(); profileCheckpoint("main_before_run"); await run(); profileCheckpoint("main_after_run"); } async function getInputPrompt( prompt: string, inputFormat: "text" | "stream-json", ): Promise> { if ( !process.stdin.isTTY && // Input hijacking breaks MCP. !process.argv.includes("mcp") ) { if (inputFormat === "stream-json") { return process.stdin; } process.stdin.setEncoding("utf8"); let data = ""; const onData = (chunk: string) => { data += chunk; }; process.stdin.on("data", onData); // If no data arrives in 3s, stop waiting and warn. Stdin is likely an // inherited pipe from a parent that isn't writing (subprocess spawned // without explicit stdin handling). 3s covers slow producers like curl, // jq on large files, python with import overhead. The warning makes // silent data loss visible for the rare producer that's slower still. const timedOut = await peekForStdinData(process.stdin, 3000); process.stdin.off("data", onData); if (timedOut) { process.stderr.write( "Warning: no stdin data received in 3s, proceeding without it. " + "If piping from a slow command, redirect stdin explicitly: < /dev/null to skip, or wait longer.\n", ); } return [prompt, data].filter(Boolean).join("\n"); } return prompt; } async function run(): Promise { profileCheckpoint("run_function_start"); // Create help config that sorts options by long option name. // Commander supports compareOptions at runtime but @commander-js/extra-typings // doesn't include it in the type definitions, so we use Object.assign to add it. function createSortedHelpConfig(): { sortSubcommands: true; sortOptions: true; } { const getOptionSortKey = (opt: Option): string => opt.long?.replace(/^--/, "") ?? opt.short?.replace(/^-/, "") ?? ""; return Object.assign( { sortSubcommands: true, sortOptions: true } as const, { compareOptions: (a: Option, b: Option) => getOptionSortKey(a).localeCompare(getOptionSortKey(b)), }, ); } const program = new CommanderCommand() .configureHelp(createSortedHelpConfig()) .enablePositionalOptions(); profileCheckpoint("run_commander_initialized"); // Use preAction hook to run initialization only when executing a command, // not when displaying help. This avoids the need for env variable signaling. program.hook("preAction", async (thisCommand) => { profileCheckpoint("preAction_start"); // Await async subprocess loads started at module evaluation (lines 12-20). // Nearly free — subprocesses complete during the ~135ms of imports above. // Must resolve before init() which triggers the first settings read // (applySafeConfigEnvironmentVariables → getSettingsForSource('policySettings') // → isRemoteManagedSettingsEligible → sync keychain reads otherwise ~65ms). await Promise.all([ ensureMdmSettingsLoaded(), ensureKeychainPrefetchCompleted(), ]); profileCheckpoint("preAction_after_mdm"); await init(); profileCheckpoint("preAction_after_init"); // process.title on Windows sets the console title directly; on POSIX, // terminal shell integration may mirror the process name to the tab. // After init() so settings.json env can also gate this (gh-4765). if (!isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_TERMINAL_TITLE)) { process.title = "claude"; } // Attach logging sinks so subcommand handlers can use logEvent/logError. // Before PR #11106 logEvent dispatched directly; after, events queue until // a sink attaches. setup() attaches sinks for the default command, but // subcommands (doctor, mcp, plugin, auth) never call setup() and would // silently drop events on process.exit(). Both inits are idempotent. const { initSinks } = await import("./utils/sinks.js"); initSinks(); profileCheckpoint("preAction_after_sinks"); // gh-33508: --plugin-dir is a top-level program option. The default // action reads it from its own options destructure, but subcommands // (plugin list, plugin install, mcp *) have their own actions and // never see it. Wire it up here so getInlinePlugins() works everywhere. // thisCommand.opts() is typed {} here because this hook is attached // before .option('--plugin-dir', ...) in the chain — extra-typings // builds the type as options are added. Narrow with a runtime guard; // the collect accumulator + [] default guarantee string[] in practice. const pluginDir = thisCommand.getOptionValue("pluginDir"); if ( Array.isArray(pluginDir) && pluginDir.length > 0 && pluginDir.every((p) => typeof p === "string") ) { setInlinePlugins(pluginDir); clearPluginCache("preAction: --plugin-dir inline plugins"); } runMigrations(); profileCheckpoint("preAction_after_migrations"); // Load remote managed settings for enterprise customers (non-blocking) // Fails open - if fetch fails, continues without remote settings // Settings are applied via hot-reload when they arrive // Must happen after init() to ensure config reading is allowed void loadRemoteManagedSettings(); void loadPolicyLimits(); profileCheckpoint("preAction_after_remote_settings"); // Load settings sync (non-blocking, fail-open) // CLI: uploads local settings to remote (CCR download is handled by print.ts) if (feature("UPLOAD_USER_SETTINGS")) { void import("./services/settingsSync/index.js").then((m) => m.uploadUserSettingsInBackground(), ); } profileCheckpoint("preAction_after_settings_sync"); }); program .name("claude") .description( `Claude Code - starts an interactive session by default, use -p/--print for non-interactive output`, ) .argument("[prompt]", "Your prompt", String) // Subcommands inherit helpOption via commander's copyInheritedSettings — // setting it once here covers mcp, plugin, auth, and all other subcommands. .helpOption("-h, --help", "Display help for command") .option( "-d, --debug [filter]", 'Enable debug mode with optional category filtering (e.g., "api,hooks" or "!1p,!file")', (_value: string | true) => { // If value is provided, it will be the filter string // If not provided but flag is present, value will be true // The actual filtering is handled in debug.ts by parsing process.argv return true; }, ) .addOption( new Option("--debug-to-stderr", "Enable debug mode (to stderr)") .argParser(Boolean) .hideHelp(), ) .option( "--debug-file ", "Write debug logs to a specific file path (implicitly enables debug mode)", () => true, ) .option( "--verbose", "Override verbose mode setting from config", () => true, ) .option( "-p, --print", "Print response and exit (useful for pipes). Note: The workspace trust dialog is skipped when Claude is run with the -p mode. Only use this flag in directories you trust.", () => true, ) .option( "--bare", "Minimal mode: skip hooks, LSP, plugin sync, attribution, auto-memory, background prefetches, keychain reads, and CLAUDE.md auto-discovery. Sets CLAUDE_CODE_SIMPLE=1. Anthropic auth is strictly ANTHROPIC_API_KEY or apiKeyHelper via --settings (OAuth and keychain are never read). 3P providers (Bedrock/Vertex/Foundry) use their own credentials. Skills still resolve via /skill-name. Explicitly provide context via: --system-prompt[-file], --append-system-prompt[-file], --add-dir (CLAUDE.md dirs), --mcp-config, --settings, --agents, --plugin-dir.", () => true, ) .addOption( new Option( "--init", "Run Setup hooks with init trigger, then continue", ).hideHelp(), ) .addOption( new Option( "--init-only", "Run Setup and SessionStart:startup hooks, then exit", ).hideHelp(), ) .addOption( new Option( "--maintenance", "Run Setup hooks with maintenance trigger, then continue", ).hideHelp(), ) .addOption( new Option( "--output-format ", 'Output format (only works with --print): "text" (default), "json" (single result), or "stream-json" (realtime streaming)', ).choices(["text", "json", "stream-json"]), ) .addOption( new Option( "--json-schema ", "JSON Schema for structured output validation. " + 'Example: {"type":"object","properties":{"name":{"type":"string"}},"required":["name"]}', ).argParser(String), ) .option( "--include-hook-events", "Include all hook lifecycle events in the output stream (only works with --output-format=stream-json)", () => true, ) .option( "--include-partial-messages", "Include partial message chunks as they arrive (only works with --print and --output-format=stream-json)", () => true, ) .addOption( new Option( "--input-format ", 'Input format (only works with --print): "text" (default), or "stream-json" (realtime streaming input)', ).choices(["text", "stream-json"]), ) .option( "--mcp-debug", "[DEPRECATED. Use --debug instead] Enable MCP debug mode (shows MCP server errors)", () => true, ) .option( "--dangerously-skip-permissions", "Bypass all permission checks. Recommended only for sandboxes with no internet access.", () => true, ) .option( "--allow-dangerously-skip-permissions", "Enable bypassing all permission checks as an option, without it being enabled by default. Recommended only for sandboxes with no internet access.", () => true, ) .addOption( new Option( "--thinking ", "Thinking mode: enabled (equivalent to adaptive), disabled", ) .choices(["enabled", "adaptive", "disabled"]) .hideHelp(), ) .addOption( new Option( "--max-thinking-tokens ", "[DEPRECATED. Use --thinking instead for newer models] Maximum number of thinking tokens (only works with --print)", ) .argParser(Number) .hideHelp(), ) .addOption( new Option( "--max-turns ", "Maximum number of agentic turns in non-interactive mode. This will early exit the conversation after the specified number of turns. (only works with --print)", ) .argParser(Number) .hideHelp(), ) .addOption( new Option( "--max-budget-usd ", "Maximum dollar amount to spend on API calls (only works with --print)", ).argParser((value) => { const amount = Number(value); if (isNaN(amount) || amount <= 0) { throw new Error( "--max-budget-usd must be a positive number greater than 0", ); } return amount; }), ) .addOption( new Option( "--task-budget ", "API-side task budget in tokens (output_config.task_budget)", ) .argParser((value) => { const tokens = Number(value); if ( isNaN(tokens) || tokens <= 0 || !Number.isInteger(tokens) ) { throw new Error( "--task-budget must be a positive integer", ); } return tokens; }) .hideHelp(), ) .option( "--replay-user-messages", "Re-emit user messages from stdin back on stdout for acknowledgment (only works with --input-format=stream-json and --output-format=stream-json)", () => true, ) .addOption( new Option( "--enable-auth-status", "Enable auth status messages in SDK mode", ) .default(false) .hideHelp(), ) .option( "--allowedTools, --allowed-tools ", 'Comma or space-separated list of tool names to allow (e.g. "Bash(git:*) Edit")', ) .option( "--tools ", 'Specify the list of available tools from the built-in set. Use "" to disable all tools, "default" to use all tools, or specify tool names (e.g. "Bash,Edit,Read").', ) .option( "--disallowedTools, --disallowed-tools ", 'Comma or space-separated list of tool names to deny (e.g. "Bash(git:*) Edit")', ) .option( "--mcp-config ", "Load MCP servers from JSON files or strings (space-separated)", ) .addOption( new Option( "--permission-prompt-tool ", "MCP tool to use for permission prompts (only works with --print)", ) .argParser(String) .hideHelp(), ) .addOption( new Option( "--system-prompt ", "System prompt to use for the session", ).argParser(String), ) .addOption( new Option( "--system-prompt-file ", "Read system prompt from a file", ) .argParser(String) .hideHelp(), ) .addOption( new Option( "--append-system-prompt ", "Append a system prompt to the default system prompt", ).argParser(String), ) .addOption( new Option( "--append-system-prompt-file ", "Read system prompt from a file and append to the default system prompt", ) .argParser(String) .hideHelp(), ) .addOption( new Option( "--permission-mode ", "Permission mode to use for the session", ) .argParser(String) .choices(PERMISSION_MODES), ) .option( "-c, --continue", "Continue the most recent conversation in the current directory", () => true, ) .option( "-r, --resume [value]", "Resume a conversation by session ID, or open interactive picker with optional search term", (value) => value || true, ) .option( "--fork-session", "When resuming, create a new session ID instead of reusing the original (use with --resume or --continue)", () => true, ) .addOption( new Option( "--prefill ", "Pre-fill the prompt input with text without submitting it", ).hideHelp(), ) .addOption( new Option( "--deep-link-origin", "Signal that this session was launched from a deep link", ).hideHelp(), ) .addOption( new Option( "--deep-link-repo ", "Repo slug the deep link ?repo= parameter resolved to the current cwd", ).hideHelp(), ) .addOption( new Option( "--deep-link-last-fetch ", "FETCH_HEAD mtime in epoch ms, precomputed by the deep link trampoline", ) .argParser((v) => { const n = Number(v); return Number.isFinite(n) ? n : undefined; }) .hideHelp(), ) .option( "--from-pr [value]", "Resume a session linked to a PR by PR number/URL, or open interactive picker with optional search term", (value) => value || true, ) .option( "--no-session-persistence", "Disable session persistence - sessions will not be saved to disk and cannot be resumed (only works with --print)", ) .addOption( new Option( "--resume-session-at ", "When resuming, only messages up to and including the assistant message with (use with --resume in print mode)", ) .argParser(String) .hideHelp(), ) .addOption( new Option( "--rewind-files ", "Restore files to state at the specified user message and exit (requires --resume)", ).hideHelp(), ) // @[MODEL LAUNCH]: Update the example model ID in the --model help text. .option( "--model ", `Model for the current session. Provide an alias for the latest model (e.g. 'sonnet' or 'opus') or a model's full name (e.g. 'claude-sonnet-4-6').`, ) .addOption( new Option( "--effort ", `Effort level for the current session (low, medium, high, max)`, ).argParser((rawValue: string) => { const value = rawValue.toLowerCase(); const allowed = ["low", "medium", "high", "max"]; if (!allowed.includes(value)) { throw new InvalidArgumentError( `It must be one of: ${allowed.join(", ")}`, ); } return value; }), ) .option( "--agent ", `Agent for the current session. Overrides the 'agent' setting.`, ) .option( "--betas ", "Beta headers to include in API requests (API key users only)", ) .option( "--fallback-model ", "Enable automatic fallback to specified model when default model is overloaded (only works with --print)", ) .addOption( new Option( "--workload ", "Workload tag for billing-header attribution (cc_workload). Process-scoped; set by SDK daemon callers that spawn subprocesses for cron work. (only works with --print)", ).hideHelp(), ) .option( "--settings ", "Path to a settings JSON file or a JSON string to load additional settings from", ) .option( "--add-dir ", "Additional directories to allow tool access to", ) .option( "--ide", "Automatically connect to IDE on startup if exactly one valid IDE is available", () => true, ) .option( "--strict-mcp-config", "Only use MCP servers from --mcp-config, ignoring all other MCP configurations", () => true, ) .option( "--session-id ", "Use a specific session ID for the conversation (must be a valid UUID)", ) .option( "-n, --name ", "Set a display name for this session (shown in /resume and terminal title)", ) .option( "--agents ", 'JSON object defining custom agents (e.g. \'{"reviewer": {"description": "Reviews code", "prompt": "You are a code reviewer"}}\')', ) .option( "--setting-sources ", "Comma-separated list of setting sources to load (user, project, local).", ) // gh-33508: (variadic) consumed everything until the next // --flag. `claude --plugin-dir /path mcp add --transport http` swallowed // `mcp` and `add` as paths, then choked on --transport as an unknown // top-level option. Single-value + collect accumulator means each // --plugin-dir takes exactly one arg; repeat the flag for multiple dirs. .option( "--plugin-dir ", "Load plugins from a directory for this session only (repeatable: --plugin-dir A --plugin-dir B)", (val: string, prev: string[]) => [...prev, val], [] as string[], ) .option("--disable-slash-commands", "Disable all skills", () => true) .option("--chrome", "Enable Claude in Chrome integration") .option("--no-chrome", "Disable Claude in Chrome integration") .option( "--file ", "File resources to download at startup. Format: file_id:relative_path (e.g., --file file_abc:doc.txt file_def:img.png)", ) .action(async (prompt, options) => { profileCheckpoint("action_handler_start"); // --bare = one-switch minimal mode. Sets SIMPLE so all the existing // gates fire (CLAUDE.md, skills, hooks inside executeHooks, agent // dir-walk). Must be set before setup() / any of the gated work runs. if ((options as { bare?: boolean }).bare) { process.env.CLAUDE_CODE_SIMPLE = "1"; } // Ignore "code" as a prompt - treat it the same as no prompt if (prompt === "code") { logEvent("tengu_code_prompt_ignored", {}); // biome-ignore lint/suspicious/noConsole:: intentional console output console.warn( chalk.yellow( "Tip: You can launch Claude Code with just `claude`", ), ); prompt = undefined; } // Log event for any single-word prompt if ( prompt && typeof prompt === "string" && !/\s/.test(prompt) && prompt.length > 0 ) { logEvent("tengu_single_word_prompt", { length: prompt.length }); } // Assistant mode: when .claude/settings.json has assistant: true AND // the tengu_kairos GrowthBook gate is on, force brief on. Permission // mode is left to the user — settings defaultMode or --permission-mode // apply as normal. REPL-typed messages already default to 'next' // priority (messageQueueManager.enqueue) so they drain mid-turn between // tool calls. SendUserMessage (BriefTool) is enabled via the brief env // var. SleepTool stays disabled (its isEnabled() gates on proactive). // kairosEnabled is computed once here and reused at the // getAssistantSystemPromptAddendum() call site further down. // // Trust gate: .claude/settings.json is attacker-controllable in an // untrusted clone. We run ~1000 lines before showSetupScreens() shows // the trust dialog, and by then we've already appended // .claude/agents/assistant.md to the system prompt. Refuse to activate // until the directory has been explicitly trusted. let kairosEnabled = false; let assistantTeamContext: | Awaited< ReturnType< NonNullable< typeof assistantModule >["initializeAssistantTeam"] > > | undefined; if ( feature("KAIROS") && (options as { assistant?: boolean }).assistant && assistantModule ) { // --assistant (Agent SDK daemon mode): force the latch before // isAssistantMode() runs below. The daemon has already checked // entitlement — don't make the child re-check tengu_kairos. assistantModule.markAssistantForced(); } if ( feature("KAIROS") && assistantModule && (assistantModule.isAssistantForced() || (options as Record).assistant === true) && // Spawned teammates share the leader's cwd + settings.json, so // the flag is true for them too. --agent-id being set // means we ARE a spawned teammate (extractTeammateOptions runs // ~170 lines later so check the raw commander option) — don't // re-init the team or override teammateMode/proactive/brief. !(options as { agentId?: unknown }).agentId && kairosGate ) { if (!checkHasTrustDialogAccepted()) { // biome-ignore lint/suspicious/noConsole:: intentional console output console.warn( chalk.yellow( "Assistant mode disabled: directory is not trusted. Accept the trust dialog and restart.", ), ); } else { // Blocking gate check — returns cached `true` instantly; if disk // cache is false/missing, lazily inits GrowthBook and fetches fresh // (max ~5s). --assistant skips the gate entirely (daemon is // pre-entitled). kairosEnabled = assistantModule.isAssistantForced() || (await kairosGate.isKairosEnabled()); if (kairosEnabled) { const opts = options as { brief?: boolean }; opts.brief = true; setKairosActive(true); // Pre-seed an in-process team so Agent(name: "foo") spawns // teammates without TeamCreate. Must run BEFORE setup() captures // the teammateMode snapshot (initializeAssistantTeam calls // setCliTeammateModeOverride internally). assistantTeamContext = await assistantModule.initializeAssistantTeam(); } } } const { debug = false, debugToStderr = false, dangerouslySkipPermissions, allowDangerouslySkipPermissions = false, tools: baseTools = [], allowedTools = [], disallowedTools = [], mcpConfig = [], permissionMode: permissionModeCli, addDir = [], fallbackModel, betas = [], ide = false, sessionId, includeHookEvents, includePartialMessages, } = options; if (options.prefill) { seedEarlyInput(options.prefill); } // Promise for file downloads - started early, awaited before REPL renders let fileDownloadPromise: Promise | undefined; const agentsJson = options.agents; const agentCli = options.agent; if (feature("BG_SESSIONS") && agentCli) { process.env.CLAUDE_CODE_AGENT = agentCli; } // NOTE: LSP manager initialization is intentionally deferred until after // the trust dialog is accepted. This prevents plugin LSP servers from // executing code in untrusted directories before user consent. // Extract these separately so they can be modified if needed let outputFormat = options.outputFormat; let inputFormat = options.inputFormat; let verbose = options.verbose ?? getGlobalConfig().verbose; let print = options.print; const init = options.init ?? false; const initOnly = options.initOnly ?? false; const maintenance = options.maintenance ?? false; // Extract disable slash commands flag const disableSlashCommands = options.disableSlashCommands || false; // Extract tasks mode options (ant-only) const tasksOption = process.env.USER_TYPE === "ant" && (options as { tasks?: boolean | string }).tasks; const taskListId = tasksOption ? typeof tasksOption === "string" ? tasksOption : DEFAULT_TASKS_MODE_TASK_LIST_ID : undefined; if (process.env.USER_TYPE === "ant" && taskListId) { process.env.CLAUDE_CODE_TASK_LIST_ID = taskListId; } // Extract worktree option // worktree can be true (flag without value) or a string (custom name or PR reference) const worktreeOption = isWorktreeModeEnabled() ? (options as { worktree?: boolean | string }).worktree : undefined; let worktreeName = typeof worktreeOption === "string" ? worktreeOption : undefined; const worktreeEnabled = worktreeOption !== undefined; // Check if worktree name is a PR reference (#N or GitHub PR URL) let worktreePRNumber: number | undefined; if (worktreeName) { const prNum = parsePRReference(worktreeName); if (prNum !== null) { worktreePRNumber = prNum; worktreeName = undefined; // slug will be generated in setup() } } // Extract tmux option (requires --worktree) const tmuxEnabled = isWorktreeModeEnabled() && (options as { tmux?: boolean }).tmux === true; // Validate tmux option if (tmuxEnabled) { if (!worktreeEnabled) { process.stderr.write( chalk.red("Error: --tmux requires --worktree\n"), ); process.exit(1); } if (getPlatform() === "windows") { process.stderr.write( chalk.red( "Error: --tmux is not supported on Windows\n", ), ); process.exit(1); } if (!(await isTmuxAvailable())) { process.stderr.write( chalk.red( `Error: tmux is not installed.\n${getTmuxInstallInstructions()}\n`, ), ); process.exit(1); } } // Extract teammate options (for tmux-spawned agents) // Declared outside the if block so it's accessible later for system prompt addendum let storedTeammateOpts: TeammateOptions | undefined; if (isAgentSwarmsEnabled()) { // Extract agent identity options (for tmux-spawned agents) // These replace the CLAUDE_CODE_* environment variables const teammateOpts = extractTeammateOptions(options); storedTeammateOpts = teammateOpts; // If any teammate identity option is provided, all three required ones must be present const hasAnyTeammateOpt = teammateOpts.agentId || teammateOpts.agentName || teammateOpts.teamName; const hasAllRequiredTeammateOpts = teammateOpts.agentId && teammateOpts.agentName && teammateOpts.teamName; if (hasAnyTeammateOpt && !hasAllRequiredTeammateOpts) { process.stderr.write( chalk.red( "Error: --agent-id, --agent-name, and --team-name must all be provided together\n", ), ); process.exit(1); } // If teammate identity is provided via CLI, set up dynamicTeamContext if ( teammateOpts.agentId && teammateOpts.agentName && teammateOpts.teamName ) { getTeammateUtils().setDynamicTeamContext?.({ agentId: teammateOpts.agentId, agentName: teammateOpts.agentName, teamName: teammateOpts.teamName, color: teammateOpts.agentColor, planModeRequired: teammateOpts.planModeRequired ?? false, parentSessionId: teammateOpts.parentSessionId, }); } // Set teammate mode CLI override if provided // This must be done before setup() captures the snapshot if (teammateOpts.teammateMode) { getTeammateModeSnapshot().setCliTeammateModeOverride?.( teammateOpts.teammateMode, ); } } // Extract remote sdk options const sdkUrl = (options as { sdkUrl?: string }).sdkUrl ?? undefined; // Allow env var to enable partial messages (used by sandbox gateway for baku) const effectiveIncludePartialMessages = includePartialMessages || isEnvTruthy(process.env.CLAUDE_CODE_INCLUDE_PARTIAL_MESSAGES); // Enable all hook event types when explicitly requested via SDK option // or when running in CLAUDE_CODE_REMOTE mode (CCR needs them). // Without this, only SessionStart and Setup events are emitted. if ( includeHookEvents || isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) ) { setAllHookEventsEnabled(true); } // Auto-set input/output formats, verbose mode, and print mode when SDK URL is provided if (sdkUrl) { // If SDK URL is provided, automatically use stream-json formats unless explicitly set if (!inputFormat) { inputFormat = "stream-json"; } if (!outputFormat) { outputFormat = "stream-json"; } // Auto-enable verbose mode unless explicitly disabled or already set if (options.verbose === undefined) { verbose = true; } // Auto-enable print mode unless explicitly disabled if (!options.print) { print = true; } } // Extract teleport option const teleport = (options as { teleport?: string | true }).teleport ?? null; // Extract remote option (can be true if no description provided, or a string) const remoteOption = (options as { remote?: string | true }).remote; const remote = remoteOption === true ? "" : (remoteOption ?? null); // Extract --remote-control / --rc flag (enable bridge in interactive session) const remoteControlOption = (options as { remoteControl?: string | true }).remoteControl ?? (options as { rc?: string | true }).rc; // Actual bridge check is deferred to after showSetupScreens() so that // trust is established and GrowthBook has auth headers. let remoteControl = false; const remoteControlName = typeof remoteControlOption === "string" && remoteControlOption.length > 0 ? remoteControlOption : undefined; // Validate session ID if provided if (sessionId) { // Check for conflicting flags // --session-id can be used with --continue or --resume when --fork-session is also provided // (to specify a custom ID for the forked session) if ( (options.continue || options.resume) && !options.forkSession ) { process.stderr.write( chalk.red( "Error: --session-id can only be used with --continue or --resume if --fork-session is also specified.\n", ), ); process.exit(1); } // When --sdk-url is provided (bridge/remote mode), the session ID is a // server-assigned tagged ID (e.g. "session_local_01...") rather than a // UUID. Skip UUID validation and local existence checks in that case. if (!sdkUrl) { const validatedSessionId = validateUuid(sessionId); if (!validatedSessionId) { process.stderr.write( chalk.red( "Error: Invalid session ID. Must be a valid UUID.\n", ), ); process.exit(1); } // Check if session ID already exists if (sessionIdExists(validatedSessionId)) { process.stderr.write( chalk.red( `Error: Session ID ${validatedSessionId} is already in use.\n`, ), ); process.exit(1); } } } // Download file resources if specified via --file flag const fileSpecs = (options as { file?: string[] }).file; if (fileSpecs && fileSpecs.length > 0) { // Get session ingress token (provided by EnvManager via CLAUDE_CODE_SESSION_ACCESS_TOKEN) const sessionToken = getSessionIngressAuthToken(); if (!sessionToken) { process.stderr.write( chalk.red( "Error: Session token required for file downloads. CLAUDE_CODE_SESSION_ACCESS_TOKEN must be set.\n", ), ); process.exit(1); } // Resolve session ID: prefer remote session ID, fall back to internal session ID const fileSessionId = process.env.CLAUDE_CODE_REMOTE_SESSION_ID || getSessionId(); const files = parseFileSpecs(fileSpecs); if (files.length > 0) { // Use ANTHROPIC_BASE_URL if set (by EnvManager), otherwise use OAuth config // This ensures consistency with session ingress API in all environments const config: FilesApiConfig = { baseUrl: process.env.ANTHROPIC_BASE_URL || getOauthConfig().BASE_API_URL, oauthToken: sessionToken, sessionId: fileSessionId, }; // Start download without blocking startup - await before REPL renders fileDownloadPromise = downloadSessionFiles(files, config); } } // Get isNonInteractiveSession from state (was set before init()) const isNonInteractiveSession = getIsNonInteractiveSession(); // Validate that fallback model is different from main model if ( fallbackModel && options.model && fallbackModel === options.model ) { process.stderr.write( chalk.red( "Error: Fallback model cannot be the same as the main model. Please specify a different model for --fallback-model.\n", ), ); process.exit(1); } // Handle system prompt options let systemPrompt = options.systemPrompt; if (options.systemPromptFile) { if (options.systemPrompt) { process.stderr.write( chalk.red( "Error: Cannot use both --system-prompt and --system-prompt-file. Please use only one.\n", ), ); process.exit(1); } try { const filePath = resolve(options.systemPromptFile); systemPrompt = readFileSync(filePath, "utf8"); } catch (error) { const code = getErrnoCode(error); if (code === "ENOENT") { process.stderr.write( chalk.red( `Error: System prompt file not found: ${resolve(options.systemPromptFile)}\n`, ), ); process.exit(1); } process.stderr.write( chalk.red( `Error reading system prompt file: ${errorMessage(error)}\n`, ), ); process.exit(1); } } // Handle append system prompt options let appendSystemPrompt = options.appendSystemPrompt; if (options.appendSystemPromptFile) { if (options.appendSystemPrompt) { process.stderr.write( chalk.red( "Error: Cannot use both --append-system-prompt and --append-system-prompt-file. Please use only one.\n", ), ); process.exit(1); } try { const filePath = resolve(options.appendSystemPromptFile); appendSystemPrompt = readFileSync(filePath, "utf8"); } catch (error) { const code = getErrnoCode(error); if (code === "ENOENT") { process.stderr.write( chalk.red( `Error: Append system prompt file not found: ${resolve(options.appendSystemPromptFile)}\n`, ), ); process.exit(1); } process.stderr.write( chalk.red( `Error reading append system prompt file: ${errorMessage(error)}\n`, ), ); process.exit(1); } } // Add teammate-specific system prompt addendum for tmux teammates if ( isAgentSwarmsEnabled() && storedTeammateOpts?.agentId && storedTeammateOpts?.agentName && storedTeammateOpts?.teamName ) { const addendum = getTeammatePromptAddendum().TEAMMATE_SYSTEM_PROMPT_ADDENDUM; appendSystemPrompt = appendSystemPrompt ? `${appendSystemPrompt}\n\n${addendum}` : addendum; } const { mode: permissionMode, notification: permissionModeNotification, } = initialPermissionModeFromCLI({ permissionModeCli, dangerouslySkipPermissions, }); // Store session bypass permissions mode for trust dialog check setSessionBypassPermissionsMode( permissionMode === "bypassPermissions", ); if (feature("TRANSCRIPT_CLASSIFIER")) { // autoModeFlagCli is the "did the user intend auto this session" signal. // Set when: --enable-auto-mode, --permission-mode auto, resolved mode // is auto, OR settings defaultMode is auto but the gate denied it // (permissionMode resolved to default with no explicit CLI override). // Used by verifyAutoModeGateAccess to decide whether to notify on // auto-unavailable, and by tengu_auto_mode_config opt-in carousel. if ( (options as { enableAutoMode?: boolean }).enableAutoMode || permissionModeCli === "auto" || permissionMode === "auto" || (!permissionModeCli && isDefaultPermissionModeAuto()) ) { autoModeStateModule?.setAutoModeFlagCli(true); } } // Parse the MCP config files/strings if provided let dynamicMcpConfig: Record = { // Built-in MCP servers (default disabled, user enables via /mcp) "mcp-chrome": { type: "http", url: "http://127.0.0.1:12306/mcp", scope: "dynamic", "headers": { "Authorization": "Bearer my-static-token", } }, }; if (mcpConfig && mcpConfig.length > 0) { // Process mcpConfig array const processedConfigs = mcpConfig .map((config) => config.trim()) .filter((config) => config.length > 0); let allConfigs: Record = {}; const allErrors: ValidationError[] = []; for (const configItem of processedConfigs) { let configs: Record | null = null; let errors: ValidationError[] = []; // First try to parse as JSON string const parsedJson = safeParseJSON(configItem); if (parsedJson) { const result = parseMcpConfig({ configObject: parsedJson, filePath: "command line", expandVars: true, scope: "dynamic", }); if (result.config) { configs = result.config.mcpServers; } else { errors = result.errors; } } else { // Try as file path const configPath = resolve(configItem); const result = parseMcpConfigFromFilePath({ filePath: configPath, expandVars: true, scope: "dynamic", }); if (result.config) { configs = result.config.mcpServers; } else { errors = result.errors; } } if (errors.length > 0) { allErrors.push(...errors); } else if (configs) { // Merge configs, later ones override earlier ones allConfigs = { ...allConfigs, ...configs }; } } if (allErrors.length > 0) { const formattedErrors = allErrors .map( (err) => `${err.path ? err.path + ": " : ""}${err.message}`, ) .join("\n"); logForDebugging( `--mcp-config validation failed (${allErrors.length} errors): ${formattedErrors}`, { level: "error", }, ); process.stderr.write( `Error: Invalid MCP configuration:\n${formattedErrors}\n`, ); process.exit(1); } if (Object.keys(allConfigs).length > 0) { // SDK hosts (Nest/Desktop) own their server naming and may reuse // built-in names — skip reserved-name checks for type:'sdk'. const nonSdkConfigNames = Object.entries(allConfigs) .filter(([, config]) => config.type !== "sdk") .map(([name]) => name); let reservedNameError: string | null = null; if (nonSdkConfigNames.some(isClaudeInChromeMCPServer)) { reservedNameError = `Invalid MCP configuration: "${CLAUDE_IN_CHROME_MCP_SERVER_NAME}" is a reserved MCP name.`; } else if (feature("CHICAGO_MCP")) { const { isComputerUseMCPServer, COMPUTER_USE_MCP_SERVER_NAME, } = await import("src/utils/computerUse/common.js"); if (nonSdkConfigNames.some(isComputerUseMCPServer)) { reservedNameError = `Invalid MCP configuration: "${COMPUTER_USE_MCP_SERVER_NAME}" is a reserved MCP name.`; } } if (reservedNameError) { // stderr+exit(1) — a throw here becomes a silent unhandled // rejection in stream-json mode (void main() in cli.tsx). process.stderr.write(`Error: ${reservedNameError}\n`); process.exit(1); } // Add dynamic scope to all configs. type:'sdk' entries pass through // unchanged — they're extracted into sdkMcpConfigs downstream and // passed to print.ts. The Python SDK relies on this path (it doesn't // send sdkMcpServers in the initialize message). Dropping them here // broke Coworker (inc-5122). The policy filter below already exempts // type:'sdk', and the entries are inert without an SDK transport on // stdin, so there's no bypass risk from letting them through. const scopedConfigs = mapValues(allConfigs, (config) => ({ ...config, scope: "dynamic" as const, })); // Enforce managed policy (allowedMcpServers / deniedMcpServers) on // --mcp-config servers. Without this, the CLI flag bypasses the // enterprise allowlist that user/project/local configs go through in // getClaudeCodeMcpConfigs — callers spread dynamicMcpConfig back on // top of filtered results. Filter here at the source so all // downstream consumers see the policy-filtered set. const { allowed, blocked } = filterMcpServersByPolicy(scopedConfigs); if (blocked.length > 0) { process.stderr.write( `Warning: MCP ${plural(blocked.length, "server")} blocked by enterprise policy: ${blocked.join(", ")}\n`, ); } dynamicMcpConfig = { ...dynamicMcpConfig, ...(allowed as Record) }; } } // Extract Claude in Chrome option and enforce claude.ai subscriber check (unless user is ant) const chromeOpts = options as { chrome?: boolean }; // Store the explicit CLI flag so teammates can inherit it setChromeFlagOverride(chromeOpts.chrome); const enableClaudeInChrome = shouldEnableClaudeInChrome(chromeOpts.chrome) && (process.env.USER_TYPE === "ant" || isClaudeAISubscriber()); const autoEnableClaudeInChrome = !enableClaudeInChrome && shouldAutoEnableClaudeInChrome(); if (enableClaudeInChrome) { const platform = getPlatform(); try { logEvent("tengu_claude_in_chrome_setup", { platform: platform as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }); const { mcpConfig: chromeMcpConfig, allowedTools: chromeMcpTools, systemPrompt: chromeSystemPrompt, } = setupClaudeInChrome(); dynamicMcpConfig = { ...dynamicMcpConfig, ...chromeMcpConfig, }; allowedTools.push(...chromeMcpTools); if (chromeSystemPrompt) { appendSystemPrompt = appendSystemPrompt ? `${chromeSystemPrompt}\n\n${appendSystemPrompt}` : chromeSystemPrompt; } } catch (error) { logEvent("tengu_claude_in_chrome_setup_failed", { platform: platform as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }); logForDebugging(`[Claude in Chrome] Error: ${error}`); logError(error); // biome-ignore lint/suspicious/noConsole:: intentional console output console.error( `Error: Failed to run with Claude in Chrome.`, ); process.exit(1); } } else if (autoEnableClaudeInChrome) { try { const { mcpConfig: chromeMcpConfig } = setupClaudeInChrome(); dynamicMcpConfig = { ...dynamicMcpConfig, ...chromeMcpConfig, }; const hint = feature("WEB_BROWSER_TOOL") && typeof Bun !== "undefined" && "WebView" in Bun ? CLAUDE_IN_CHROME_SKILL_HINT_WITH_WEBBROWSER : CLAUDE_IN_CHROME_SKILL_HINT; appendSystemPrompt = appendSystemPrompt ? `${appendSystemPrompt}\n\n${hint}` : hint; } catch (error) { // Silently skip any errors for the auto-enable logForDebugging( `[Claude in Chrome] Error (auto-enable): ${error}`, ); } } // Extract strict MCP config flag const strictMcpConfig = options.strictMcpConfig || false; // Check if enterprise MCP configuration exists. When it does, only allow dynamic MCP // configs that contain special server types (sdk) if (doesEnterpriseMcpConfigExist()) { if (strictMcpConfig) { process.stderr.write( chalk.red( "You cannot use --strict-mcp-config when an enterprise MCP config is present", ), ); process.exit(1); } // For --mcp-config, allow if all servers are internal types (sdk) if ( dynamicMcpConfig && !areMcpConfigsAllowedWithEnterpriseMcpConfig( dynamicMcpConfig, ) ) { process.stderr.write( chalk.red( "You cannot dynamically configure MCP servers when an enterprise MCP config is present", ), ); process.exit(1); } } // chicago MCP: guarded Computer Use (app allowlist + frontmost gate + // SCContentFilter screenshots). Ant-only, GrowthBook-gated — failures // are silent (this is dogfooding). Platform + interactive checks inline // so non-macOS / print-mode ants skip the heavy @ant/computer-use-mcp // import entirely. gates.js is light (type-only package import). // // Placed AFTER the enterprise-MCP-config check: that check rejects any // dynamicMcpConfig entry with `type !== 'sdk'`, and our config is // `type: 'stdio'`. An enterprise-config ant with the GB gate on would // otherwise process.exit(1). Chrome has the same latent issue but has // shipped without incident; chicago places itself correctly. if ( feature("CHICAGO_MCP") && getPlatform() !== "unknown" && !getIsNonInteractiveSession() ) { try { const { getChicagoEnabled } = await import("src/utils/computerUse/gates.js"); if (getChicagoEnabled()) { const { setupComputerUseMCP } = await import("src/utils/computerUse/setup.js"); const { mcpConfig, allowedTools: cuTools } = setupComputerUseMCP(); dynamicMcpConfig = { ...dynamicMcpConfig, ...mcpConfig, }; allowedTools.push(...cuTools); } } catch (error) { logForDebugging( `[Computer Use MCP] Setup failed: ${errorMessage(error)}`, ); } } // Store additional directories for CLAUDE.md loading (controlled by env var) setAdditionalDirectoriesForClaudeMd(addDir); // Channel server allowlist from --channels flag — servers whose // inbound push notifications should register this session. The option // is added inside a feature() block so TS doesn't know about it // on the options type — same pattern as --assistant at main.tsx:1824. // devChannels is deferred: showSetupScreens shows a confirmation dialog // and only appends to allowedChannels on accept. let devChannels: ChannelEntry[] | undefined; // Parse plugin:name@marketplace / server:Y tags into typed entries. // Tag decides trust model downstream: plugin-kind hits marketplace // verification + GrowthBook allowlist, server-kind always fails // allowlist (schema is plugin-only) unless dev flag is set. // Untagged or marketplace-less plugin entries are hard errors — // silently not-matching in the gate would look like channels are // "on" but nothing ever fires. const parseChannelEntries = ( raw: string[], flag: string, ): ChannelEntry[] => { const entries: ChannelEntry[] = []; const bad: string[] = []; for (const c of raw) { if (c.startsWith("plugin:")) { const rest = c.slice(7); const at = rest.indexOf("@"); if (at <= 0 || at === rest.length - 1) { bad.push(c); } else { entries.push({ kind: "plugin", name: rest.slice(0, at), marketplace: rest.slice(at + 1), }); } } else if (c.startsWith("server:") && c.length > 7) { entries.push({ kind: "server", name: c.slice(7) }); } else { bad.push(c); } } if (bad.length > 0) { process.stderr.write( chalk.red( `${flag} entries must be tagged: ${bad.join(", ")}\n` + ` plugin:@ — plugin-provided channel (allowlist enforced)\n` + ` server: — manually configured MCP server\n`, ), ); process.exit(1); } return entries; }; const channelOpts = options as { channels?: string[]; dangerouslyLoadDevelopmentChannels?: string[]; }; const rawChannels = channelOpts.channels; const rawDev = channelOpts.dangerouslyLoadDevelopmentChannels; // Always parse + set. ChannelsNotice reads getAllowedChannels() and // renders the appropriate branch (disabled/noAuth/policyBlocked/ // listening) in the startup screen. gateChannelServer() enforces. // --channels works in both interactive and print/SDK modes; dev-channels // stays interactive-only (requires a confirmation dialog). let channelEntries: ChannelEntry[] = []; if (rawChannels && rawChannels.length > 0) { channelEntries = parseChannelEntries( rawChannels, "--channels", ); setAllowedChannels(channelEntries); } if (!isNonInteractiveSession) { if (rawDev && rawDev.length > 0) { devChannels = parseChannelEntries( rawDev, "--dangerously-load-development-channels", ); } } // Flag-usage telemetry. Plugin identifiers are logged (same tier as // tengu_plugin_installed — public-registry-style names); server-kind // names are not (MCP-server-name tier, opt-in-only elsewhere). // Per-server gate outcomes land in tengu_mcp_channel_gate once // servers connect. Dev entries go through a confirmation dialog after // this — dev_plugins captures what was typed, not what was accepted. if ( channelEntries.length > 0 || (devChannels?.length ?? 0) > 0 ) { const joinPluginIds = (entries: ChannelEntry[]) => { const ids = entries.flatMap((e) => e.kind === "plugin" ? [`${e.name}@${e.marketplace}`] : [], ); return ids.length > 0 ? (ids .sort() .join( ",", ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS) : undefined; }; logEvent("tengu_mcp_channel_flags", { channels_count: channelEntries.length, dev_count: devChannels?.length ?? 0, plugins: joinPluginIds(channelEntries), dev_plugins: joinPluginIds(devChannels ?? []), }); } // SDK opt-in for SendUserMessage via --tools. All sessions require // explicit opt-in; listing it in --tools signals intent. Runs BEFORE // initializeToolPermissionContext so getToolsForDefaultPreset() sees // the tool as enabled when computing the base-tools disallow filter. // Conditional require avoids leaking the tool-name string into // external builds. if ( (feature("KAIROS") || feature("KAIROS_BRIEF")) && baseTools.length > 0 ) { /* eslint-disable @typescript-eslint/no-require-imports */ const { BRIEF_TOOL_NAME, LEGACY_BRIEF_TOOL_NAME } = require("@claude-code-best/builtin-tools/tools/BriefTool/prompt.js") as typeof import("@claude-code-best/builtin-tools/tools/BriefTool/prompt.js"); const { isBriefEntitled } = require("@claude-code-best/builtin-tools/tools/BriefTool/BriefTool.js") as typeof import("@claude-code-best/builtin-tools/tools/BriefTool/BriefTool.js"); /* eslint-enable @typescript-eslint/no-require-imports */ const parsed = parseToolListFromCLI(baseTools); if ( (parsed.includes(BRIEF_TOOL_NAME) || parsed.includes(LEGACY_BRIEF_TOOL_NAME)) && isBriefEntitled() ) { setUserMsgOptIn(true); } } // This await replaces blocking existsSync/statSync calls that were already in // the startup path. Wall-clock time is unchanged; we just yield to the event // loop during the fs I/O instead of blocking it. See #19661. const initResult = await initializeToolPermissionContext({ allowedToolsCli: allowedTools, disallowedToolsCli: disallowedTools, baseToolsCli: baseTools, permissionMode, allowDangerouslySkipPermissions, addDirs: addDir, }); let toolPermissionContext = initResult.toolPermissionContext; const { warnings, dangerousPermissions, overlyBroadBashPermissions, } = initResult; // Handle overly broad shell allow rules for ant users (Bash(*), PowerShell(*)) if ( process.env.USER_TYPE === "ant" && overlyBroadBashPermissions.length > 0 ) { for (const permission of overlyBroadBashPermissions) { logForDebugging( `Ignoring overly broad shell permission ${permission.ruleDisplay} from ${permission.sourceDisplay}`, ); } toolPermissionContext = removeDangerousPermissions( toolPermissionContext, overlyBroadBashPermissions, ); } if ( feature("TRANSCRIPT_CLASSIFIER") && dangerousPermissions.length > 0 ) { toolPermissionContext = stripDangerousPermissionsForAutoMode( toolPermissionContext, ); } // Print any warnings from initialization warnings.forEach((warning) => { // biome-ignore lint/suspicious/noConsole:: intentional console output console.error(warning); }); // claude.ai config fetch: -p mode only (interactive uses useManageMCPConnections // two-phase loading). Kicked off here to overlap with setup(); awaited // before runHeadless so single-turn -p sees connectors. Skipped under // enterprise/strict MCP to preserve policy boundaries. const claudeaiConfigPromise: Promise< Record > = isNonInteractiveSession && !strictMcpConfig && !doesEnterpriseMcpConfigExist() && // --bare / SIMPLE: skip claude.ai proxy servers (datadog, Gmail, // Slack, BigQuery, PubMed — 6-14s each to connect). Scripted calls // that need MCP pass --mcp-config explicitly. !isBareMode() ? fetchClaudeAIMcpConfigsIfEligible().then((configs) => { const { allowed, blocked } = filterMcpServersByPolicy(configs); if (blocked.length > 0) { process.stderr.write( `Warning: claude.ai MCP ${plural(blocked.length, "server")} blocked by enterprise policy: ${blocked.join(", ")}\n`, ); } return allowed; }) : Promise.resolve({}); // Kick off MCP config loading early (safe - just reads files, no execution). // Both interactive and -p use getClaudeCodeMcpConfigs (local file reads only). // The local promise is awaited later (before prefetchAllMcpResources) to // overlap config I/O with setup(), commands loading, and trust dialog. logForDebugging("[STARTUP] Loading MCP configs..."); const mcpConfigStart = Date.now(); let mcpConfigResolvedMs: number | undefined; // --bare skips auto-discovered MCP (.mcp.json, user settings, plugins) — // only explicit --mcp-config works. dynamicMcpConfig is spread onto // allMcpConfigs downstream so it survives this skip. const mcpConfigPromise = ( strictMcpConfig || isBareMode() ? Promise.resolve({ servers: {} as Record< string, ScopedMcpServerConfig >, }) : getClaudeCodeMcpConfigs(dynamicMcpConfig) ).then((result) => { mcpConfigResolvedMs = Date.now() - mcpConfigStart; return result; }); // NOTE: We do NOT call prefetchAllMcpResources here - that's deferred until after trust dialog if ( inputFormat && inputFormat !== "text" && inputFormat !== "stream-json" ) { // biome-ignore lint/suspicious/noConsole:: intentional console output console.error(`Error: Invalid input format "${inputFormat}".`); process.exit(1); } if ( inputFormat === "stream-json" && outputFormat !== "stream-json" ) { // biome-ignore lint/suspicious/noConsole:: intentional console output console.error( `Error: --input-format=stream-json requires output-format=stream-json.`, ); process.exit(1); } // Validate sdkUrl is only used with appropriate formats (formats are auto-set above) if (sdkUrl) { if ( inputFormat !== "stream-json" || outputFormat !== "stream-json" ) { // biome-ignore lint/suspicious/noConsole:: intentional console output console.error( `Error: --sdk-url requires both --input-format=stream-json and --output-format=stream-json.`, ); process.exit(1); } } // Validate replayUserMessages is only used with stream-json formats if (options.replayUserMessages) { if ( inputFormat !== "stream-json" || outputFormat !== "stream-json" ) { // biome-ignore lint/suspicious/noConsole:: intentional console output console.error( `Error: --replay-user-messages requires both --input-format=stream-json and --output-format=stream-json.`, ); process.exit(1); } } // Validate includePartialMessages is only used with print mode and stream-json output if (effectiveIncludePartialMessages) { if ( !isNonInteractiveSession || outputFormat !== "stream-json" ) { writeToStderr( `Error: --include-partial-messages requires --print and --output-format=stream-json.`, ); process.exit(1); } } // Validate --no-session-persistence is only used with print mode if ( options.sessionPersistence === false && !isNonInteractiveSession ) { writeToStderr( `Error: --no-session-persistence can only be used with --print mode.`, ); process.exit(1); } const effectivePrompt = prompt || ""; let inputPrompt = await getInputPrompt( effectivePrompt, (inputFormat ?? "text") as "text" | "stream-json", ); profileCheckpoint("action_after_input_prompt"); // Activate proactive mode BEFORE getTools() so SleepTool.isEnabled() // (which returns isProactiveActive()) passes and Sleep is included. // The later REPL-path maybeActivateProactive() calls are idempotent. maybeActivateProactive(options); let tools = getTools(toolPermissionContext); // Apply coordinator mode tool filtering for headless path // (mirrors useMergedTools.ts filtering for REPL/interactive path) if ( feature("COORDINATOR_MODE") && isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE) ) { const { applyCoordinatorToolFilter } = await import("./utils/toolPool.js"); tools = applyCoordinatorToolFilter(tools); } profileCheckpoint("action_tools_loaded"); let jsonSchema: ToolInputJSONSchema | undefined; if ( isSyntheticOutputToolEnabled({ isNonInteractiveSession }) && options.jsonSchema ) { jsonSchema = jsonParse( options.jsonSchema, ) as ToolInputJSONSchema; } if (jsonSchema) { const syntheticOutputResult = createSyntheticOutputTool(jsonSchema); if ("tool" in syntheticOutputResult) { // Add SyntheticOutputTool to the tools array AFTER getTools() filtering. // This tool is excluded from normal filtering (see tools.ts) because it's // an implementation detail for structured output, not a user-controlled tool. tools = [...tools, syntheticOutputResult.tool]; logEvent("tengu_structured_output_enabled", { schema_property_count: Object.keys( (jsonSchema.properties as Record< string, unknown >) || {}, ) .length as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, has_required_fields: Boolean( jsonSchema.required, ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }); } else { logEvent("tengu_structured_output_failure", { error: "Invalid JSON schema" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }); } } // IMPORTANT: setup() must be called before any other code that depends on the cwd or worktree setup profileCheckpoint("action_before_setup"); logForDebugging("[STARTUP] Running setup()..."); const setupStart = Date.now(); const { setup } = await import("./setup.js"); const messagingSocketPath = feature("UDS_INBOX") ? (options as { messagingSocketPath?: string }) .messagingSocketPath : undefined; // Parallelize setup() with commands+agents loading. setup()'s ~28ms is // mostly startUdsMessaging (socket bind, ~20ms) — not disk-bound, so it // doesn't contend with getCommands' file reads. Gated on !worktreeEnabled // since --worktree makes setup() process.chdir() (setup.ts:203), and // commands/agents need the post-chdir cwd. const preSetupCwd = getCwd(); // Register bundled skills/plugins before kicking getCommands() — they're // pure in-memory array pushes (<1ms, zero I/O) that getBundledSkills() // reads synchronously. Previously ran inside setup() after ~20ms of // await points, so the parallel getCommands() memoized an empty list. if (process.env.CLAUDE_CODE_ENTRYPOINT !== "local-agent") { initBuiltinPlugins(); initBundledSkills(); } const setupPromise = setup( preSetupCwd, permissionMode, allowDangerouslySkipPermissions, worktreeEnabled, worktreeName, tmuxEnabled, sessionId ? validateUuid(sessionId) : undefined, worktreePRNumber, messagingSocketPath, ); const commandsPromise = worktreeEnabled ? null : getCommands(preSetupCwd); const agentDefsPromise = worktreeEnabled ? null : getAgentDefinitionsWithOverrides(preSetupCwd); // Suppress transient unhandledRejection if these reject during the // ~28ms setupPromise await before Promise.all joins them below. commandsPromise?.catch(() => {}); agentDefsPromise?.catch(() => {}); await setupPromise; logForDebugging( `[STARTUP] setup() completed in ${Date.now() - setupStart}ms`, ); profileCheckpoint("action_after_setup"); // Replay user messages into stream-json only when the socket was // explicitly requested. The auto-generated socket is passive — it // lets tools inject if they want to, but turning it on by default // shouldn't reshape stream-json for SDK consumers who never touch it. // Callers who inject and also want those injections visible in the // stream pass --messaging-socket-path explicitly (or --replay-user-messages). let effectiveReplayUserMessages = !!options.replayUserMessages; if (feature("UDS_INBOX")) { if ( !effectiveReplayUserMessages && outputFormat === "stream-json" ) { effectiveReplayUserMessages = !!( options as { messagingSocketPath?: string } ).messagingSocketPath; } } if (getIsNonInteractiveSession()) { // Apply full merged settings env now (including project-scoped // .claude/settings.json PATH/GIT_DIR/GIT_WORK_TREE) so gitExe() and // the git spawn below see it. Trust is implicit in -p mode; the // docstring at managedEnv.ts:96-97 says this applies "potentially // dangerous environment variables such as LD_PRELOAD, PATH" from all // sources. The later call in the isNonInteractiveSession block below // is idempotent (Object.assign, configureGlobalAgents ejects prior // interceptor) and picks up any plugin-contributed env after plugin // init. Project settings are already loaded here: // applySafeConfigEnvironmentVariables in init() called // getSettings_DEPRECATED at managedEnv.ts:86 which merges all enabled // sources including projectSettings/localSettings. applyConfigEnvironmentVariables(); // Spawn git status/log/branch now so the subprocess execution overlaps // with the getCommands await below and startDeferredPrefetches. After // setup() so cwd is final (setup.ts:254 may process.chdir(worktreePath) // for --worktree) and after the applyConfigEnvironmentVariables above // so PATH/GIT_DIR/GIT_WORK_TREE from all sources (trusted + project) // are applied. getSystemContext is memoized; the // prefetchSystemContextIfSafe call in startDeferredPrefetches becomes // a cache hit. The microtask from await getIsGit() drains at the // getCommands Promise.all await below. Trust is implicit in -p mode // (same gate as prefetchSystemContextIfSafe). void getSystemContext(); // Kick getUserContext now too — its first await (fs.readFile in // getMemoryFiles) yields naturally, so the CLAUDE.md directory walk // runs during the ~280ms overlap window before the context // Promise.all join in print.ts. The void getUserContext() in // startDeferredPrefetches becomes a memoize cache-hit. void getUserContext(); // Kick ensureModelStringsInitialized now — for Bedrock this triggers // a 100-200ms profile fetch that was awaited serially at // print.ts:739. updateBedrockModelStrings is sequential()-wrapped so // the await joins the in-flight fetch. Non-Bedrock is a sync // early-return (zero-cost). void ensureModelStringsInitialized(); } // Apply --name: cache-only so no orphan file is created before the // session ID is finalized by --continue/--resume. materializeSessionFile // persists it on the first user message; REPL's useTerminalTitle reads it // via getCurrentSessionTitle. const sessionNameArg = options.name?.trim(); if (sessionNameArg) { cacheSessionTitle(sessionNameArg); } // Ant model aliases (capybara-fast etc.) resolve via the // tengu_ant_model_override GrowthBook flag. _CACHED_MAY_BE_STALE reads // disk synchronously; disk is populated by a fire-and-forget write. On a // cold cache, parseUserSpecifiedModel returns the unresolved alias, the // API 404s, and -p exits before the async write lands — crashloop on // fresh pods. Awaiting init here populates the in-memory payload map that // _CACHED_MAY_BE_STALE now checks first. Gated so the warm path stays // non-blocking: // - explicit model via --model or ANTHROPIC_MODEL (both feed alias resolution) // - no env override (which short-circuits _CACHED_MAY_BE_STALE before disk) // - flag absent from disk (== null also catches pre-#22279 poisoned null) const explicitModel = options.model || process.env.ANTHROPIC_MODEL; if ( process.env.USER_TYPE === "ant" && explicitModel && explicitModel !== "default" && !hasGrowthBookEnvOverride("tengu_ant_model_override") && getGlobalConfig().cachedGrowthBookFeatures?.[ "tengu_ant_model_override" ] == null ) { await initializeGrowthBook(); } // Special case the default model with the null keyword // NOTE: Model resolution happens after setup() to ensure trust is established before AWS auth const userSpecifiedModel = options.model === "default" ? getDefaultMainLoopModel() : options.model; const userSpecifiedFallbackModel = fallbackModel === "default" ? getDefaultMainLoopModel() : fallbackModel; // Reuse preSetupCwd unless setup() chdir'd (worktreeEnabled). Saves a // getCwd() syscall in the common path. const currentCwd = worktreeEnabled ? getCwd() : preSetupCwd; logForDebugging("[STARTUP] Loading commands and agents..."); const commandsStart = Date.now(); // Join the promises kicked before setup() (or start fresh if // worktreeEnabled gated the early kick). Both memoized by cwd. const [commands, agentDefinitionsResult] = await Promise.all([ commandsPromise ?? getCommands(currentCwd), agentDefsPromise ?? getAgentDefinitionsWithOverrides(currentCwd), ]); logForDebugging( `[STARTUP] Commands and agents loaded in ${Date.now() - commandsStart}ms`, ); profileCheckpoint("action_commands_loaded"); // Parse CLI agents if provided via --agents flag let cliAgents: typeof agentDefinitionsResult.activeAgents = []; if (agentsJson) { try { const parsedAgents = safeParseJSON(agentsJson); if (parsedAgents) { cliAgents = parseAgentsFromJson( parsedAgents, "flagSettings", ); } } catch (error) { logError(error); } } // Merge CLI agents with existing ones const allAgents = [ ...agentDefinitionsResult.allAgents, ...cliAgents, ]; const agentDefinitions = { ...agentDefinitionsResult, allAgents, activeAgents: getActiveAgentsFromList(allAgents), }; // Look up main thread agent from CLI flag or settings const agentSetting = agentCli ?? getInitialSettings().agent; let mainThreadAgentDefinition: | (typeof agentDefinitions.activeAgents)[number] | undefined; if (agentSetting) { mainThreadAgentDefinition = agentDefinitions.activeAgents.find( (agent) => agent.agentType === agentSetting, ); if (!mainThreadAgentDefinition) { logForDebugging( `Warning: agent "${agentSetting}" not found. ` + `Available agents: ${agentDefinitions.activeAgents.map((a) => a.agentType).join(", ")}. ` + `Using default behavior.`, ); } } // Store the main thread agent type in bootstrap state so hooks can access it setMainThreadAgentType(mainThreadAgentDefinition?.agentType); // Log agent flag usage — only log agent name for built-in agents to avoid leaking custom agent names if (mainThreadAgentDefinition) { logEvent("tengu_agent_flag", { agentType: isBuiltInAgent(mainThreadAgentDefinition) ? (mainThreadAgentDefinition.agentType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS) : ("custom" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS), ...(agentCli && { source: "cli" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }), }); } // Persist agent setting to session transcript for resume view display and restoration if (mainThreadAgentDefinition?.agentType) { saveAgentSetting(mainThreadAgentDefinition.agentType); } // Apply the agent's system prompt for non-interactive sessions // (interactive mode uses buildEffectiveSystemPrompt instead) if ( isNonInteractiveSession && mainThreadAgentDefinition && !systemPrompt && !isBuiltInAgent(mainThreadAgentDefinition) ) { const agentSystemPrompt = mainThreadAgentDefinition.getSystemPrompt(); if (agentSystemPrompt) { systemPrompt = agentSystemPrompt; } } // initialPrompt goes first so its slash command (if any) is processed; // user-provided text becomes trailing context. // Only concatenate when inputPrompt is a string. When it's an // AsyncIterable (SDK stream-json mode), template interpolation would // call .toString() producing "[object Object]". The AsyncIterable case // is handled in print.ts via structuredIO.prependUserMessage(). if (mainThreadAgentDefinition?.initialPrompt) { if (typeof inputPrompt === "string") { inputPrompt = inputPrompt ? `${mainThreadAgentDefinition.initialPrompt}\n\n${inputPrompt}` : mainThreadAgentDefinition.initialPrompt; } else if (!inputPrompt) { inputPrompt = mainThreadAgentDefinition.initialPrompt; } } // Compute effective model early so hooks can run in parallel with MCP // If user didn't specify a model but agent has one, use the agent's model let effectiveModel = userSpecifiedModel; if ( !effectiveModel && mainThreadAgentDefinition?.model && mainThreadAgentDefinition.model !== "inherit" ) { effectiveModel = parseUserSpecifiedModel( mainThreadAgentDefinition.model, ); } setMainLoopModelOverride(effectiveModel); // Compute resolved model for hooks (use user-specified model at launch) setInitialMainLoopModel(getUserSpecifiedModelSetting() || null); const initialMainLoopModel = getInitialMainLoopModel(); const resolvedInitialModel = parseUserSpecifiedModel( initialMainLoopModel ?? getDefaultMainLoopModel(), ); let advisorModel: string | undefined; if (isAdvisorEnabled()) { const advisorOption = canUserConfigureAdvisor() ? (options as { advisor?: string }).advisor : undefined; if (advisorOption) { logForDebugging(`[AdvisorTool] --advisor ${advisorOption}`); if (!modelSupportsAdvisor(resolvedInitialModel)) { process.stderr.write( chalk.red( `Error: The model "${resolvedInitialModel}" does not support the advisor tool.\n`, ), ); process.exit(1); } const normalizedAdvisorModel = normalizeModelStringForAPI( parseUserSpecifiedModel(advisorOption), ); if (!isValidAdvisorModel(normalizedAdvisorModel)) { process.stderr.write( chalk.red( `Error: The model "${advisorOption}" cannot be used as an advisor.\n`, ), ); process.exit(1); } } advisorModel = canUserConfigureAdvisor() ? (advisorOption ?? getInitialAdvisorSetting()) : advisorOption; if (advisorModel) { logForDebugging( `[AdvisorTool] Advisor model: ${advisorModel}`, ); } } // For tmux teammates with --agent-type, append the custom agent's prompt if ( isAgentSwarmsEnabled() && storedTeammateOpts?.agentId && storedTeammateOpts?.agentName && storedTeammateOpts?.teamName && storedTeammateOpts?.agentType ) { // Look up the custom agent definition const customAgent = agentDefinitions.activeAgents.find( (a) => a.agentType === storedTeammateOpts.agentType, ); if (customAgent) { // Get the prompt - need to handle both built-in and custom agents let customPrompt: string | undefined; if (customAgent.source === "built-in") { // Built-in agents have getSystemPrompt that takes toolUseContext // We can't access full toolUseContext here, so skip for now logForDebugging( `[teammate] Built-in agent ${storedTeammateOpts.agentType} - skipping custom prompt (not supported)`, ); } else { // Custom agents have getSystemPrompt that takes no args customPrompt = customAgent.getSystemPrompt(); } // Log agent memory loaded event for tmux teammates if (customAgent.memory) { logEvent("tengu_agent_memory_loaded", { ...(process.env.USER_TYPE === "ant" && { agent_type: customAgent.agentType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }), scope: customAgent.memory as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, source: "teammate" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }); } if (customPrompt) { const customInstructions = `\n# Custom Agent Instructions\n${customPrompt}`; appendSystemPrompt = appendSystemPrompt ? `${appendSystemPrompt}\n\n${customInstructions}` : customInstructions; } } else { logForDebugging( `[teammate] Custom agent ${storedTeammateOpts.agentType} not found in available agents`, ); } } maybeActivateBrief(options); // defaultView: 'chat' is a persisted opt-in — check entitlement and set // userMsgOptIn so the tool + prompt section activate. Interactive-only: // defaultView is a display preference; SDK sessions have no display, and // the assistant installer writes defaultView:'chat' to settings.local.json // which would otherwise leak into --print sessions in the same directory. // Runs right after maybeActivateBrief() so all startup opt-in paths fire // BEFORE any isBriefEnabled() read below (proactive prompt's // briefVisibility). A persisted 'chat' after a GB kill-switch falls // through (entitlement fails). if ( (feature("KAIROS") || feature("KAIROS_BRIEF")) && !getIsNonInteractiveSession() && !getUserMsgOptIn() && getInitialSettings().defaultView === "chat" ) { /* eslint-disable @typescript-eslint/no-require-imports */ const { isBriefEntitled } = require("@claude-code-best/builtin-tools/tools/BriefTool/BriefTool.js") as typeof import("@claude-code-best/builtin-tools/tools/BriefTool/BriefTool.js"); /* eslint-enable @typescript-eslint/no-require-imports */ if (isBriefEntitled()) { setUserMsgOptIn(true); } } // Coordinator mode has its own system prompt and filters out Sleep, so // the generic proactive prompt would tell it to call a tool it can't // access and conflict with delegation instructions. if ( (feature("PROACTIVE") || feature("KAIROS")) && ((options as { proactive?: boolean }).proactive || isEnvTruthy(process.env.CLAUDE_CODE_PROACTIVE)) && !coordinatorModeModule?.isCoordinatorMode() ) { /* eslint-disable @typescript-eslint/no-require-imports */ const briefVisibility = feature("KAIROS") || feature("KAIROS_BRIEF") ? ( require("@claude-code-best/builtin-tools/tools/BriefTool/BriefTool.js") as typeof import("@claude-code-best/builtin-tools/tools/BriefTool/BriefTool.js") ).isBriefEnabled() ? "Call SendUserMessage at checkpoints to mark where things stand." : "The user will see any text you output." : "The user will see any text you output."; /* eslint-enable @typescript-eslint/no-require-imports */ const proactivePrompt = `\n# Proactive Mode\n\nYou are in proactive mode. Take initiative — explore, act, and make progress without waiting for instructions.\n\nStart by briefly greeting the user.\n\nYou will receive periodic prompts. These are check-ins. Do whatever seems most useful, or call Sleep if there's nothing to do. ${briefVisibility}`; appendSystemPrompt = appendSystemPrompt ? `${appendSystemPrompt}\n\n${proactivePrompt}` : proactivePrompt; } if (feature("KAIROS") && kairosEnabled && assistantModule) { const assistantAddendum = assistantModule.getAssistantSystemPromptAddendum(); appendSystemPrompt = appendSystemPrompt ? `${appendSystemPrompt}\n\n${assistantAddendum}` : assistantAddendum; } // Ink root is only needed for interactive sessions — patchConsole in the // Ink constructor would swallow console output in headless mode. let root!: Root; let getFpsMetrics!: () => FpsMetrics | undefined; let stats!: StatsStore; // Show setup screens after commands are loaded if (!isNonInteractiveSession) { const ctx = getRenderContext(false); getFpsMetrics = ctx.getFpsMetrics; stats = ctx.stats; // Install asciicast recorder before Ink mounts (ant-only, opt-in via CLAUDE_CODE_TERMINAL_RECORDING=1) if (process.env.USER_TYPE === "ant") { installAsciicastRecorder(); } const { createRoot } = await import('@anthropic/ink') root = await createRoot(ctx.renderOptions) // Log startup time now, before any blocking dialog renders. Logging // from REPL's first render (the old location) included however long // the user sat on trust/OAuth/onboarding/resume-picker — p99 was ~70s // dominated by dialog-wait time, not code-path startup. logEvent("tengu_timer", { event: "startup" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, durationMs: Math.round(process.uptime() * 1000), }); logForDebugging("[STARTUP] Running showSetupScreens()..."); const setupScreensStart = Date.now(); const onboardingShown = await showSetupScreens( root, permissionMode, allowDangerouslySkipPermissions, commands, enableClaudeInChrome, devChannels, ); logForDebugging( `[STARTUP] showSetupScreens() completed in ${Date.now() - setupScreensStart}ms`, ); // Now that trust is established and GrowthBook has auth headers, // resolve the --remote-control / --rc entitlement gate. if ( feature("BRIDGE_MODE") && remoteControlOption !== undefined ) { const { getBridgeDisabledReason } = await import("./bridge/bridgeEnabled.js"); const disabledReason = await getBridgeDisabledReason(); remoteControl = disabledReason === null; if (disabledReason) { process.stderr.write( chalk.yellow( `${disabledReason}\n--rc flag ignored.\n`, ), ); } } // Check for pending agent memory snapshot updates (only for --agent mode, ant-only) if ( feature("AGENT_MEMORY_SNAPSHOT") && mainThreadAgentDefinition && isCustomAgent(mainThreadAgentDefinition) && mainThreadAgentDefinition.memory && mainThreadAgentDefinition.pendingSnapshotUpdate ) { const agentDef = mainThreadAgentDefinition; const choice = await launchSnapshotUpdateDialog(root, { agentType: agentDef.agentType, scope: agentDef.memory!, snapshotTimestamp: agentDef.pendingSnapshotUpdate!.snapshotTimestamp, }); if (choice === "merge") { const { buildMergePrompt } = await import("./components/agents/SnapshotUpdateDialog.js"); const mergePrompt = buildMergePrompt( agentDef.agentType, agentDef.memory!, ); inputPrompt = inputPrompt ? `${mergePrompt}\n\n${inputPrompt}` : mergePrompt; } agentDef.pendingSnapshotUpdate = undefined; } // Skip executing /login if we just completed onboarding for it if ( onboardingShown && prompt?.trim().toLowerCase() === "/login" ) { prompt = ""; } if (onboardingShown) { // Refresh auth-dependent services now that the user has logged in during onboarding. // Keep in sync with the post-login logic in src/commands/login.tsx void refreshRemoteManagedSettings(); void refreshPolicyLimits(); // Clear user data cache BEFORE GrowthBook refresh so it picks up fresh credentials resetUserCache(); // Refresh GrowthBook after login to get updated feature flags (e.g., for claude.ai MCPs) refreshGrowthBookAfterAuthChange(); // Clear any stale trusted device token then enroll for Remote Control. // Both self-gate on tengu_sessions_elevated_auth_enforcement internally // — enrollTrustedDevice() via checkGate_CACHED_OR_BLOCKING (awaits // the GrowthBook reinit above), clearTrustedDeviceToken() via the // sync cached check (acceptable since clear is idempotent). void import("./bridge/trustedDevice.js").then((m) => { m.clearTrustedDeviceToken(); return m.enrollTrustedDevice(); }); } // Validate that the active token's org matches forceLoginOrgUUID (if set // in managed settings). Runs after onboarding so managed settings and // login state are fully loaded. const orgValidation = await validateForceLoginOrg(); if (!orgValidation.valid) { await exitWithError(root, (orgValidation as { valid: false; message: string }).message); } } // If gracefulShutdown was initiated (e.g., user rejected trust dialog), // process.exitCode will be set. Skip all subsequent operations that could // trigger code execution before the process exits (e.g. we don't want apiKeyHelper // to run if trust was not established). if (process.exitCode !== undefined) { logForDebugging( "Graceful shutdown initiated, skipping further initialization", ); return; } // Initialize LSP manager AFTER trust is established (or in non-interactive mode // where trust is implicit). This prevents plugin LSP servers from executing // code in untrusted directories before user consent. // Must be after inline plugins are set (if any) so --plugin-dir LSP servers are included. initializeLspServerManager(); // Show settings validation errors after trust is established // MCP config errors don't block settings from loading, so exclude them if (!isNonInteractiveSession) { const { errors } = getSettingsWithErrors(); const nonMcpErrors = errors.filter((e) => !e.mcpErrorMetadata); if (nonMcpErrors.length > 0) { await launchInvalidSettingsDialog(root, { settingsErrors: nonMcpErrors, onExit: () => gracefulShutdownSync(1), }); } } // Check quota status, fast mode, passes eligibility, and bootstrap data // after trust is established. These make API calls which could trigger // apiKeyHelper execution. // --bare / SIMPLE: skip — these are cache-warms for the REPL's // first-turn responsiveness (quota, passes, fastMode, bootstrap data). Fast // mode doesn't apply to the Agent SDK anyway (see getFastModeUnavailableReason). const bgRefreshThrottleMs = getFeatureValue_CACHED_MAY_BE_STALE( "tengu_cicada_nap_ms", 0, ); const lastPrefetched = getGlobalConfig().startupPrefetchedAt ?? 0; const skipStartupPrefetches = isBareMode() || (bgRefreshThrottleMs > 0 && Date.now() - lastPrefetched < bgRefreshThrottleMs); if (!skipStartupPrefetches) { const lastPrefetchedInfo = lastPrefetched > 0 ? ` last ran ${Math.round((Date.now() - lastPrefetched) / 1000)}s ago` : ""; logForDebugging( `Starting background startup prefetches${lastPrefetchedInfo}`, ); checkQuotaStatus().catch((error) => logError(error)); // Fetch bootstrap data from the server and update all cache values. void fetchBootstrapData(); // TODO: Consolidate other prefetches into a single bootstrap request. void prefetchPassesEligibility(); if ( !getFeatureValue_CACHED_MAY_BE_STALE( "tengu_miraculo_the_bard", false, ) ) { void prefetchFastModeStatus(); } else { // Kill switch skips the network call, not org-policy enforcement. // Resolve from cache so orgStatus doesn't stay 'pending' (which // getFastModeUnavailableReason treats as permissive). resolveFastModeStatusFromCache(); } if (bgRefreshThrottleMs > 0) { saveGlobalConfig((current) => ({ ...current, startupPrefetchedAt: Date.now(), })); } } else { logForDebugging( `Skipping startup prefetches, last ran ${Math.round((Date.now() - lastPrefetched) / 1000)}s ago`, ); // Resolve fast mode org status from cache (no network) resolveFastModeStatusFromCache(); } if (!isNonInteractiveSession) { void refreshExampleCommands(); // Pre-fetch example commands (runs git log, no API call) } // Resolve MCP configs (started early, overlaps with setup/trust dialog work) const { servers: existingMcpConfigs } = await mcpConfigPromise; logForDebugging( `[STARTUP] MCP configs resolved in ${mcpConfigResolvedMs}ms (awaited at +${Date.now() - mcpConfigStart}ms)`, ); // CLI flag (--mcp-config) should override file-based configs, matching settings precedence const allMcpConfigs = { ...existingMcpConfigs, ...dynamicMcpConfig, }; // Separate SDK configs from regular MCP configs const sdkMcpConfigs: Record = {}; const regularMcpConfigs: Record = {}; for (const [name, config] of Object.entries(allMcpConfigs)) { const typedConfig = config as | ScopedMcpServerConfig | McpSdkServerConfig; if (typedConfig.type === "sdk") { sdkMcpConfigs[name] = typedConfig as McpSdkServerConfig; } else { regularMcpConfigs[name] = typedConfig as ScopedMcpServerConfig; } } profileCheckpoint("action_mcp_configs_loaded"); // Prefetch MCP resources after trust dialog (this is where execution happens). // Interactive mode only: print mode defers connects until headlessStore exists // and pushes per-server (below), so ToolSearch's pending-client handling works // and one slow server doesn't block the batch. const localMcpPromise = isNonInteractiveSession ? Promise.resolve({ clients: [], tools: [], commands: [] }) : prefetchAllMcpResources(regularMcpConfigs); const claudeaiMcpPromise = isNonInteractiveSession ? Promise.resolve({ clients: [], tools: [], commands: [] }) : claudeaiConfigPromise.then((configs) => Object.keys(configs).length > 0 ? prefetchAllMcpResources(configs) : { clients: [], tools: [], commands: [] }, ); // Merge with dedup by name: each prefetchAllMcpResources call independently // adds helper tools (ListMcpResourcesTool, ReadMcpResourceTool) via // local dedup flags, so merging two calls can yield duplicates. print.ts // already uniqBy's the final tool pool, but dedup here keeps appState clean. const mcpPromise = Promise.all([ localMcpPromise, claudeaiMcpPromise, ]).then(([local, claudeai]) => ({ clients: [...local.clients, ...claudeai.clients], tools: uniqBy([...local.tools, ...claudeai.tools], "name"), commands: uniqBy( [...local.commands, ...claudeai.commands], "name", ), })); // Start hooks early so they run in parallel with MCP connections. // Skip for initOnly/init/maintenance (handled separately), non-interactive // (handled via setupTrigger), and resume/continue (conversationRecovery.ts // fires 'resume' instead — without this guard, hooks fire TWICE on /resume // and the second systemMessage clobbers the first. gh-30825) const hooksPromise = initOnly || init || maintenance || isNonInteractiveSession || options.continue || options.resume ? null : processSessionStartHooks("startup", { agentType: mainThreadAgentDefinition?.agentType, model: resolvedInitialModel, }); // MCP never blocks REPL render OR turn 1 TTFT. useManageMCPConnections // populates appState.mcp async as servers connect (connectToServer is // memoized — the prefetch calls above and the hook converge on the same // connections). getToolUseContext reads store.getState() fresh via // computeTools(), so turn 1 sees whatever's connected by query time. // Slow servers populate for turn 2+. Matches interactive-no-prompt // behavior. Print mode: per-server push into headlessStore (below). const hookMessages: Awaited> = []; // Suppress transient unhandledRejection — the prefetch warms the // memoized connectToServer cache but nobody awaits it in interactive. mcpPromise.catch(() => {}); const mcpClients: Awaited["clients"] = []; const mcpTools: Awaited["tools"] = []; const mcpCommands: Awaited["commands"] = []; let thinkingEnabled = shouldEnableThinkingByDefault(); let thinkingConfig: ThinkingConfig = thinkingEnabled !== false ? { type: "adaptive" } : { type: "disabled" }; if ( options.thinking === "adaptive" || options.thinking === "enabled" ) { thinkingEnabled = true; thinkingConfig = { type: "adaptive" }; } else if (options.thinking === "disabled") { thinkingEnabled = false; thinkingConfig = { type: "disabled" }; } else { const maxThinkingTokens = process.env.MAX_THINKING_TOKENS ? parseInt(process.env.MAX_THINKING_TOKENS, 10) : options.maxThinkingTokens; if (maxThinkingTokens !== undefined) { if (maxThinkingTokens > 0) { thinkingEnabled = true; thinkingConfig = { type: "enabled", budgetTokens: maxThinkingTokens, }; } else if (maxThinkingTokens === 0) { thinkingEnabled = false; thinkingConfig = { type: "disabled" }; } } } logForDiagnosticsNoPII("info", "started", { version: MACRO.VERSION, is_native_binary: isInBundledMode(), }); registerCleanup(async () => { logForDiagnosticsNoPII("info", "exited"); }); void logTenguInit({ hasInitialPrompt: Boolean(prompt), hasStdin: Boolean(inputPrompt), verbose, debug, debugToStderr, print: print ?? false, outputFormat: outputFormat ?? "text", inputFormat: inputFormat ?? "text", numAllowedTools: allowedTools.length, numDisallowedTools: disallowedTools.length, mcpClientCount: Object.keys(allMcpConfigs).length, worktreeEnabled, skipWebFetchPreflight: getInitialSettings().skipWebFetchPreflight, githubActionInputs: process.env.GITHUB_ACTION_INPUTS, dangerouslySkipPermissionsPassed: dangerouslySkipPermissions ?? false, permissionMode, modeIsBypass: permissionMode === "bypassPermissions", allowDangerouslySkipPermissionsPassed: allowDangerouslySkipPermissions, systemPromptFlag: systemPrompt ? options.systemPromptFile ? "file" : "flag" : undefined, appendSystemPromptFlag: appendSystemPrompt ? options.appendSystemPromptFile ? "file" : "flag" : undefined, thinkingConfig, assistantActivationPath: feature("KAIROS") && kairosEnabled ? assistantModule?.getAssistantActivationPath() : undefined, }); // Log context metrics once at initialization void logContextMetrics(regularMcpConfigs, toolPermissionContext); void logPermissionContextForAnts(null, "initialization"); logManagedSettings(); // Register PID file for concurrent-session detection (~/.claude/sessions/) // and fire multi-clauding telemetry. Lives here (not init.ts) so only the // REPL path registers — not subcommands like `claude doctor`. Chained: // count must run after register's write completes or it misses our own file. void registerSession().then((registered) => { if (!registered) return; if (sessionNameArg) { void updateSessionName(sessionNameArg); } void countConcurrentSessions().then((count) => { if (count >= 2) { logEvent("tengu_concurrent_sessions", { num_sessions: count, }); } }); }); // Initialize versioned plugins system (triggers V1→V2 migration if // needed). Then run orphan GC, THEN warm the Grep/Glob exclusion cache. // Sequencing matters: the warmup scans disk for .orphaned_at markers, // so it must see the GC's Pass 1 (remove markers from reinstalled // versions) and Pass 2 (stamp unmarked orphans) already applied. The // warm also lands before autoupdate (fires on first submit in REPL) // can orphan this session's active version underneath us. // --bare / SIMPLE: skip plugin version sync + orphan cleanup. These // are install/upgrade bookkeeping that scripted calls don't need — // the next interactive session will reconcile. The await here was // blocking -p on a marketplace round-trip. if (isBareMode()) { // skip — no-op } else if (isNonInteractiveSession) { // In headless mode, await to ensure plugin sync completes before CLI exits await initializeVersionedPlugins(); profileCheckpoint("action_after_plugins_init"); void cleanupOrphanedPluginVersionsInBackground().then(() => getGlobExclusionsForPluginCache(), ); } else { // In interactive mode, fire-and-forget — this is purely bookkeeping // that doesn't affect runtime behavior of the current session void initializeVersionedPlugins().then(async () => { profileCheckpoint("action_after_plugins_init"); await cleanupOrphanedPluginVersionsInBackground(); void getGlobExclusionsForPluginCache(); }); } const setupTrigger = initOnly || init ? "init" : maintenance ? "maintenance" : null; if (initOnly) { applyConfigEnvironmentVariables(); await processSetupHooks("init", { forceSyncExecution: true }); await processSessionStartHooks("startup", { forceSyncExecution: true, }); gracefulShutdownSync(0); return; } // --print mode if (isNonInteractiveSession) { if (outputFormat === "stream-json" || outputFormat === "json") { setHasFormattedOutput(true); } // Apply full environment variables in print mode since trust dialog is bypassed // This includes potentially dangerous environment variables from untrusted sources // but print mode is considered trusted (as documented in help text) applyConfigEnvironmentVariables(); // Initialize telemetry after env vars are applied so OTEL endpoint env vars and // otelHeadersHelper (which requires trust to execute) are available. initializeTelemetryAfterTrust(); // Kick SessionStart hooks now so the subprocess spawn overlaps with // MCP connect + plugin init + print.ts import below. loadInitialMessages // joins this at print.ts:4397. Guarded same as loadInitialMessages — // continue/resume/teleport paths don't fire startup hooks (or fire them // conditionally inside the resume branch, where this promise is // undefined and the ?? fallback runs). Also skip when setupTrigger is // set — those paths run setup hooks first (print.ts:544), and session // start hooks must wait until setup completes. const sessionStartHooksPromise = options.continue || options.resume || teleport || setupTrigger ? undefined : processSessionStartHooks("startup"); // Suppress transient unhandledRejection if this rejects before // loadInitialMessages awaits it. Downstream await still observes the // rejection — this just prevents the spurious global handler fire. sessionStartHooksPromise?.catch(() => {}); profileCheckpoint("before_validateForceLoginOrg"); // Validate org restriction for non-interactive sessions const orgValidation = await validateForceLoginOrg(); if (!orgValidation.valid) { process.stderr.write((orgValidation as { valid: false; message: string }).message + "\n"); process.exit(1); } // Headless mode supports all prompt commands and some local commands // If disableSlashCommands is true, return empty array const commandsHeadless = disableSlashCommands ? [] : commands.filter( (command) => (command.type === "prompt" && !command.disableNonInteractive) || (command.type === "local" && command.supportsNonInteractive), ); const defaultState = getDefaultAppState(); const headlessInitialState: AppState = { ...defaultState, mcp: { ...defaultState.mcp, clients: mcpClients, commands: mcpCommands, tools: mcpTools, }, toolPermissionContext, effortValue: parseEffortValue(options.effort) ?? getInitialEffortSetting(), ...(isFastModeEnabled() && { fastMode: getInitialFastModeSetting( effectiveModel ?? null, ), }), ...(isAdvisorEnabled() && advisorModel && { advisorModel }), // kairosEnabled gates the async fire-and-forget path in // executeForkedSlashCommand (processSlashCommand.tsx:132) and // AgentTool's shouldRunAsync. The REPL initialState sets this at // ~3459; headless was defaulting to false, so the daemon child's // scheduled tasks and Agent-tool calls ran synchronously — N // overdue cron tasks on spawn = N serial subagent turns blocking // user input. Computed at :1620, well before this branch. ...(feature("KAIROS") ? { kairosEnabled } : {}), }; // Init app state const headlessStore = createStore( headlessInitialState, onChangeAppState, ); // Async check of auto mode gate — corrects state and disables auto if needed. if (feature("TRANSCRIPT_CLASSIFIER")) { void verifyAutoModeGateAccess( toolPermissionContext, headlessStore.getState().fastMode, ).then(({ updateContext }) => { headlessStore.setState((prev) => { const nextCtx = updateContext( prev.toolPermissionContext, ); if (nextCtx === prev.toolPermissionContext) return prev; return { ...prev, toolPermissionContext: nextCtx }; }); }); } // Set global state for session persistence if (options.sessionPersistence === false) { setSessionPersistenceDisabled(true); } // Store SDK betas in global state for context window calculation // Only store allowed betas (filters by allowlist and subscriber status) setSdkBetas(filterAllowedSdkBetas(betas)); // Print-mode MCP: per-server incremental push into headlessStore. // Mirrors useManageMCPConnections — push pending first (so ToolSearch's // pending-check at ToolSearchTool.ts:334 sees them), then replace with // connected/failed as each server settles. const connectMcpBatch = ( configs: Record, label: string, ): Promise => { if (Object.keys(configs).length === 0) return Promise.resolve(); headlessStore.setState((prev) => ({ ...prev, mcp: { ...prev.mcp, clients: [ ...prev.mcp.clients, ...Object.entries(configs).map( ([name, config]) => ({ name, type: "pending" as const, config, }), ), ], }, })); return getMcpToolsCommandsAndResources( ({ client, tools, commands }) => { headlessStore.setState((prev) => ({ ...prev, mcp: { ...prev.mcp, clients: prev.mcp.clients.some( (c) => c.name === client.name, ) ? prev.mcp.clients.map((c) => c.name === client.name ? client : c, ) : [...prev.mcp.clients, client], tools: uniqBy( [...prev.mcp.tools, ...tools], "name", ), commands: uniqBy( [...prev.mcp.commands, ...commands], "name", ), }, })); }, configs, ).catch((err) => logForDebugging(`[MCP] ${label} connect error: ${err}`), ); }; // Await all MCP configs — print mode is often single-turn, so // "late-connecting servers visible next turn" doesn't help. SDK init // message and turn-1 tool list both need configured MCP tools present. // Zero-server case is free via the early return in connectMcpBatch. // Connectors parallelize inside getMcpToolsCommandsAndResources // (processBatched with Promise.all). claude.ai is awaited too — its // fetch was kicked off early (line ~2558) so only residual time blocks // here. --bare skips claude.ai entirely for perf-sensitive scripts. profileCheckpoint("before_connectMcp"); await connectMcpBatch(regularMcpConfigs, "regular"); profileCheckpoint("after_connectMcp"); // Dedup: suppress plugin MCP servers that duplicate a claude.ai // connector (connector wins), then connect claude.ai servers. // Bounded wait — #23725 made this blocking so single-turn -p sees // connectors, but with 40+ slow connectors tengu_startup_perf p99 // climbed to 76s. If fetch+connect doesn't finish in time, proceed; // the promise keeps running and updates headlessStore in the // background so turn 2+ still sees connectors. const CLAUDE_AI_MCP_TIMEOUT_MS = 5_000; const claudeaiConnect = claudeaiConfigPromise.then( (claudeaiConfigs) => { if (Object.keys(claudeaiConfigs).length > 0) { const claudeaiSigs = new Set(); for (const config of Object.values( claudeaiConfigs, )) { const sig = getMcpServerSignature(config); if (sig) claudeaiSigs.add(sig); } const suppressed = new Set(); for (const [name, config] of Object.entries( regularMcpConfigs, )) { if (!name.startsWith("plugin:")) continue; const sig = getMcpServerSignature(config); if (sig && claudeaiSigs.has(sig)) suppressed.add(name); } if (suppressed.size > 0) { logForDebugging( `[MCP] Lazy dedup: suppressing ${suppressed.size} plugin server(s) that duplicate claude.ai connectors: ${[...suppressed].join(", ")}`, ); // Disconnect before filtering from state. Only connected // servers need cleanup — clearServerCache on a never-connected // server triggers a real connect just to kill it (memoize // cache-miss path, see useManageMCPConnections.ts:870). for (const c of headlessStore.getState().mcp .clients) { if ( !suppressed.has(c.name) || c.type !== "connected" ) continue; c.client.onclose = undefined; void clearServerCache( c.name, c.config, ).catch(() => {}); } headlessStore.setState((prev) => { let { clients, tools, commands, resources, } = prev.mcp; clients = clients.filter( (c) => !suppressed.has(c.name), ); tools = tools.filter( (t) => !t.mcpInfo || !suppressed.has( t.mcpInfo.serverName, ), ); for (const name of suppressed) { commands = excludeCommandsByServer( commands, name, ); resources = excludeResourcesByServer( resources, name, ); } return { ...prev, mcp: { ...prev.mcp, clients, tools, commands, resources, }, }; }); } } // Suppress claude.ai connectors that duplicate an enabled // manual server (URL-signature match). Plugin dedup above only // handles `plugin:*` keys; this catches manual `.mcp.json` entries. // plugin:* must be excluded here — step 1 already suppressed // those (claude.ai wins); leaving them in suppresses the // connector too, and neither survives (gh-39974). const nonPluginConfigs = pickBy( regularMcpConfigs, (_, n) => !n.startsWith("plugin:"), ); const { servers: dedupedClaudeAi } = dedupClaudeAiMcpServers( claudeaiConfigs, nonPluginConfigs, ); return connectMcpBatch(dedupedClaudeAi, "claudeai"); }, ); let claudeaiTimer: ReturnType | undefined; const claudeaiTimedOut = await Promise.race([ claudeaiConnect.then(() => false), new Promise((resolve) => { claudeaiTimer = setTimeout( (r) => r(true), CLAUDE_AI_MCP_TIMEOUT_MS, resolve, ); }), ]); if (claudeaiTimer) clearTimeout(claudeaiTimer); if (claudeaiTimedOut) { logForDebugging( `[MCP] claude.ai connectors not ready after ${CLAUDE_AI_MCP_TIMEOUT_MS}ms — proceeding; background connection continues`, ); } profileCheckpoint("after_connectMcp_claudeai"); // In headless mode, start deferred prefetches immediately (no user typing delay) // --bare / SIMPLE: startDeferredPrefetches early-returns internally. // backgroundHousekeeping (initExtractMemories, pruneShellSnapshots, // cleanupOldMessageFiles) and sdkHeapDumpMonitor are all bookkeeping // that scripted calls don't need — the next interactive session reconciles. if (!isBareMode()) { startDeferredPrefetches(); void import("./utils/backgroundHousekeeping.js").then((m) => m.startBackgroundHousekeeping(), ); if (process.env.USER_TYPE === "ant") { void import("./utils/sdkHeapDumpMonitor.js").then((m) => m.startSdkMemoryMonitor(), ); } } logSessionTelemetry(); profileCheckpoint("before_print_import"); const { runHeadless } = await import("src/cli/print.js"); profileCheckpoint("after_print_import"); void runHeadless( inputPrompt, () => headlessStore.getState(), headlessStore.setState, commandsHeadless, tools, sdkMcpConfigs, agentDefinitions.activeAgents, { continue: options.continue, resume: options.resume, verbose: verbose, outputFormat: outputFormat, jsonSchema, permissionPromptToolName: options.permissionPromptTool, allowedTools, thinkingConfig, maxTurns: options.maxTurns, maxBudgetUsd: options.maxBudgetUsd, taskBudget: options.taskBudget ? { total: options.taskBudget } : undefined, systemPrompt, appendSystemPrompt, userSpecifiedModel: effectiveModel, fallbackModel: userSpecifiedFallbackModel, teleport, sdkUrl, replayUserMessages: effectiveReplayUserMessages, includePartialMessages: effectiveIncludePartialMessages, forkSession: options.forkSession || false, resumeSessionAt: options.resumeSessionAt || undefined, rewindFiles: options.rewindFiles, enableAuthStatus: options.enableAuthStatus, agent: agentCli, workload: options.workload, setupTrigger: setupTrigger ?? undefined, sessionStartHooksPromise, }, ); return; } // Log model config at startup logEvent("tengu_startup_manual_model_config", { cli_flag: options.model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, env_var: process.env .ANTHROPIC_MODEL as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, settings_file: (getInitialSettings() || {}) .model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, subscriptionType: getSubscriptionType() as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, agent: agentSetting as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }); // Get deprecation warning for the initial model (resolvedInitialModel computed earlier for hooks parallelization) const deprecationWarning = getModelDeprecationWarning(resolvedInitialModel); // Build initial notification queue const initialNotifications: Array<{ key: string; text: string; color?: "warning"; priority: "high"; }> = []; if (permissionModeNotification) { initialNotifications.push({ key: "permission-mode-notification", text: permissionModeNotification, priority: "high", }); } if (deprecationWarning) { initialNotifications.push({ key: "model-deprecation-warning", text: deprecationWarning, color: "warning", priority: "high", }); } if (overlyBroadBashPermissions.length > 0) { const displayList = uniq( overlyBroadBashPermissions.map((p) => p.ruleDisplay), ); const displays = displayList.join(", "); const sources = uniq( overlyBroadBashPermissions.map((p) => p.sourceDisplay), ).join(", "); const n = displayList.length; initialNotifications.push({ key: "overly-broad-bash-notification", text: `${displays} allow ${plural(n, "rule")} from ${sources} ${plural(n, "was", "were")} ignored \u2014 not available for Ants, please use auto-mode instead`, color: "warning", priority: "high", }); } const teammateUtils = getTeammateUtils(); const effectiveToolPermissionContext = { ...toolPermissionContext, mode: isAgentSwarmsEnabled() && teammateUtils?.isPlanModeRequired?.() ? ("plan" as const) : toolPermissionContext.mode, }; // All startup opt-in paths (--tools, --brief, defaultView) have fired // above; initialIsBriefOnly just reads the resulting state. const initialIsBriefOnly = feature("KAIROS") || feature("KAIROS_BRIEF") ? getUserMsgOptIn() : false; const fullRemoteControl = remoteControl || getRemoteControlAtStartup() || kairosEnabled; let ccrMirrorEnabled = false; if (feature("CCR_MIRROR") && !fullRemoteControl) { /* eslint-disable @typescript-eslint/no-require-imports */ const { isCcrMirrorEnabled } = require("./bridge/bridgeEnabled.js") as typeof import("./bridge/bridgeEnabled.js"); /* eslint-enable @typescript-eslint/no-require-imports */ ccrMirrorEnabled = isCcrMirrorEnabled(); } const initialState: AppState = { settings: getInitialSettings(), tasks: {}, agentNameRegistry: new Map(), verbose: verbose ?? getGlobalConfig().verbose ?? false, mainLoopModel: initialMainLoopModel, mainLoopModelForSession: null, isBriefOnly: initialIsBriefOnly, expandedView: getGlobalConfig().showSpinnerTree ? "teammates" : getGlobalConfig().showExpandedTodos ? "tasks" : "none", showTeammateMessagePreview: isAgentSwarmsEnabled() ? false : undefined, selectedIPAgentIndex: -1, coordinatorTaskIndex: -1, viewSelectionMode: "none", footerSelection: null, toolPermissionContext: effectiveToolPermissionContext, agent: mainThreadAgentDefinition?.agentType, agentDefinitions, mcp: { clients: [], tools: [], commands: [], resources: {}, pluginReconnectKey: 0, }, plugins: { enabled: [], disabled: [], commands: [], errors: [], installationStatus: { marketplaces: [], plugins: [], }, needsRefresh: false, }, statusLineText: undefined, kairosEnabled, remoteSessionUrl: undefined, remoteConnectionStatus: "connecting", remoteBackgroundTaskCount: 0, replBridgeEnabled: fullRemoteControl || ccrMirrorEnabled, replBridgeExplicit: remoteControl, replBridgeOutboundOnly: ccrMirrorEnabled, replBridgeConnected: false, replBridgeSessionActive: false, replBridgeReconnecting: false, replBridgeConnectUrl: undefined, replBridgeSessionUrl: undefined, replBridgeEnvironmentId: undefined, replBridgeSessionId: undefined, replBridgeError: undefined, replBridgeInitialName: remoteControlName, showRemoteCallout: false, notifications: { current: null, queue: initialNotifications, }, elicitation: { queue: [], }, todos: {}, remoteAgentTaskSuggestions: [], fileHistory: { snapshots: [], trackedFiles: new Set(), snapshotSequence: 0, }, attribution: createEmptyAttributionState(), thinkingEnabled, promptSuggestionEnabled: shouldEnablePromptSuggestion(), sessionHooks: new Map(), inbox: { messages: [], }, promptSuggestion: { text: null, promptId: null, shownAt: 0, acceptedAt: 0, generationRequestId: null, }, speculation: IDLE_SPECULATION_STATE, speculationSessionTimeSavedMs: 0, skillImprovement: { suggestion: null, }, workerSandboxPermissions: { queue: [], selectedIndex: 0, }, pendingWorkerRequest: null, pendingSandboxRequest: null, authVersion: 0, initialMessage: inputPrompt ? { message: createUserMessage({ content: String(inputPrompt), }), } : null, effortValue: parseEffortValue(options.effort) ?? getInitialEffortSetting(), activeOverlays: new Set(), fastMode: getInitialFastModeSetting(resolvedInitialModel), ...(isAdvisorEnabled() && advisorModel && { advisorModel }), // Compute teamContext synchronously to avoid useEffect setState during render. // KAIROS: assistantTeamContext takes precedence — set earlier in the // KAIROS block so Agent(name: "foo") can spawn in-process teammates // without TeamCreate. computeInitialTeamContext() is for tmux-spawned // teammates reading their own identity, not the assistant-mode leader. teamContext: (feature("KAIROS") ? (assistantTeamContext ?? computeInitialTeamContext()) : computeInitialTeamContext()) as AppState["teamContext"], }; // Add CLI initial prompt to history if (inputPrompt) { addToHistory(String(inputPrompt)); } const initialTools = mcpTools; // Increment numStartups synchronously — first-render readers like // shouldShowEffortCallout (via useState initializer) need the updated // value before setImmediate fires. Defer only telemetry. saveGlobalConfig((current) => ({ ...current, numStartups: (current.numStartups ?? 0) + 1, })); setImmediate(() => { void logStartupTelemetry(); logSessionTelemetry(); }); // Set up per-turn session environment data uploader (ant-only build). // Default-enabled for all ant users when working in an Anthropic-owned // repo. Captures git/filesystem state (NOT transcripts) at each turn so // environments can be recreated at any user message index. Gating: // - Build-time: this import is stubbed in external builds. // - Runtime: uploader checks github.com/anthropics/* remote + gcloud auth. // - Safety: CLAUDE_CODE_DISABLE_SESSION_DATA_UPLOAD=1 bypasses (tests set this). // Import is dynamic + async to avoid adding startup latency. const sessionUploaderPromise = process.env.USER_TYPE === "ant" ? import("./utils/sessionDataUploader.js") : null; // Defer session uploader resolution to the onTurnComplete callback to avoid // adding a new top-level await in main.tsx (performance-critical path). // The per-turn auth logic in sessionDataUploader.ts handles unauthenticated // state gracefully (re-checks each turn, so auth recovery mid-session works). const uploaderReady = sessionUploaderPromise ? sessionUploaderPromise .then((mod) => mod.createSessionTurnUploader()) .catch(() => null) : null; const sessionConfig = { debug: debug || debugToStderr, commands: [...commands, ...mcpCommands], initialTools, mcpClients, autoConnectIdeFlag: ide, mainThreadAgentDefinition, disableSlashCommands, dynamicMcpConfig, strictMcpConfig, systemPrompt, appendSystemPrompt, taskListId, thinkingConfig, ...(uploaderReady && { onTurnComplete: (messages: MessageType[]) => { void uploaderReady.then((uploader) => (uploader as ((msgs: MessageType[]) => void) | null)?.(messages), ); }, }), }; // Shared context for processResumedConversation calls const resumeContext = { modeApi: coordinatorModeModule, mainThreadAgentDefinition, agentDefinitions, currentCwd, cliAgents, initialState, }; if (options.continue) { // Continue the most recent conversation directly let resumeSucceeded = false; try { const resumeStart = performance.now(); // Clear stale caches before resuming to ensure fresh file/skill discovery const { clearSessionCaches } = await import("./commands/clear/caches.js"); clearSessionCaches(); const result = await loadConversationForResume( undefined /* sessionId */, undefined /* sourceFile */, ); if (!result) { logEvent("tengu_continue", { success: false, }); return await exitWithError( root, "No conversation found to continue", ); } const loaded = await processResumedConversation( result, { forkSession: !!options.forkSession, includeAttribution: true, transcriptPath: result.fullPath, }, resumeContext, ); if (loaded.restoredAgentDef) { mainThreadAgentDefinition = loaded.restoredAgentDef; } maybeActivateProactive(options); maybeActivateBrief(options); logEvent("tengu_continue", { success: true, resume_duration_ms: Math.round( performance.now() - resumeStart, ), }); resumeSucceeded = true; await launchRepl( root, { getFpsMetrics, stats, initialState: loaded.initialState, }, { ...sessionConfig, mainThreadAgentDefinition: loaded.restoredAgentDef ?? mainThreadAgentDefinition, initialMessages: loaded.messages, initialFileHistorySnapshots: loaded.fileHistorySnapshots, initialContentReplacements: loaded.contentReplacements, initialAgentName: loaded.agentName, initialAgentColor: loaded.agentColor, }, renderAndRun, ); } catch (error) { if (!resumeSucceeded) { logEvent("tengu_continue", { success: false, }); } logError(error); process.exit(1); } } else if (feature("DIRECT_CONNECT") && _pendingConnect?.url) { // `claude connect ` — full interactive TUI connected to a remote server let directConnectConfig; try { const session = await createDirectConnectSession({ serverUrl: _pendingConnect.url, authToken: _pendingConnect.authToken, cwd: getOriginalCwd(), dangerouslySkipPermissions: _pendingConnect.dangerouslySkipPermissions, }); if (session.workDir) { setOriginalCwd(session.workDir); setCwdState(session.workDir); } setDirectConnectServerUrl(_pendingConnect.url); directConnectConfig = session.config; } catch (err) { return await exitWithError( root, err instanceof DirectConnectError ? err.message : String(err), () => gracefulShutdown(1), ); } const connectInfoMessage = createSystemMessage( `Connected to server at ${_pendingConnect.url}\nSession: ${directConnectConfig.sessionId}`, "info", ); await launchRepl( root, { getFpsMetrics, stats, initialState }, { debug: debug || debugToStderr, commands, initialTools: [], initialMessages: [connectInfoMessage], mcpClients: [], autoConnectIdeFlag: ide, mainThreadAgentDefinition, disableSlashCommands, directConnectConfig, thinkingConfig, }, renderAndRun, ); return; } else if (feature("SSH_REMOTE") && _pendingSSH?.host) { // `claude ssh [dir]` — probe remote, deploy binary if needed, // spawn ssh with unix-socket -R forward to a local auth proxy, hand // the REPL an SSHSession. Tools run remotely, UI renders locally. // `--local` skips probe/deploy/ssh and spawns the current binary // directly with the same env — e2e test of the proxy/auth plumbing. const { createSSHSession, createLocalSSHSession, SSHSessionError, } = await import("./ssh/createSSHSession.js"); let sshSession: import('./ssh/createSSHSession.js').SSHSession | undefined; try { if (_pendingSSH.local) { process.stderr.write( "Starting local ssh-proxy test session...\n", ); sshSession = await createLocalSSHSession({ cwd: _pendingSSH.cwd, permissionMode: _pendingSSH.permissionMode, dangerouslySkipPermissions: _pendingSSH.dangerouslySkipPermissions, }); } else { process.stderr.write( `Connecting to ${_pendingSSH.host}…\n`, ); // In-place progress: \r + EL0 (erase to end of line). Final \n on // success so the next message lands on a fresh line. No-op when // stderr isn't a TTY (piped/redirected) — \r would just emit noise. const isTTY = process.stderr.isTTY; let hadProgress = false; sshSession = await createSSHSession( { host: _pendingSSH.host, cwd: _pendingSSH.cwd, localVersion: MACRO.VERSION, permissionMode: _pendingSSH.permissionMode, dangerouslySkipPermissions: _pendingSSH.dangerouslySkipPermissions, extraCliArgs: _pendingSSH.extraCliArgs, remoteBin: _pendingSSH.remoteBin, }, isTTY ? { onProgress: (msg: string) => { hadProgress = true; process.stderr.write( `\r ${msg}\x1b[K`, ); }, } : {}, ); if (hadProgress) process.stderr.write("\n"); } setOriginalCwd(sshSession.remoteCwd); setCwdState(sshSession.remoteCwd); setDirectConnectServerUrl( _pendingSSH.local ? "local" : _pendingSSH.host, ); } catch (err) { return await exitWithError( root, err instanceof SSHSessionError ? err.message : String(err), () => gracefulShutdown(1), ); } const sshInfoMessage = createSystemMessage( _pendingSSH.local ? `Local ssh-proxy test session\ncwd: ${sshSession.remoteCwd}\nAuth: unix socket → local proxy` : `SSH session to ${_pendingSSH.host}\nRemote cwd: ${sshSession.remoteCwd}\nAuth: unix socket -R → local proxy`, "info", ); await launchRepl( root, { getFpsMetrics, stats, initialState }, { debug: debug || debugToStderr, commands, initialTools: [], initialMessages: [sshInfoMessage], mcpClients: [], autoConnectIdeFlag: ide, mainThreadAgentDefinition, disableSlashCommands, sshSession, thinkingConfig, }, renderAndRun, ); return; } else if ( feature("KAIROS") && _pendingAssistantChat && (_pendingAssistantChat.sessionId || _pendingAssistantChat.discover) ) { // `claude assistant [sessionId]` — REPL as a pure viewer client // of a remote assistant session. The agentic loop runs remotely; this // process streams live events and POSTs messages. History is lazy- // loaded by useAssistantHistory on scroll-up (no blocking fetch here). const { discoverAssistantSessions } = await import("./assistant/sessionDiscovery.js"); let targetSessionId = _pendingAssistantChat.sessionId; // Discovery flow — list bridge environments, filter sessions if (!targetSessionId) { let sessions; try { sessions = await discoverAssistantSessions(); } catch (e) { return await exitWithError( root, `Failed to discover sessions: ${e instanceof Error ? e.message : e}`, () => gracefulShutdown(1), ); } if (sessions.length === 0) { let installedDir: string | null; try { installedDir = await launchAssistantInstallWizard(root); } catch (e) { return await exitWithError( root, `Assistant installation failed: ${e instanceof Error ? e.message : e}`, () => gracefulShutdown(1), ); } if (installedDir === null) { await gracefulShutdown(0); process.exit(0); } // The daemon needs a few seconds to spin up its worker and // establish a bridge session before discovery will find it. return await exitWithMessage( root, `Assistant installed in ${installedDir}. The daemon is starting up — run \`claude assistant\` again in a few seconds to connect.`, { exitCode: 0, beforeExit: () => gracefulShutdown(0), }, ); } if (sessions.length === 1) { targetSessionId = sessions[0]!.id; } else { const picked = await launchAssistantSessionChooser( root, { sessions, }, ); if (!picked) { await gracefulShutdown(0); process.exit(0); } targetSessionId = picked; } } // Auth — call prepareApiRequest() once for orgUUID, but use a // getAccessToken closure for the token so reconnects get fresh tokens. const { checkAndRefreshOAuthTokenIfNeeded, getClaudeAIOAuthTokens, } = await import("./utils/auth.js"); await checkAndRefreshOAuthTokenIfNeeded(); let apiCreds; try { apiCreds = await prepareApiRequest(); } catch (e) { return await exitWithError( root, `Error: ${e instanceof Error ? e.message : "Failed to authenticate"}`, () => gracefulShutdown(1), ); } const getAccessToken = (): string => getClaudeAIOAuthTokens()?.accessToken ?? apiCreds.accessToken; // Brief mode activation: setKairosActive(true) satisfies BOTH opt-in // and entitlement for isBriefEnabled() (BriefTool.ts:124-132). setKairosActive(true); setUserMsgOptIn(true); setIsRemoteMode(true); const remoteSessionConfig = createRemoteSessionConfig( targetSessionId, getAccessToken, apiCreds.orgUUID, /* hasInitialPrompt */ false, /* viewerOnly */ true, ); const infoMessage = createSystemMessage( `Attached to assistant session ${targetSessionId.slice(0, 8)}…`, "info", ); const assistantInitialState: AppState = { ...initialState, isBriefOnly: true, kairosEnabled: false, replBridgeEnabled: false, }; const remoteCommands = filterCommandsForRemoteMode(commands); await launchRepl( root, { getFpsMetrics, stats, initialState: assistantInitialState, }, { debug: debug || debugToStderr, commands: remoteCommands, initialTools: [], initialMessages: [infoMessage], mcpClients: [], autoConnectIdeFlag: ide, mainThreadAgentDefinition, disableSlashCommands, remoteSessionConfig, thinkingConfig, }, renderAndRun, ); return; } else if ( options.resume || options.fromPr || teleport || remote !== null ) { // Handle resume flow - from file (ant-only), session ID, or interactive selector // Clear stale caches before resuming to ensure fresh file/skill discovery const { clearSessionCaches } = await import("./commands/clear/caches.js"); clearSessionCaches(); let messages: MessageType[] | null = null; let processedResume: ProcessedResume | undefined; let maybeSessionId = validateUuid(options.resume); let searchTerm: string | undefined; // Store full LogOption when found by custom title (for cross-worktree resume) let matchedLog: LogOption | null = null; // PR filter for --from-pr flag let filterByPr: boolean | number | string | undefined; // Handle --from-pr flag if (options.fromPr) { if (options.fromPr === true) { // Show all sessions with linked PRs filterByPr = true; } else if (typeof options.fromPr === "string") { // Could be a PR number or URL filterByPr = options.fromPr; } } // If resume value is not a UUID, try exact match by custom title first if ( options.resume && typeof options.resume === "string" && !maybeSessionId ) { const trimmedValue = options.resume.trim(); if (trimmedValue) { const matches = await searchSessionsByCustomTitle( trimmedValue, { exact: true, }, ); if (matches.length === 1) { // Exact match found - store full LogOption for cross-worktree resume matchedLog = matches[0]!; maybeSessionId = getSessionIdFromLog(matchedLog) ?? null; } else { // No match or multiple matches - use as search term for picker searchTerm = trimmedValue; } } } // --remote and --teleport both create/resume Claude Code Web (CCR) sessions. // Remote Control (--rc) is a separate feature gated in initReplBridge.ts. if (remote !== null || teleport) { await waitForPolicyLimitsToLoad(); if (!isPolicyAllowed("allow_remote_sessions")) { return await exitWithError( root, "Error: Remote sessions are disabled by your organization's policy.", () => gracefulShutdown(1), ); } } if (remote !== null) { // Create remote session (optionally with initial prompt) const hasInitialPrompt = remote.length > 0; // Check if TUI mode is enabled - description is only optional in TUI mode const isRemoteTuiEnabled = getFeatureValue_CACHED_MAY_BE_STALE( "tengu_remote_backend", false, ); if (!isRemoteTuiEnabled && !hasInitialPrompt) { return await exitWithError( root, 'Error: --remote requires a description.\nUsage: claude --remote "your task description"', () => gracefulShutdown(1), ); } logEvent("tengu_remote_create_session", { has_initial_prompt: String( hasInitialPrompt, ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }); // Pass current branch so CCR clones the repo at the right revision const currentBranch = await getBranch(); const createdSession = await teleportToRemoteWithErrorHandling( root, hasInitialPrompt ? remote : null, new AbortController().signal, currentBranch || undefined, ); if (!createdSession) { logEvent("tengu_remote_create_session_error", { error: "unable_to_create_session" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }); return await exitWithError( root, "Error: Unable to create remote session", () => gracefulShutdown(1), ); } logEvent("tengu_remote_create_session_success", { session_id: createdSession.id as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }); // Check if new remote TUI mode is enabled via feature gate if (!isRemoteTuiEnabled) { // Original behavior: print session info and exit process.stdout.write( `Created remote session: ${createdSession.title}\n`, ); process.stdout.write( `View: ${getRemoteSessionUrl(createdSession.id)}?m=0\n`, ); process.stdout.write( `Resume with: claude --teleport ${createdSession.id}\n`, ); await gracefulShutdown(0); process.exit(0); } // New behavior: start local TUI with CCR engine // Mark that we're in remote mode for command visibility setIsRemoteMode(true); switchSession(asSessionId(createdSession.id)); // Get OAuth credentials for remote session let apiCreds: { accessToken: string; orgUUID: string }; try { apiCreds = await prepareApiRequest(); } catch (error) { logError(toError(error)); return await exitWithError( root, `Error: ${errorMessage(error) || "Failed to authenticate"}`, () => gracefulShutdown(1), ); } // Create remote session config for the REPL const { getClaudeAIOAuthTokens: getTokensForRemote } = await import("./utils/auth.js"); const getAccessTokenForRemote = (): string => getTokensForRemote()?.accessToken ?? apiCreds.accessToken; const remoteSessionConfig = createRemoteSessionConfig( createdSession.id, getAccessTokenForRemote, apiCreds.orgUUID, hasInitialPrompt, ); // Add remote session info as initial system message const remoteSessionUrl = `${getRemoteSessionUrl(createdSession.id)}?m=0`; const remoteInfoMessage = createSystemMessage( `/remote-control is active. Code in CLI or at ${remoteSessionUrl}`, "info", ); // Create initial user message from the prompt if provided (CCR echoes it back but we ignore that) const initialUserMessage = hasInitialPrompt ? createUserMessage({ content: remote }) : null; // Set remote session URL in app state for footer indicator const remoteInitialState = { ...initialState, remoteSessionUrl, }; // Pre-filter commands to only include remote-safe ones. // CCR's init response may further refine the list (via handleRemoteInit in REPL). const remoteCommands = filterCommandsForRemoteMode(commands); await launchRepl( root, { getFpsMetrics, stats, initialState: remoteInitialState, }, { debug: debug || debugToStderr, commands: remoteCommands, initialTools: [], initialMessages: initialUserMessage ? [remoteInfoMessage, initialUserMessage] : [remoteInfoMessage], mcpClients: [], autoConnectIdeFlag: ide, mainThreadAgentDefinition, disableSlashCommands, remoteSessionConfig, thinkingConfig, }, renderAndRun, ); return; } else if (teleport) { if (teleport === true || teleport === "") { // Interactive mode: show task selector and handle resume logEvent("tengu_teleport_interactive_mode", {}); logForDebugging( "selectAndResumeTeleportTask: Starting teleport flow...", ); const teleportResult = await launchTeleportResumeWrapper(root); if (!teleportResult) { // User cancelled or error occurred await gracefulShutdown(0); process.exit(0); } const { branchError } = await checkOutTeleportedSessionBranch( teleportResult.branch, ); messages = processMessagesForTeleportResume( teleportResult.log, branchError, ); } else if (typeof teleport === "string") { logEvent("tengu_teleport_resume_session", { mode: "direct" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }); try { // First, fetch session and validate repository before checking git state const sessionData = await fetchSession(teleport); const repoValidation = await validateSessionRepository(sessionData); // Handle repo mismatch or not in repo cases if ( repoValidation.status === "mismatch" || repoValidation.status === "not_in_repo" ) { const sessionRepo = repoValidation.sessionRepo; if (sessionRepo) { // Check for known paths const knownPaths = getKnownPathsForRepo(sessionRepo); const existingPaths = await filterExistingPaths(knownPaths); if (existingPaths.length > 0) { // Show directory switch dialog const selectedPath = await launchTeleportRepoMismatchDialog( root, { targetRepo: sessionRepo, initialPaths: existingPaths, }, ); if (selectedPath) { // Change to the selected directory process.chdir(selectedPath); setCwd(selectedPath); setOriginalCwd(selectedPath); } else { // User cancelled await gracefulShutdown(0); } } else { // No known paths - show original error throw new TeleportOperationError( `You must run claude --teleport ${teleport} from a checkout of ${sessionRepo}.`, chalk.red( `You must run claude --teleport ${teleport} from a checkout of ${chalk.bold(sessionRepo)}.\n`, ), ); } } } else if (repoValidation.status === "error") { throw new TeleportOperationError( repoValidation.errorMessage || "Failed to validate session", chalk.red( `Error: ${repoValidation.errorMessage || "Failed to validate session"}\n`, ), ); } await validateGitState(); // Use progress UI for teleport const { teleportWithProgress } = await import("./components/TeleportProgress.js"); const result = await teleportWithProgress( root, teleport, ); // Track teleported session for reliability logging setTeleportedSessionInfo({ sessionId: teleport }); messages = result.messages; } catch (error) { if (error instanceof TeleportOperationError) { process.stderr.write( error.formattedMessage + "\n", ); } else { logError(error); process.stderr.write( chalk.red( `Error: ${errorMessage(error)}\n`, ), ); } await gracefulShutdown(1); } } } if (process.env.USER_TYPE === "ant") { if ( options.resume && typeof options.resume === "string" && !maybeSessionId ) { // Check for ccshare URL (e.g. https://go/ccshare/boris-20260311-211036) const { parseCcshareId, loadCcshare } = await import("./utils/ccshareResume.js"); const ccshareId = parseCcshareId(options.resume); if (ccshareId) { try { const resumeStart = performance.now(); const logOption = await loadCcshare(ccshareId); const result = await loadConversationForResume( logOption, undefined, ); if (result) { processedResume = await processResumedConversation( result, { forkSession: true, transcriptPath: result.fullPath, }, resumeContext, ); if (processedResume.restoredAgentDef) { mainThreadAgentDefinition = processedResume.restoredAgentDef; } logEvent("tengu_session_resumed", { entrypoint: "ccshare" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, success: true, resume_duration_ms: Math.round( performance.now() - resumeStart, ), }); } else { logEvent("tengu_session_resumed", { entrypoint: "ccshare" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, success: false, }); } } catch (error) { logEvent("tengu_session_resumed", { entrypoint: "ccshare" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, success: false, }); logError(error); await exitWithError( root, `Unable to resume from ccshare: ${errorMessage(error)}`, () => gracefulShutdown(1), ); } } else { const resolvedPath = resolve(options.resume); try { const resumeStart = performance.now(); let logOption; try { // Attempt to load as a transcript file; ENOENT falls through to session-ID handling logOption = await loadTranscriptFromFile( resolvedPath, ); } catch (error) { if (!isENOENT(error)) throw error; // ENOENT: not a file path — fall through to session-ID handling } if (logOption) { const result = await loadConversationForResume( logOption, undefined /* sourceFile */, ); if (result) { processedResume = await processResumedConversation( result, { forkSession: !!options.forkSession, transcriptPath: result.fullPath, }, resumeContext, ); if (processedResume.restoredAgentDef) { mainThreadAgentDefinition = processedResume.restoredAgentDef; } logEvent("tengu_session_resumed", { entrypoint: "file" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, success: true, resume_duration_ms: Math.round( performance.now() - resumeStart, ), }); } else { logEvent("tengu_session_resumed", { entrypoint: "file" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, success: false, }); } } } catch (error) { logEvent("tengu_session_resumed", { entrypoint: "file" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, success: false, }); logError(error); await exitWithError( root, `Unable to load transcript from file: ${options.resume}`, () => gracefulShutdown(1), ); } } } } // If not loaded as a file, try as session ID if (maybeSessionId) { // Resume specific session by ID const sessionId = maybeSessionId; try { const resumeStart = performance.now(); // Use matchedLog if available (for cross-worktree resume by custom title) // Otherwise fall back to sessionId string (for direct UUID resume) const result = await loadConversationForResume( matchedLog ?? sessionId, undefined, ); if (!result) { logEvent("tengu_session_resumed", { entrypoint: "cli_flag" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, success: false, }); return await exitWithError( root, `No conversation found with session ID: ${sessionId}`, ); } const fullPath = matchedLog?.fullPath ?? result.fullPath; processedResume = await processResumedConversation( result, { forkSession: !!options.forkSession, sessionIdOverride: sessionId, transcriptPath: fullPath, }, resumeContext, ); if (processedResume.restoredAgentDef) { mainThreadAgentDefinition = processedResume.restoredAgentDef; } logEvent("tengu_session_resumed", { entrypoint: "cli_flag" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, success: true, resume_duration_ms: Math.round( performance.now() - resumeStart, ), }); } catch (error) { logEvent("tengu_session_resumed", { entrypoint: "cli_flag" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, success: false, }); logError(error); await exitWithError( root, `Failed to resume session ${sessionId}`, ); } } // Await file downloads before rendering REPL (files must be available) if (fileDownloadPromise) { try { const results = await fileDownloadPromise; const failedCount = count(results, (r) => !r.success); if (failedCount > 0) { process.stderr.write( chalk.yellow( `Warning: ${failedCount}/${results.length} file(s) failed to download.\n`, ), ); } } catch (error) { return await exitWithError( root, `Error downloading files: ${errorMessage(error)}`, ); } } // If we have a processed resume or teleport messages, render the REPL const resumeData = processedResume ?? (Array.isArray(messages) ? { messages, fileHistorySnapshots: undefined, agentName: undefined, agentColor: undefined as | AgentColorName | undefined, restoredAgentDef: mainThreadAgentDefinition, initialState, contentReplacements: undefined, } : undefined); if (resumeData) { maybeActivateProactive(options); maybeActivateBrief(options); await launchRepl( root, { getFpsMetrics, stats, initialState: resumeData.initialState, }, { ...sessionConfig, mainThreadAgentDefinition: resumeData.restoredAgentDef ?? mainThreadAgentDefinition, initialMessages: resumeData.messages, initialFileHistorySnapshots: resumeData.fileHistorySnapshots, initialContentReplacements: resumeData.contentReplacements, initialAgentName: resumeData.agentName, initialAgentColor: resumeData.agentColor, }, renderAndRun, ); } else { // Show interactive selector (includes same-repo worktrees) // Note: ResumeConversation loads logs internally to ensure proper GC after selection await launchResumeChooser( root, { getFpsMetrics, stats, initialState }, getWorktreePaths(getOriginalCwd()), { ...sessionConfig, initialSearchQuery: searchTerm, forkSession: options.forkSession, filterByPr, }, ); } } else { // Pass unresolved hooks promise to REPL so it can render immediately // instead of blocking ~500ms waiting for SessionStart hooks to finish. // REPL will inject hook messages when they resolve and await them before // the first API call so the model always sees hook context. const pendingHookMessages = hooksPromise && hookMessages.length === 0 ? hooksPromise : undefined; profileCheckpoint("action_after_hooks"); maybeActivateProactive(options); maybeActivateBrief(options); // Persist the current mode for fresh sessions so future resumes know what mode was used if (feature("COORDINATOR_MODE")) { saveMode( coordinatorModeModule?.isCoordinatorMode() ? "coordinator" : "normal", ); } // If launched via a deep link, show a provenance banner so the user // knows the session originated externally. Linux xdg-open and // browsers with "always allow" set dispatch the link with no OS-level // confirmation, so this is the only signal the user gets that the // prompt — and the working directory / CLAUDE.md it implies — came // from an external source rather than something they typed. let deepLinkBanner: ReturnType< typeof createSystemMessage > | null = null; if (feature("LODESTONE")) { if (options.deepLinkOrigin) { logEvent("tengu_deep_link_opened", { has_prefill: Boolean(options.prefill), has_repo: Boolean(options.deepLinkRepo), }); deepLinkBanner = createSystemMessage( buildDeepLinkBanner({ cwd: getCwd(), prefillLength: options.prefill?.length, repo: options.deepLinkRepo, lastFetch: options.deepLinkLastFetch !== undefined ? new Date(options.deepLinkLastFetch) : undefined, }), "warning", ); } else if (options.prefill) { deepLinkBanner = createSystemMessage( "Launched with a pre-filled prompt — review it before pressing Enter.", "warning", ); } } const initialMessages = deepLinkBanner ? [deepLinkBanner, ...hookMessages] : hookMessages.length > 0 ? hookMessages : undefined; await launchRepl( root, { getFpsMetrics, stats, initialState }, { ...sessionConfig, initialMessages, pendingHookMessages, }, renderAndRun, ); } }) .version( `${MACRO.VERSION} (Claude Code)`, "-v, --version", "Output the version number", ); // Worktree flags program.option( "-w, --worktree [name]", "Create a new git worktree for this session (optionally specify a name)", ); program.option( "--tmux", "Create a tmux session for the worktree (requires --worktree). Uses iTerm2 native panes when available; use --tmux=classic for traditional tmux.", ); if (canUserConfigureAdvisor()) { program.addOption( new Option( "--advisor ", "Enable the server-side advisor tool with the specified model (alias or full ID).", ).hideHelp(), ); } if (process.env.USER_TYPE === "ant") { program.addOption( new Option( "--delegate-permissions", "[ANT-ONLY] Alias for --permission-mode auto.", ).implies({ permissionMode: "auto", }), ); program.addOption( new Option( "--dangerously-skip-permissions-with-classifiers", "[ANT-ONLY] Deprecated alias for --permission-mode auto.", ) .hideHelp() .implies({ permissionMode: "auto" }), ); program.addOption( new Option( "--afk", "[ANT-ONLY] Deprecated alias for --permission-mode auto.", ) .hideHelp() .implies({ permissionMode: "auto" }), ); program.addOption( new Option( "--tasks [id]", '[ANT-ONLY] Tasks mode: watch for tasks and auto-process them. Optional id is used as both the task list ID and agent ID (defaults to "tasklist").', ) .argParser(String) .hideHelp(), ); program.option( "--agent-teams", "[ANT-ONLY] Force Claude to use multi-agent mode for solving problems", () => true, ); } if (feature("TRANSCRIPT_CLASSIFIER")) { program.addOption( new Option("--enable-auto-mode", "Opt in to auto mode").hideHelp(), ); } if (feature("PROACTIVE") || feature("KAIROS")) { program.addOption( new Option("--proactive", "Start in proactive autonomous mode"), ); } if (feature("UDS_INBOX")) { program.addOption( new Option( "--messaging-socket-path ", "Unix domain socket path for the UDS messaging server (defaults to a tmp path)", ), ); } if (feature("KAIROS") || feature("KAIROS_BRIEF")) { program.addOption( new Option( "--brief", "Enable SendUserMessage tool for agent-to-user communication", ), ); } if (feature("KAIROS")) { program.addOption( new Option( "--assistant", "Force assistant mode (Agent SDK daemon use)", ).hideHelp(), ); } program.addOption( new Option( "--channels ", "MCP servers whose channel notifications (inbound push) should register this session. Space-separated server names.", ).hideHelp(), ); program.addOption( new Option( "--dangerously-load-development-channels ", "Load channel servers not on the approved allowlist. For local channel development only. Shows a confirmation dialog at startup.", ).hideHelp(), ); // Teammate identity options (set by leader when spawning tmux teammates) // These replace the CLAUDE_CODE_* environment variables program.addOption( new Option("--agent-id ", "Teammate agent ID").hideHelp(), ); program.addOption( new Option("--agent-name ", "Teammate display name").hideHelp(), ); program.addOption( new Option( "--team-name ", "Team name for swarm coordination", ).hideHelp(), ); program.addOption( new Option("--agent-color ", "Teammate UI color").hideHelp(), ); program.addOption( new Option( "--plan-mode-required", "Require plan mode before implementation", ).hideHelp(), ); program.addOption( new Option( "--parent-session-id ", "Parent session ID for analytics correlation", ).hideHelp(), ); program.addOption( new Option( "--teammate-mode ", 'How to spawn teammates: "tmux", "in-process", or "auto"', ) .choices(["auto", "tmux", "in-process"]) .hideHelp(), ); program.addOption( new Option( "--agent-type ", "Custom agent type for this teammate", ).hideHelp(), ); // Enable SDK URL for all builds but hide from help program.addOption( new Option( "--sdk-url ", "Use remote WebSocket endpoint for SDK I/O streaming (only with -p and stream-json format)", ).hideHelp(), ); // Enable teleport/remote flags for all builds but keep them undocumented until GA program.addOption( new Option( "--teleport [session]", "Resume a teleport session, optionally specify session ID", ).hideHelp(), ); program.addOption( new Option( "--remote [description]", "Create a remote session with the given description", ).hideHelp(), ); if (feature("BRIDGE_MODE")) { program.addOption( new Option( "--remote-control [name]", "Start an interactive session with Remote Control enabled (optionally named)", ) .argParser((value) => value || true) .hideHelp(), ); program.addOption( new Option("--rc [name]", "Alias for --remote-control") .argParser((value) => value || true) .hideHelp(), ); } if (feature("HARD_FAIL")) { program.addOption( new Option( "--hard-fail", "Crash on logError calls instead of silently logging", ).hideHelp(), ); } profileCheckpoint("run_main_options_built"); // -p/--print mode: skip subcommand registration. The 52 subcommands // (mcp, auth, plugin, skill, task, config, doctor, update, etc.) are // never dispatched in print mode — commander routes the prompt to the // default action. The subcommand registration path was measured at ~65ms // on baseline — mostly the isBridgeEnabled() call (25ms settings Zod parse // + 40ms sync keychain subprocess), both hidden by the try/catch that // always returns false before enableConfigs(). cc:// URLs are rewritten to // `open` at main() line ~851 BEFORE this runs, so argv check is safe here. const isPrintMode = process.argv.includes("-p") || process.argv.includes("--print"); const isCcUrl = process.argv.some( (a) => a.startsWith("cc://") || a.startsWith("cc+unix://"), ); if (isPrintMode && !isCcUrl) { profileCheckpoint("run_before_parse"); await program.parseAsync(process.argv); profileCheckpoint("run_after_parse"); return program; } // claude mcp const mcp = program .command("mcp") .description("Configure and manage MCP servers") .configureHelp(createSortedHelpConfig()) .enablePositionalOptions(); mcp.command("serve") .description(`Start the Claude Code MCP server`) .option("-d, --debug", "Enable debug mode", () => true) .option( "--verbose", "Override verbose mode setting from config", () => true, ) .action( async ({ debug, verbose, }: { debug?: boolean; verbose?: boolean; }) => { const { mcpServeHandler } = await import("./cli/handlers/mcp.js"); await mcpServeHandler({ debug, verbose }); }, ); // Register the mcp add subcommand (extracted for testability) registerMcpAddCommand(mcp); if (isXaaEnabled()) { registerMcpXaaIdpCommand(mcp); } mcp.command("remove ") .description("Remove an MCP server") .option( "-s, --scope ", "Configuration scope (local, user, or project) - if not specified, removes from whichever scope it exists in", ) .action(async (name: string, options: { scope?: string }) => { const { mcpRemoveHandler } = await import("./cli/handlers/mcp.js"); await mcpRemoveHandler(name, options); }); mcp.command("list") .description( "List configured MCP servers. Note: The workspace trust dialog is skipped and stdio servers from .mcp.json are spawned for health checks. Only use this command in directories you trust.", ) .action(async () => { const { mcpListHandler } = await import("./cli/handlers/mcp.js"); await mcpListHandler(); }); mcp.command("get ") .description( "Get details about an MCP server. Note: The workspace trust dialog is skipped and stdio servers from .mcp.json are spawned for health checks. Only use this command in directories you trust.", ) .action(async (name: string) => { const { mcpGetHandler } = await import("./cli/handlers/mcp.js"); await mcpGetHandler(name); }); mcp.command("add-json ") .description("Add an MCP server (stdio or SSE) with a JSON string") .option( "-s, --scope ", "Configuration scope (local, user, or project)", "local", ) .option( "--client-secret", "Prompt for OAuth client secret (or set MCP_CLIENT_SECRET env var)", ) .action( async ( name: string, json: string, options: { scope?: string; clientSecret?: true }, ) => { const { mcpAddJsonHandler } = await import("./cli/handlers/mcp.js"); await mcpAddJsonHandler(name, json, options); }, ); mcp.command("add-from-claude-desktop") .description( "Import MCP servers from Claude Desktop (Mac and WSL only)", ) .option( "-s, --scope ", "Configuration scope (local, user, or project)", "local", ) .action(async (options: { scope?: string }) => { const { mcpAddFromDesktopHandler } = await import("./cli/handlers/mcp.js"); await mcpAddFromDesktopHandler(options); }); mcp.command("reset-project-choices") .description( "Reset all approved and rejected project-scoped (.mcp.json) servers within this project", ) .action(async () => { const { mcpResetChoicesHandler } = await import("./cli/handlers/mcp.js"); await mcpResetChoicesHandler(); }); // claude server if (feature("DIRECT_CONNECT")) { program .command("server") .description("Start a Claude Code session server") .option("--port ", "HTTP port", "0") .option("--host ", "Bind address", "0.0.0.0") .option("--auth-token ", "Bearer token for auth") .option("--unix ", "Listen on a unix domain socket") .option( "--workspace ", "Default working directory for sessions that do not specify cwd", ) .option( "--idle-timeout ", "Idle timeout for detached sessions in ms (0 = never expire)", "600000", ) .option( "--max-sessions ", "Maximum concurrent sessions (0 = unlimited)", "32", ) .action( async (opts: { port: string; host: string; authToken?: string; unix?: string; workspace?: string; idleTimeout: string; maxSessions: string; }) => { const { randomBytes } = await import("crypto"); const { startServer } = await import("./server/server.js"); const { SessionManager } = await import("./server/sessionManager.js"); const { DangerousBackend } = await import("./server/backends/dangerousBackend.js"); const { printBanner } = await import("./server/serverBanner.js"); const { createServerLogger } = await import("./server/serverLog.js"); const { writeServerLock, removeServerLock, probeRunningServer, } = await import("./server/lockfile.js"); const existing = await probeRunningServer(); if (existing) { process.stderr.write( `A claude server is already running (pid ${existing.pid}) at ${existing.httpUrl}\n`, ); process.exit(1); } const authToken = opts.authToken ?? `sk-ant-cc-${randomBytes(16).toString("base64url")}`; const config = { port: parseInt(opts.port, 10), host: opts.host, authToken, unix: opts.unix, workspace: opts.workspace, idleTimeoutMs: parseInt(opts.idleTimeout, 10), maxSessions: parseInt(opts.maxSessions, 10), }; const backend = new DangerousBackend(); const sessionManager = new SessionManager(backend, { idleTimeoutMs: config.idleTimeoutMs, maxSessions: config.maxSessions, }); const logger = createServerLogger(); const server = startServer(config, sessionManager, logger); const actualPort = server.port ?? config.port; printBanner(config, authToken, actualPort); await writeServerLock({ pid: process.pid, port: actualPort, host: config.host, httpUrl: config.unix ? `unix:${config.unix}` : `http://${config.host}:${actualPort}`, startedAt: Date.now(), }); let shuttingDown = false; const shutdown = async () => { if (shuttingDown) return; shuttingDown = true; // Stop accepting new connections before tearing down sessions. server.stop(true); await sessionManager.destroyAll(); await removeServerLock(); process.exit(0); }; process.once("SIGINT", () => void shutdown()); process.once("SIGTERM", () => void shutdown()); }, ); } // `claude ssh [dir]` — registered here only so --help shows it. // The actual interactive flow is handled by early argv rewriting in main() // (parallels the DIRECT_CONNECT/cc:// pattern above). If commander reaches // this action it means the argv rewrite didn't fire (e.g. user ran // `claude ssh` with no host) — just print usage. if (feature("SSH_REMOTE")) { program .command("ssh [dir]") .description( "Run Claude Code on a remote host over SSH. Deploys the binary and " + "tunnels API auth back through your local machine — no remote setup needed.", ) .option( "--permission-mode ", "Permission mode for the remote session", ) .option( "--dangerously-skip-permissions", "Skip all permission prompts on the remote (dangerous)", ) .option( "--remote-bin ", "Custom remote binary command (skips probe/deploy). " + "Example: --remote-bin 'bun /path/to/project/dist/cli.js'", ) .option( "--local", "e2e test mode — spawn the child CLI locally (skip ssh/deploy). " + "Exercises the auth proxy and unix-socket plumbing without a remote host.", ) .action(async () => { // Argv rewriting in main() should have consumed `ssh ` before // commander runs. Reaching here means host was missing or the // rewrite predicate didn't match. process.stderr.write( "Usage: claude ssh [dir]\n\n" + "Runs Claude Code on a remote Linux host. You don't need to install\n" + "anything on the remote or run `claude auth login` there — the binary is\n" + "deployed over SSH and API auth tunnels back through your local machine.\n", ); process.exit(1); }); } // claude connect — subcommand only handles -p (headless) mode. // Interactive mode (without -p) is handled by early argv rewriting in main() // which redirects to the main command with full TUI support. if (feature("DIRECT_CONNECT")) { program .command("open ") .description( "Connect to a Claude Code server (internal — use cc:// URLs)", ) .option("-p, --print [prompt]", "Print mode (headless)") .option( "--output-format ", "Output format: text, json, stream-json", "text", ) .action( async ( ccUrl: string, opts: { print?: string | true; outputFormat?: string; }, ) => { const { parseConnectUrl } = await import("./server/parseConnectUrl.js"); const { serverUrl, authToken } = parseConnectUrl(ccUrl); let connectConfig; try { const session = await createDirectConnectSession({ serverUrl, authToken, cwd: getOriginalCwd(), dangerouslySkipPermissions: _pendingConnect?.dangerouslySkipPermissions, }); if (session.workDir) { setOriginalCwd(session.workDir); setCwdState(session.workDir); } setDirectConnectServerUrl(serverUrl); connectConfig = session.config; } catch (err) { // biome-ignore lint/suspicious/noConsole: intentional error output console.error( err instanceof DirectConnectError ? err.message : String(err), ); process.exit(1); } const { runConnectHeadless } = await import("./server/connectHeadless.js"); const prompt = typeof opts.print === "string" ? opts.print : ""; const interactive = opts.print === true; await runConnectHeadless( connectConfig, prompt, opts.outputFormat, interactive, ); }, ); } // claude auth const auth = program .command("auth") .description("Manage authentication") .configureHelp(createSortedHelpConfig()); auth.command("login") .description("Sign in to your Anthropic account") .option( "--email ", "Pre-populate email address on the login page", ) .option("--sso", "Force SSO login flow") .option( "--console", "Use Anthropic Console (API usage billing) instead of Claude subscription", ) .option("--claudeai", "Use Claude subscription (default)") .action( async ({ email, sso, console: useConsole, claudeai, }: { email?: string; sso?: boolean; console?: boolean; claudeai?: boolean; }) => { const { authLogin } = await import("./cli/handlers/auth.js"); await authLogin({ email, sso, console: useConsole, claudeai }); }, ); auth.command("status") .description("Show authentication status") .option("--json", "Output as JSON (default)") .option("--text", "Output as human-readable text") .action(async (opts: { json?: boolean; text?: boolean }) => { const { authStatus } = await import("./cli/handlers/auth.js"); await authStatus(opts); }); auth.command("logout") .description("Log out from your Anthropic account") .action(async () => { const { authLogout } = await import("./cli/handlers/auth.js"); await authLogout(); }); /** * Helper function to handle marketplace command errors consistently. * Logs the error and exits the process with status 1. * @param error The error that occurred * @param action Description of the action that failed */ // Hidden flag on all plugin/marketplace subcommands to target cowork_plugins. const coworkOption = () => new Option("--cowork", "Use cowork_plugins directory").hideHelp(); // Plugin validate command const pluginCmd = program .command("plugin") .alias("plugins") .description("Manage Claude Code plugins") .configureHelp(createSortedHelpConfig()); pluginCmd .command("validate ") .description("Validate a plugin or marketplace manifest") .addOption(coworkOption()) .action(async (manifestPath: string, options: { cowork?: boolean }) => { const { pluginValidateHandler } = await import("./cli/handlers/plugins.js"); await pluginValidateHandler(manifestPath, options); }); // Plugin list command pluginCmd .command("list") .description("List installed plugins") .option("--json", "Output as JSON") .option( "--available", "Include available plugins from marketplaces (requires --json)", ) .addOption(coworkOption()) .action( async (options: { json?: boolean; available?: boolean; cowork?: boolean; }) => { const { pluginListHandler } = await import("./cli/handlers/plugins.js"); await pluginListHandler(options); }, ); // Marketplace subcommands const marketplaceCmd = pluginCmd .command("marketplace") .description("Manage Claude Code marketplaces") .configureHelp(createSortedHelpConfig()); marketplaceCmd .command("add ") .description("Add a marketplace from a URL, path, or GitHub repo") .addOption(coworkOption()) .option( "--sparse ", "Limit checkout to specific directories via git sparse-checkout (for monorepos). Example: --sparse .claude-plugin plugins", ) .option( "--scope ", "Where to declare the marketplace: user (default), project, or local", ) .action( async ( source: string, options: { cowork?: boolean; sparse?: string[]; scope?: string; }, ) => { const { marketplaceAddHandler } = await import("./cli/handlers/plugins.js"); await marketplaceAddHandler(source, options); }, ); marketplaceCmd .command("list") .description("List all configured marketplaces") .option("--json", "Output as JSON") .addOption(coworkOption()) .action(async (options: { json?: boolean; cowork?: boolean }) => { const { marketplaceListHandler } = await import("./cli/handlers/plugins.js"); await marketplaceListHandler(options); }); marketplaceCmd .command("remove ") .alias("rm") .description("Remove a configured marketplace") .addOption(coworkOption()) .action(async (name: string, options: { cowork?: boolean }) => { const { marketplaceRemoveHandler } = await import("./cli/handlers/plugins.js"); await marketplaceRemoveHandler(name, options); }); marketplaceCmd .command("update [name]") .description( "Update marketplace(s) from their source - updates all if no name specified", ) .addOption(coworkOption()) .action( async (name: string | undefined, options: { cowork?: boolean }) => { const { marketplaceUpdateHandler } = await import("./cli/handlers/plugins.js"); await marketplaceUpdateHandler(name, options); }, ); // Plugin install command pluginCmd .command("install ") .alias("i") .description( "Install a plugin from available marketplaces (use plugin@marketplace for specific marketplace)", ) .option( "-s, --scope ", "Installation scope: user, project, or local", "user", ) .addOption(coworkOption()) .action( async ( plugin: string, options: { scope?: string; cowork?: boolean }, ) => { const { pluginInstallHandler } = await import("./cli/handlers/plugins.js"); await pluginInstallHandler(plugin, options); }, ); // Plugin uninstall command pluginCmd .command("uninstall ") .alias("remove") .alias("rm") .description("Uninstall an installed plugin") .option( "-s, --scope ", "Uninstall from scope: user, project, or local", "user", ) .option( "--keep-data", "Preserve the plugin's persistent data directory (~/.claude/plugins/data/{id}/)", ) .addOption(coworkOption()) .action( async ( plugin: string, options: { scope?: string; cowork?: boolean; keepData?: boolean; }, ) => { const { pluginUninstallHandler } = await import("./cli/handlers/plugins.js"); await pluginUninstallHandler(plugin, options); }, ); // Plugin enable command pluginCmd .command("enable ") .description("Enable a disabled plugin") .option( "-s, --scope ", `Installation scope: ${VALID_INSTALLABLE_SCOPES.join(", ")} (default: auto-detect)`, ) .addOption(coworkOption()) .action( async ( plugin: string, options: { scope?: string; cowork?: boolean }, ) => { const { pluginEnableHandler } = await import("./cli/handlers/plugins.js"); await pluginEnableHandler(plugin, options); }, ); // Plugin disable command pluginCmd .command("disable [plugin]") .description("Disable an enabled plugin") .option("-a, --all", "Disable all enabled plugins") .option( "-s, --scope ", `Installation scope: ${VALID_INSTALLABLE_SCOPES.join(", ")} (default: auto-detect)`, ) .addOption(coworkOption()) .action( async ( plugin: string | undefined, options: { scope?: string; cowork?: boolean; all?: boolean }, ) => { const { pluginDisableHandler } = await import("./cli/handlers/plugins.js"); await pluginDisableHandler(plugin, options); }, ); // Plugin update command pluginCmd .command("update ") .description( "Update a plugin to the latest version (restart required to apply)", ) .option( "-s, --scope ", `Installation scope: ${VALID_UPDATE_SCOPES.join(", ")} (default: user)`, ) .addOption(coworkOption()) .action( async ( plugin: string, options: { scope?: string; cowork?: boolean }, ) => { const { pluginUpdateHandler } = await import("./cli/handlers/plugins.js"); await pluginUpdateHandler(plugin, options); }, ); // END ANT-ONLY // Setup token command program .command('setup-token') .description( 'Set up a long-lived authentication token (requires Claude subscription)', ) .action(async () => { const [{ setupTokenHandler }, { createRoot }] = await Promise.all([ import('./cli/handlers/util.js'), import('@anthropic/ink'), ]) const root = await createRoot(getBaseRenderOptions(false)) await setupTokenHandler(root) }) // Agents command - list configured agents program .command("agents") .description("List configured agents") .option( "--setting-sources ", "Comma-separated list of setting sources to load (user, project, local).", ) .action(async () => { const { agentsHandler } = await import("./cli/handlers/agents.js"); await agentsHandler(); process.exit(0); }); if (feature("TRANSCRIPT_CLASSIFIER")) { // Skip when tengu_auto_mode_config.enabled === 'disabled' (circuit breaker). // Reads from disk cache — GrowthBook isn't initialized at registration time. if (getAutoModeEnabledStateIfCached() !== "disabled") { const autoModeCmd = program .command("auto-mode") .description("Inspect auto mode classifier configuration"); autoModeCmd .command("defaults") .description( "Print the default auto mode environment, allow, and deny rules as JSON", ) .action(async () => { const { autoModeDefaultsHandler } = await import("./cli/handlers/autoMode.js"); autoModeDefaultsHandler(); process.exit(0); }); autoModeCmd .command("config") .description( "Print the effective auto mode config as JSON: your settings where set, defaults otherwise", ) .action(async () => { const { autoModeConfigHandler } = await import("./cli/handlers/autoMode.js"); autoModeConfigHandler(); process.exit(0); }); autoModeCmd .command("critique") .description("Get AI feedback on your custom auto mode rules") .option("--model ", "Override which model is used") .action(async (options) => { const { autoModeCritiqueHandler } = await import("./cli/handlers/autoMode.js"); await autoModeCritiqueHandler(options); process.exit(); }); } } // claude autonomy — CLI subcommands mirroring /autonomy slash command { const autonomyCmd = program .command("autonomy") .description("Inspect and manage automatic autonomy runs and flows"); autonomyCmd .command("status") .description("Print autonomy run, flow, team, pipe, and remote-control status") .option("--deep", "Include teams, pipes, daemon, and remote-control sections") .action(async (options: { deep?: boolean }) => { const { autonomyStatusHandler } = await import("./cli/handlers/autonomy.js"); await autonomyStatusHandler(options); process.exit(0); }); autonomyCmd .command("runs [limit]") .description("List recent autonomy runs") .action(async (limit?: string) => { const { autonomyRunsHandler } = await import("./cli/handlers/autonomy.js"); await autonomyRunsHandler(limit); process.exit(0); }); autonomyCmd .command("flows [limit]") .description("List recent autonomy flows") .action(async (limit?: string) => { const { autonomyFlowsHandler } = await import("./cli/handlers/autonomy.js"); await autonomyFlowsHandler(limit); process.exit(0); }); const flowCmd = autonomyCmd .command("flow ") .description("Inspect a single autonomy flow") .action(async (flowId: string) => { const { autonomyFlowHandler } = await import("./cli/handlers/autonomy.js"); await autonomyFlowHandler(flowId); process.exit(0); }); flowCmd .command("cancel ") .description("Cancel a queued, waiting, or running autonomy flow") .action(async (flowId: string) => { const { autonomyFlowCancelHandler } = await import("./cli/handlers/autonomy.js"); await autonomyFlowCancelHandler(flowId); process.exit(0); }); flowCmd .command("resume ") .description("Resume a waiting autonomy flow") .action(async (flowId: string) => { const { autonomyFlowResumeHandler } = await import("./cli/handlers/autonomy.js"); await autonomyFlowResumeHandler(flowId); process.exit(0); }); } // Remote Control command — connect local environment to claude.ai/code. // The actual command is intercepted by the fast-path in cli.tsx before // Commander.js runs, so this registration exists only for help output. // Always hidden: isBridgeEnabled() at this point (before enableConfigs) // would throw inside isClaudeAISubscriber → getGlobalConfig and return // false via the try/catch — but not before paying ~65ms of side effects // (25ms settings Zod parse + 40ms sync `security` keychain subprocess). // The dynamic visibility never worked; the command was always hidden. if (feature("BRIDGE_MODE")) { program .command("remote-control", { hidden: true }) .alias("rc") .description( "Connect your local environment for remote-control sessions via claude.ai/code", ) .action(async () => { // Unreachable — cli.tsx fast-path handles this command before main.tsx loads. // If somehow reached, delegate to bridgeMain. const { bridgeMain } = await import("./bridge/bridgeMain.js"); await bridgeMain(process.argv.slice(3)); }); } if (feature("KAIROS")) { program .command("assistant [sessionId]") .description( "Attach the REPL as a client to a running bridge session. Discovers sessions via API if no sessionId given.", ) .action(() => { // Argv rewriting above should have consumed `assistant [id]` // before commander runs. Reaching here means a root flag came first // (e.g. `--debug assistant`) and the position-0 predicate // didn't match. Print usage like the ssh stub does. process.stderr.write( "Usage: claude assistant [sessionId]\n\n" + "Attach the REPL as a viewer client to a running bridge session.\n" + "Omit sessionId to discover and pick from available sessions.\n", ); process.exit(1); }); } // Doctor command - check installation health program .command('doctor') .description( 'Check the health of your Claude Code auto-updater. Note: The workspace trust dialog is skipped and stdio servers from .mcp.json are spawned for health checks. Only use this command in directories you trust.', ) .action(async () => { const [{ doctorHandler }, { createRoot }] = await Promise.all([ import('./cli/handlers/util.js'), import('@anthropic/ink'), ]) const root = await createRoot(getBaseRenderOptions(false)) await doctorHandler(root) }) // claude up — run the project's CLAUDE.md "# claude up" setup instructions. if (process.env.USER_TYPE === "ant") { program .command("up") .description( '[ANT-ONLY] Initialize or upgrade the local dev environment using the "# claude up" section of the nearest CLAUDE.md', ) .action(async () => { const { up } = await import("src/cli/up.js"); await up(); }); } // claude rollback (ant-only) // Rolls back to previous releases if (process.env.USER_TYPE === "ant") { program .command("rollback [target]") .description( "[ANT-ONLY] Roll back to a previous release\n\nExamples:\n claude rollback Go 1 version back from current\n claude rollback 3 Go 3 versions back from current\n claude rollback 2.0.73-dev.20251217.t190658 Roll back to a specific version", ) .option("-l, --list", "List recent published versions with ages") .option( "--dry-run", "Show what would be installed without installing", ) .option( "--safe", "Roll back to the server-pinned safe version (set by oncall during incidents)", ) .action( async ( target?: string, options?: { list?: boolean; dryRun?: boolean; safe?: boolean; }, ) => { const { rollback } = await import("src/cli/rollback.js"); await rollback(target, options); }, ); } // claude install program .command("install [target]") .description( "Install Claude Code native build. Use [target] to specify version (stable, latest, or specific version)", ) .option("--force", "Force installation even if already installed") .action( async ( target: string | undefined, options: { force?: boolean }, ) => { const { installHandler } = await import("./cli/handlers/util.js"); await installHandler(target, options); }, ); // claude update — update ccb to the latest version via npm or bun program .command("update") .description("Update claude-code-best (ccb) to the latest version") .action(async () => { const { updateCCB } = await import("./cli/updateCCB.js"); await updateCCB(); }); // ant-only commands if (process.env.USER_TYPE === "ant") { const validateLogId = (value: string) => { const maybeSessionId = validateUuid(value); if (maybeSessionId) return maybeSessionId; return Number(value); }; // claude log program .command("log") .description("[ANT-ONLY] Manage conversation logs.") .argument( "[number|sessionId]", "A number (0, 1, 2, etc.) to display a specific log, or the sesssion ID (uuid) of a log", validateLogId, ) .action(async (logId: string | number | undefined) => { const { logHandler } = await import("./cli/handlers/ant.js"); await logHandler(logId); }); // claude error program .command("error") .description( "[ANT-ONLY] View error logs. Optionally provide a number (0, -1, -2, etc.) to display a specific log.", ) .argument( "[number]", "A number (0, 1, 2, etc.) to display a specific log", parseInt, ) .action(async (number: number | undefined) => { const { errorHandler } = await import("./cli/handlers/ant.js"); await errorHandler(number); }); // claude export program .command("export") .description("[ANT-ONLY] Export a conversation to a text file.") .usage(" ") .argument( "", "Session ID, log index (0, 1, 2...), or path to a .json/.jsonl log file", ) .argument("", "Output file path for the exported text") .addHelpText( "after", ` Examples: $ claude export 0 conversation.txt Export conversation at log index 0 $ claude export conversation.txt Export conversation by session ID $ claude export input.json output.txt Render JSON log file to text $ claude export .jsonl output.txt Render JSONL session file to text`, ) .action(async (source: string, outputFile: string) => { const { exportHandler } = await import("./cli/handlers/ant.js"); await exportHandler(source, outputFile); }); if (process.env.USER_TYPE === "ant") { const taskCmd = program .command("task") .description("[ANT-ONLY] Manage task list tasks"); taskCmd .command("create ") .description("Create a new task") .option("-d, --description ", "Task description") .option( "-l, --list ", 'Task list ID (defaults to "tasklist")', ) .action( async ( subject: string, opts: { description?: string; list?: string }, ) => { const { taskCreateHandler } = await import("./cli/handlers/ant.js"); await taskCreateHandler(subject, opts); }, ); taskCmd .command("list") .description("List all tasks") .option( "-l, --list ", 'Task list ID (defaults to "tasklist")', ) .option("--pending", "Show only pending tasks") .option("--json", "Output as JSON") .action( async (opts: { list?: string; pending?: boolean; json?: boolean; }) => { const { taskListHandler } = await import("./cli/handlers/ant.js"); await taskListHandler(opts); }, ); taskCmd .command("get ") .description("Get details of a task") .option( "-l, --list ", 'Task list ID (defaults to "tasklist")', ) .action(async (id: string, opts: { list?: string }) => { const { taskGetHandler } = await import("./cli/handlers/ant.js"); await taskGetHandler(id, opts); }); taskCmd .command("update ") .description("Update a task") .option( "-l, --list ", 'Task list ID (defaults to "tasklist")', ) .option( "-s, --status ", `Set status (${TASK_STATUSES.join(", ")})`, ) .option("--subject ", "Update subject") .option("-d, --description ", "Update description") .option("--owner ", "Set owner") .option("--clear-owner", "Clear owner") .action( async ( id: string, opts: { list?: string; status?: string; subject?: string; description?: string; owner?: string; clearOwner?: boolean; }, ) => { const { taskUpdateHandler } = await import("./cli/handlers/ant.js"); await taskUpdateHandler(id, opts); }, ); taskCmd .command("dir") .description("Show the tasks directory path") .option( "-l, --list ", 'Task list ID (defaults to "tasklist")', ) .action(async (opts: { list?: string }) => { const { taskDirHandler } = await import("./cli/handlers/ant.js"); await taskDirHandler(opts); }); } // claude completion program .command("completion ", { hidden: true }) .description( "Generate shell completion script (bash, zsh, or fish)", ) .option( "--output ", "Write completion script directly to a file instead of stdout", ) .action(async (shell: string, opts: { output?: string }) => { const { completionHandler } = await import("./cli/handlers/ant.js"); await completionHandler(shell, opts, program); }); } profileCheckpoint("run_before_parse"); await program.parseAsync(process.argv); profileCheckpoint("run_after_parse"); // Record final checkpoint for total_time calculation profileCheckpoint("main_after_run"); // Log startup perf to Statsig (sampled) and output detailed report if enabled profileReport(); return program; } async function logTenguInit({ hasInitialPrompt, hasStdin, verbose, debug, debugToStderr, print, outputFormat, inputFormat, numAllowedTools, numDisallowedTools, mcpClientCount, worktreeEnabled, skipWebFetchPreflight, githubActionInputs, dangerouslySkipPermissionsPassed, permissionMode, modeIsBypass, allowDangerouslySkipPermissionsPassed, systemPromptFlag, appendSystemPromptFlag, thinkingConfig, assistantActivationPath, }: { hasInitialPrompt: boolean; hasStdin: boolean; verbose: boolean; debug: boolean; debugToStderr: boolean; print: boolean; outputFormat: string; inputFormat: string; numAllowedTools: number; numDisallowedTools: number; mcpClientCount: number; worktreeEnabled: boolean; skipWebFetchPreflight: boolean | undefined; githubActionInputs: string | undefined; dangerouslySkipPermissionsPassed: boolean; permissionMode: string; modeIsBypass: boolean; allowDangerouslySkipPermissionsPassed: boolean; systemPromptFlag: "file" | "flag" | undefined; appendSystemPromptFlag: "file" | "flag" | undefined; thinkingConfig: ThinkingConfig; assistantActivationPath: string | undefined; }): Promise { try { logEvent("tengu_init", { entrypoint: "claude" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, hasInitialPrompt, hasStdin, verbose, debug, debugToStderr, print, outputFormat: outputFormat as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, inputFormat: inputFormat as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, numAllowedTools, numDisallowedTools, mcpClientCount, worktree: worktreeEnabled, skipWebFetchPreflight, ...(githubActionInputs && { githubActionInputs: githubActionInputs as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }), dangerouslySkipPermissionsPassed, permissionMode: permissionMode as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, modeIsBypass, inProtectedNamespace: isInProtectedNamespace(), allowDangerouslySkipPermissionsPassed, thinkingType: thinkingConfig.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, ...(systemPromptFlag && { systemPromptFlag: systemPromptFlag as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }), ...(appendSystemPromptFlag && { appendSystemPromptFlag: appendSystemPromptFlag as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }), is_simple: isBareMode() || undefined, is_coordinator: feature("COORDINATOR_MODE") && coordinatorModeModule?.isCoordinatorMode() ? true : undefined, ...(assistantActivationPath && { assistantActivationPath: assistantActivationPath as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }), autoUpdatesChannel: (getInitialSettings().autoUpdatesChannel ?? "latest") as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, ...(process.env.USER_TYPE === "ant" ? (() => { const cwd = getCwd(); const gitRoot = findGitRoot(cwd); const rp = gitRoot ? relative(gitRoot, cwd) || "." : undefined; return rp ? { relativeProjectPath: rp as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, } : {}; })() : {}), }); } catch (error) { logError(error); } } function maybeActivateProactive(options: unknown): void { if ( (feature("PROACTIVE") || feature("KAIROS")) && ((options as { proactive?: boolean }).proactive || isEnvTruthy(process.env.CLAUDE_CODE_PROACTIVE)) ) { // eslint-disable-next-line @typescript-eslint/no-require-imports const proactiveModule = require("./proactive/index.js"); if (!proactiveModule.isProactiveActive()) { proactiveModule.activateProactive("command"); } } } function maybeActivateBrief(options: unknown): void { if (!(feature("KAIROS") || feature("KAIROS_BRIEF"))) return; const briefFlag = (options as { brief?: boolean }).brief; const briefEnv = isEnvTruthy(process.env.CLAUDE_CODE_BRIEF); if (!briefFlag && !briefEnv) return; // --brief / CLAUDE_CODE_BRIEF are explicit opt-ins: check entitlement, // then set userMsgOptIn to activate the tool + prompt section. The env // var also grants entitlement (isBriefEntitled() reads it), so setting // CLAUDE_CODE_BRIEF=1 alone force-enables for dev/testing — no GB gate // needed. initialIsBriefOnly reads getUserMsgOptIn() directly. // Conditional require: static import would leak the tool name string // into external builds via BriefTool.ts → prompt.ts. /* eslint-disable @typescript-eslint/no-require-imports */ const { isBriefEntitled } = require("@claude-code-best/builtin-tools/tools/BriefTool/BriefTool.js") as typeof import("@claude-code-best/builtin-tools/tools/BriefTool/BriefTool.js"); /* eslint-enable @typescript-eslint/no-require-imports */ const entitled = isBriefEntitled(); if (entitled) { setUserMsgOptIn(true); } // Fire unconditionally once intent is seen: enabled=false captures the // "user tried but was gated" failure mode in Datadog. logEvent("tengu_brief_mode_enabled", { enabled: entitled, gated: !entitled, source: (briefEnv ? "env" : "flag") as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }); } function resetCursor() { const terminal = process.stderr.isTTY ? process.stderr : process.stdout.isTTY ? process.stdout : undefined; terminal?.write(SHOW_CURSOR); } type TeammateOptions = { agentId?: string; agentName?: string; teamName?: string; agentColor?: string; planModeRequired?: boolean; parentSessionId?: string; teammateMode?: "auto" | "tmux" | "in-process"; agentType?: string; }; function extractTeammateOptions(options: unknown): TeammateOptions { if (typeof options !== "object" || options === null) { return {}; } const opts = options as Record; const teammateMode = opts.teammateMode; return { agentId: typeof opts.agentId === "string" ? opts.agentId : undefined, agentName: typeof opts.agentName === "string" ? opts.agentName : undefined, teamName: typeof opts.teamName === "string" ? opts.teamName : undefined, agentColor: typeof opts.agentColor === "string" ? opts.agentColor : undefined, planModeRequired: typeof opts.planModeRequired === "boolean" ? opts.planModeRequired : undefined, parentSessionId: typeof opts.parentSessionId === "string" ? opts.parentSessionId : undefined, teammateMode: teammateMode === "auto" || teammateMode === "tmux" || teammateMode === "in-process" ? teammateMode : undefined, agentType: typeof opts.agentType === "string" ? opts.agentType : undefined, }; }