diff --git a/src/buddy/useBuddyNotification.tsx b/src/buddy/useBuddyNotification.tsx
index 2f1cfe3..6453163 100644
--- a/src/buddy/useBuddyNotification.tsx
+++ b/src/buddy/useBuddyNotification.tsx
@@ -10,12 +10,12 @@ import { getRainbowColor } from '../utils/thinking.js';
// buzz instead of a single UTC-midnight spike, gentler on soul-gen load.
// Teaser window: April 1-7, 2026 only. Command stays live forever after.
export function isBuddyTeaserWindow(): boolean {
- if (("external" as string) === 'ant') return true;
+ if ((process.env.USER_TYPE) === 'ant') return true;
const d = new Date();
return d.getFullYear() === 2026 && d.getMonth() === 3 && d.getDate() <= 7;
}
export function isBuddyLive(): boolean {
- if (("external" as string) === 'ant') return true;
+ if ((process.env.USER_TYPE) === 'ant') return true;
const d = new Date();
return d.getFullYear() > 2026 || d.getFullYear() === 2026 && d.getMonth() >= 3;
}
diff --git a/src/commands/mcp/mcp.tsx b/src/commands/mcp/mcp.tsx
index 2456ea8..be0f074 100644
--- a/src/commands/mcp/mcp.tsx
+++ b/src/commands/mcp/mcp.tsx
@@ -77,7 +77,7 @@ export async function call(onDone: LocalJSXCommandOnDone, _context: unknown, arg
}
// Redirect base /mcp command to /plugins installed tab for ant users
- if (("external" as string) === 'ant') {
+ if ((process.env.USER_TYPE) === 'ant') {
return ;
}
return ;
diff --git a/src/commands/terminalSetup/terminalSetup.tsx b/src/commands/terminalSetup/terminalSetup.tsx
index 4fd8b4e..694a7b5 100644
--- a/src/commands/terminalSetup/terminalSetup.tsx
+++ b/src/commands/terminalSetup/terminalSetup.tsx
@@ -119,7 +119,7 @@ export async function setupTerminal(theme: ThemeName): Promise {
maybeMarkProjectOnboardingComplete();
// Install shell completions (ant-only, since the completion command is ant-only)
- if (("external" as string) === 'ant') {
+ if ((process.env.USER_TYPE) === 'ant') {
result += await setupShellCompletion(theme);
}
return result;
diff --git a/src/commands/thinkback/thinkback.tsx b/src/commands/thinkback/thinkback.tsx
index a8bb1ee..2c50b1b 100644
--- a/src/commands/thinkback/thinkback.tsx
+++ b/src/commands/thinkback/thinkback.tsx
@@ -29,10 +29,10 @@ const INTERNAL_MARKETPLACE_NAME = 'claude-code-marketplace';
const INTERNAL_MARKETPLACE_REPO = 'anthropics/claude-code-marketplace';
const OFFICIAL_MARKETPLACE_REPO = 'anthropics/claude-plugins-official';
function getMarketplaceName(): string {
- return ("external" as string) === 'ant' ? INTERNAL_MARKETPLACE_NAME : OFFICIAL_MARKETPLACE_NAME;
+ return (process.env.USER_TYPE) === 'ant' ? INTERNAL_MARKETPLACE_NAME : OFFICIAL_MARKETPLACE_NAME;
}
function getMarketplaceRepo(): string {
- return ("external" as string) === 'ant' ? INTERNAL_MARKETPLACE_REPO : OFFICIAL_MARKETPLACE_REPO;
+ return (process.env.USER_TYPE) === 'ant' ? INTERNAL_MARKETPLACE_REPO : OFFICIAL_MARKETPLACE_REPO;
}
function getPluginId(): string {
return `thinkback@${getMarketplaceName()}`;
diff --git a/src/commands/ultraplan.tsx b/src/commands/ultraplan.tsx
index 0d52baa..a5a9f64 100644
--- a/src/commands/ultraplan.tsx
+++ b/src/commands/ultraplan.tsx
@@ -53,7 +53,7 @@ const DEFAULT_INSTRUCTIONS: string = (typeof _rawPrompt === 'string' ? _rawPromp
// Shell-set env only, so top-level process.env read is fine
// — settings.env never injects this.
/* eslint-disable custom-rules/no-process-env-top-level, custom-rules/no-sync-fs -- ant-only dev override; eager top-level read is the point (crash at startup, not silently inside the slash-command try/catch) */
-const ULTRAPLAN_INSTRUCTIONS: string = ("external" as string) === 'ant' && process.env.ULTRAPLAN_PROMPT_FILE ? readFileSync(process.env.ULTRAPLAN_PROMPT_FILE, 'utf8').trimEnd() : DEFAULT_INSTRUCTIONS;
+const ULTRAPLAN_INSTRUCTIONS: string = (process.env.USER_TYPE) === 'ant' && process.env.ULTRAPLAN_PROMPT_FILE ? readFileSync(process.env.ULTRAPLAN_PROMPT_FILE, 'utf8').trimEnd() : DEFAULT_INSTRUCTIONS;
/* eslint-enable custom-rules/no-process-env-top-level, custom-rules/no-sync-fs */
/**
@@ -464,7 +464,7 @@ export default {
name: 'ultraplan',
description: `~10–30 min · Claude Code on the web drafts an advanced plan you can edit and approve. See ${CCR_TERMS_URL}`,
argumentHint: '',
- isEnabled: () => ("external" as string) === 'ant',
+ isEnabled: () => (process.env.USER_TYPE) === 'ant',
load: () => Promise.resolve({
call
})
diff --git a/src/components/DevBar.tsx b/src/components/DevBar.tsx
index bce6c2f..bf99f32 100644
--- a/src/components/DevBar.tsx
+++ b/src/components/DevBar.tsx
@@ -6,7 +6,7 @@ import { Text, useInterval } from '../ink.js';
// Show DevBar for dev builds or all ants
function shouldShowDevBar(): boolean {
- return ("production" as string) === 'development' || ("external" as string) === 'ant';
+ return ("production" as string) === 'development' || (process.env.USER_TYPE) === 'ant';
}
export function DevBar() {
const $ = _c(5);
diff --git a/src/components/Feedback.tsx b/src/components/Feedback.tsx
index d03bdbb..18603d6 100644
--- a/src/components/Feedback.tsx
+++ b/src/components/Feedback.tsx
@@ -32,7 +32,7 @@ import TextInput from './TextInput.js';
// This value was determined experimentally by testing the URL length limit
const GITHUB_URL_LIMIT = 7250;
-const GITHUB_ISSUES_REPO_URL = ("external" as string) === 'ant' ? 'https://github.com/anthropics/claude-cli-internal/issues' : 'https://github.com/anthropics/claude-code/issues';
+const GITHUB_ISSUES_REPO_URL = (process.env.USER_TYPE) === 'ant' ? 'https://github.com/anthropics/claude-cli-internal/issues' : 'https://github.com/anthropics/claude-code/issues';
type Props = {
abortSignal: AbortSignal;
messages: Message[];
diff --git a/src/components/FeedbackSurvey/useMemorySurvey.tsx b/src/components/FeedbackSurvey/useMemorySurvey.tsx
index e1e6b85..bd28ee6 100644
--- a/src/components/FeedbackSurvey/useMemorySurvey.tsx
+++ b/src/components/FeedbackSurvey/useMemorySurvey.tsx
@@ -87,7 +87,7 @@ export function useMemorySurvey(messages: Message[], isLoading: boolean, hasActi
});
}, []);
const shouldShowTranscriptPrompt = useCallback((selected_0: FeedbackSurveyResponse) => {
- if (("external" as string) !== 'ant') {
+ if ((process.env.USER_TYPE) !== 'ant') {
return false;
}
if (selected_0 !== 'bad' && selected_0 !== 'good') {
diff --git a/src/components/LogoV2/feedConfigs.tsx b/src/components/LogoV2/feedConfigs.tsx
index 71a5526..cf88419 100644
--- a/src/components/LogoV2/feedConfigs.tsx
+++ b/src/components/LogoV2/feedConfigs.tsx
@@ -26,7 +26,7 @@ export function createRecentActivityFeed(activities: LogOption[]): FeedConfig {
}
export function createWhatsNewFeed(releaseNotes: string[]): FeedConfig {
const lines: FeedLine[] = releaseNotes.map(note => {
- if (("external" as string) === 'ant') {
+ if ((process.env.USER_TYPE) === 'ant') {
const match = note.match(/^(\d+\s+\w+\s+ago)\s+(.+)$/);
if (match) {
return {
@@ -39,9 +39,9 @@ export function createWhatsNewFeed(releaseNotes: string[]): FeedConfig {
text: note
};
});
- const emptyMessage = ("external" as string) === 'ant' ? 'Unable to fetch latest claude-cli-internal commits' : 'Check the Claude Code changelog for updates';
+ const emptyMessage = (process.env.USER_TYPE) === 'ant' ? 'Unable to fetch latest claude-cli-internal commits' : 'Check the Claude Code changelog for updates';
return {
- title: ("external" as string) === 'ant' ? "What's new [ANT-ONLY: Latest CC commits]" : "What's new",
+ title: (process.env.USER_TYPE) === 'ant' ? "What's new [ANT-ONLY: Latest CC commits]" : "What's new",
lines,
footer: lines.length > 0 ? '/release-notes for more' : undefined,
emptyMessage
diff --git a/src/components/MemoryUsageIndicator.tsx b/src/components/MemoryUsageIndicator.tsx
index 5fae7d9..c7b0fef 100644
--- a/src/components/MemoryUsageIndicator.tsx
+++ b/src/components/MemoryUsageIndicator.tsx
@@ -7,7 +7,7 @@ export function MemoryUsageIndicator(): React.ReactNode {
// the hook means the 10s polling interval is never set up in external builds.
// USER_TYPE is a build-time constant, so the hook call below is either always
// reached or dead-code-eliminated — never conditional at runtime.
- if (("external" as string) !== 'ant') {
+ if ((process.env.USER_TYPE) !== 'ant') {
return null;
}
diff --git a/src/components/MessageSelector.tsx b/src/components/MessageSelector.tsx
index 888ec93..8ee0890 100644
--- a/src/components/MessageSelector.tsx
+++ b/src/components/MessageSelector.tsx
@@ -118,7 +118,7 @@ export function MessageSelector({
...summarizeInputProps,
onChange: setSummarizeFromFeedback
});
- if (("external" as string) === 'ant') {
+ if ((process.env.USER_TYPE) === 'ant') {
baseOptions.push({
value: 'summarize_up_to',
label: 'Summarize up to here',
diff --git a/src/components/NativeAutoUpdater.tsx b/src/components/NativeAutoUpdater.tsx
index 2438a11..3d860c2 100644
--- a/src/components/NativeAutoUpdater.tsx
+++ b/src/components/NativeAutoUpdater.tsx
@@ -184,7 +184,7 @@ export function NativeAutoUpdater({
{autoUpdaterResult?.status === 'install_failed' &&
✗ Auto-update failed · Try /status
}
- {maxVersionIssue && ("external" as string) === 'ant' &&
+ {maxVersionIssue && (process.env.USER_TYPE) === 'ant' &&
⚠ Known issue: {maxVersionIssue} · Run{' '}
claude rollback --safe to downgrade
}
diff --git a/src/components/PromptInput/PromptInput.tsx b/src/components/PromptInput/PromptInput.tsx
index dbcf36d..bc80851 100644
--- a/src/components/PromptInput/PromptInput.tsx
+++ b/src/components/PromptInput/PromptInput.tsx
@@ -294,8 +294,8 @@ function PromptInput({
// otherwise bridge becomes an invisible selection stop.
const bridgeFooterVisible = replBridgeConnected && (replBridgeExplicit || replBridgeReconnecting);
// Tmux pill (ant-only) — visible when there's an active tungsten session
- const hasTungstenSession = useAppState(s => ("external" as string) === 'ant' && s.tungstenActiveSession !== undefined);
- const tmuxFooterVisible = ("external" as string) === 'ant' && hasTungstenSession;
+ const hasTungstenSession = useAppState(s => (process.env.USER_TYPE) === 'ant' && s.tungstenActiveSession !== undefined);
+ const tmuxFooterVisible = (process.env.USER_TYPE) === 'ant' && hasTungstenSession;
// WebBrowser pill — visible when a browser is open
const bagelFooterVisible = useAppState(s => false);
const teamContext = useAppState(s => s.teamContext);
@@ -391,7 +391,7 @@ function PromptInput({
// exist. When only local_agent tasks are running (coordinator/fork mode), the
// pill is absent, so the -1 sentinel would leave nothing visually selected.
// In that case, skip -1 and treat 0 as the minimum selectable index.
- const hasBgTaskPill = useMemo(() => Object.values(tasks).some(t => isBackgroundTask(t) && !(("external" as string) === 'ant' && isPanelAgentTask(t))), [tasks]);
+ const hasBgTaskPill = useMemo(() => Object.values(tasks).some(t => isBackgroundTask(t) && !((process.env.USER_TYPE) === 'ant' && isPanelAgentTask(t))), [tasks]);
const minCoordinatorIndex = hasBgTaskPill ? -1 : 0;
// Clamp index when tasks complete and the list shrinks beneath the cursor
useEffect(() => {
@@ -455,7 +455,7 @@ function PromptInput({
// Panel shows retained-completed agents too (getVisibleAgentTasks), so the
// pill must stay navigable whenever the panel has rows — not just when
// something is running.
- const tasksFooterVisible = (runningTaskCount > 0 || ("external" as string) === 'ant' && coordinatorTaskCount > 0) && !shouldHideTasksFooter(tasks, showSpinnerTree);
+ const tasksFooterVisible = (runningTaskCount > 0 || (process.env.USER_TYPE) === 'ant' && coordinatorTaskCount > 0) && !shouldHideTasksFooter(tasks, showSpinnerTree);
const teamsFooterVisible = cachedTeams.length > 0;
const footerItems = useMemo(() => [tasksFooterVisible && 'tasks', tmuxFooterVisible && 'tmux', bagelFooterVisible && 'bagel', teamsFooterVisible && 'teams', bridgeFooterVisible && 'bridge', companionFooterVisible && 'companion'].filter(Boolean) as FooterItem[], [tasksFooterVisible, tmuxFooterVisible, bagelFooterVisible, teamsFooterVisible, bridgeFooterVisible, companionFooterVisible]);
@@ -1742,7 +1742,7 @@ function PromptInput({
useKeybindings({
'footer:up': () => {
// ↑ scrolls within the coordinator task list before leaving the pill
- if (tasksSelected && ("external" as string) === 'ant' && coordinatorTaskCount > 0 && coordinatorTaskIndex > minCoordinatorIndex) {
+ if (tasksSelected && (process.env.USER_TYPE) === 'ant' && coordinatorTaskCount > 0 && coordinatorTaskIndex > minCoordinatorIndex) {
setCoordinatorTaskIndex(prev => prev - 1);
return;
}
@@ -1750,7 +1750,7 @@ function PromptInput({
},
'footer:down': () => {
// ↓ scrolls within the coordinator task list, never leaves the pill
- if (tasksSelected && ("external" as string) === 'ant' && coordinatorTaskCount > 0) {
+ if (tasksSelected && (process.env.USER_TYPE) === 'ant' && coordinatorTaskCount > 0) {
if (coordinatorTaskIndex < coordinatorTaskCount - 1) {
setCoordinatorTaskIndex(prev => prev + 1);
}
@@ -1813,7 +1813,7 @@ function PromptInput({
}
break;
case 'tmux':
- if (("external" as string) === 'ant') {
+ if ((process.env.USER_TYPE) === 'ant') {
setAppState(prev => prev.tungstenPanelAutoHidden ? {
...prev,
tungstenPanelAutoHidden: false
diff --git a/src/components/PromptInput/PromptInputFooter.tsx b/src/components/PromptInput/PromptInputFooter.tsx
index 8aacad1..8f23dfd 100644
--- a/src/components/PromptInput/PromptInputFooter.tsx
+++ b/src/components/PromptInput/PromptInputFooter.tsx
@@ -143,11 +143,11 @@ function PromptInputFooter({
{isFullscreen ? null : }
- {("external" as string) === 'ant' && isUndercover() && undercover}
+ {(process.env.USER_TYPE) === 'ant' && isUndercover() && undercover}
- {("external" as string) === 'ant' && }
+ {(process.env.USER_TYPE) === 'ant' && }
>;
}
export default memo(PromptInputFooter);
diff --git a/src/components/PromptInput/PromptInputFooterLeftSide.tsx b/src/components/PromptInput/PromptInputFooterLeftSide.tsx
index ed8c672..381dcc5 100644
--- a/src/components/PromptInput/PromptInputFooterLeftSide.tsx
+++ b/src/components/PromptInput/PromptInputFooterLeftSide.tsx
@@ -260,7 +260,7 @@ function ModeIndicator({
const expandedView = useAppState(s_3 => s_3.expandedView);
const showSpinnerTree = expandedView === 'teammates';
const prStatus = usePrStatus(isLoading, isPrStatusEnabled());
- const hasTmuxSession = useAppState(s_4 => ("external" as string) === 'ant' && s_4.tungstenActiveSession !== undefined);
+ const hasTmuxSession = useAppState(s_4 => (process.env.USER_TYPE) === 'ant' && s_4.tungstenActiveSession !== undefined);
const nextTickAt = useSyncExternalStore(proactiveModule?.subscribeToProactiveChanges ?? NO_OP_SUBSCRIBE, proactiveModule?.getNextTickAt ?? NULL, NULL);
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
const voiceEnabled = feature('VOICE_MODE') ? useVoiceEnabled() : false;
@@ -274,7 +274,7 @@ function ModeIndicator({
const selGetState = useSelection().getState;
const hasNextTick = nextTickAt !== null;
const isCoordinator = feature('COORDINATOR_MODE') ? coordinatorModule?.isCoordinatorMode() === true : false;
- const runningTaskCount = useMemo(() => count(Object.values(tasks), t => isBackgroundTask(t) && !(("external" as string) === 'ant' && isPanelAgentTask(t))), [tasks]);
+ const runningTaskCount = useMemo(() => count(Object.values(tasks), t => isBackgroundTask(t) && !((process.env.USER_TYPE) === 'ant' && isPanelAgentTask(t))), [tasks]);
const tasksV2 = useTasksV2();
const hasTaskItems = tasksV2 !== undefined && tasksV2.length > 0;
const escShortcut = useShortcutDisplay('chat:cancel', 'Chat', 'esc').toLowerCase();
@@ -365,7 +365,7 @@ function ModeIndicator({
// its click-target Box isn't nested inside the
// wrapper (reconciler throws on Box-in-Text).
// Tmux pill (ant-only) — appears right after tasks in nav order
- ...(("external" as string) === 'ant' && hasTmuxSession ? [] : []), ...(isAgentSwarmsEnabled() && hasTeams ? [] : []), ...(shouldShowPrStatus ? [] : [])];
+ ...((process.env.USER_TYPE) === 'ant' && hasTmuxSession ? [] : []), ...(isAgentSwarmsEnabled() && hasTeams ? [] : []), ...(shouldShowPrStatus ? [] : [])];
// Check if any in-process teammates exist (for hint text cycling)
const hasAnyInProcessTeammates = Object.values(tasks).some(t_2 => t_2.type === 'in_process_teammate' && t_2.status === 'running');
@@ -399,7 +399,7 @@ function ModeIndicator({
}
// Add "↓ to manage tasks" hint when panel has visible rows
- const hasCoordinatorTasks = ("external" as string) === 'ant' && getVisibleAgentTasks(tasks).length > 0;
+ const hasCoordinatorTasks = (process.env.USER_TYPE) === 'ant' && getVisibleAgentTasks(tasks).length > 0;
// Tasks pill renders as a Box sibling (not a parts entry) so its
// click-target Box isn't nested inside — the
diff --git a/src/components/Settings/Config.tsx b/src/components/Settings/Config.tsx
index df8c71f..7d0595f 100644
--- a/src/components/Settings/Config.tsx
+++ b/src/components/Settings/Config.tsx
@@ -392,7 +392,7 @@ export function Config({
}
}] : []),
// Speculation toggle (ant-only)
- ...(("external" as string) === 'ant' ? [{
+ ...((process.env.USER_TYPE) === 'ant' ? [{
id: 'speculationEnabled',
label: 'Speculative execution',
value: globalConfig.speculationEnabled ?? true,
diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx
index d1084f2..9ac9ffe 100644
--- a/src/components/Spinner.tsx
+++ b/src/components/Spinner.tsx
@@ -220,7 +220,7 @@ function SpinnerWithVerbInner({
// doesn't trigger re-renders; we pick up updates on the parent's ~25x/turn
// re-render cadence, same as the old ApiMetricsLine did.
let ttftText: string | null = null;
- if (("external" as string) === 'ant' && apiMetricsRef?.current && apiMetricsRef.current.length > 0) {
+ if ((process.env.USER_TYPE) === 'ant' && apiMetricsRef?.current && apiMetricsRef.current.length > 0) {
ttftText = computeTtftText(apiMetricsRef.current);
}
diff --git a/src/components/Stats.tsx b/src/components/Stats.tsx
index d58b9d2..d3d9521 100644
--- a/src/components/Stats.tsx
+++ b/src/components/Stats.tsx
@@ -512,7 +512,7 @@ function OverviewTab({
{/* Speculation time saved (ant-only) */}
- {("external" as string) === 'ant' && stats.totalSpeculationTimeSavedMs > 0 &&
+ {(process.env.USER_TYPE) === 'ant' && stats.totalSpeculationTimeSavedMs > 0 &&
Speculation saved:{' '}
@@ -1151,7 +1151,7 @@ function renderOverviewToAnsi(stats: ClaudeCodeStats): string[] {
lines.push(row('Active days', activeDaysVal, 'Peak hour', peakHourVal));
// Speculation time saved (ant-only)
- if (("external" as string) === 'ant' && stats.totalSpeculationTimeSavedMs > 0) {
+ if ((process.env.USER_TYPE) === 'ant' && stats.totalSpeculationTimeSavedMs > 0) {
const label = 'Speculation saved:'.padEnd(COL1_LABEL_WIDTH);
lines.push(label + h(formatDuration(stats.totalSpeculationTimeSavedMs)));
}
diff --git a/src/components/agents/ToolSelector.tsx b/src/components/agents/ToolSelector.tsx
index dadecfd..27766ab 100644
--- a/src/components/agents/ToolSelector.tsx
+++ b/src/components/agents/ToolSelector.tsx
@@ -58,7 +58,7 @@ function getToolBuckets(): ToolBuckets {
},
EXECUTION: {
name: 'Execution tools',
- toolNames: new Set([BashTool.name, ("external" as string) === 'ant' ? TungstenTool.name : undefined].filter(n => n !== undefined))
+ toolNames: new Set([BashTool.name, (process.env.USER_TYPE) === 'ant' ? TungstenTool.name : undefined].filter(n => n !== undefined))
},
MCP: {
name: 'MCP tools',
diff --git a/src/components/messages/AttachmentMessage.tsx b/src/components/messages/AttachmentMessage.tsx
index fb34022..3b23cd8 100644
--- a/src/components/messages/AttachmentMessage.tsx
+++ b/src/components/messages/AttachmentMessage.tsx
@@ -114,7 +114,7 @@ export function AttachmentMessage({
// names — shortId is undefined outside ant builds anyway.
const names = attachment.skills.map(s => s.shortId ? `${s.name} [${s.shortId}]` : s.name).join(', ');
const firstId = attachment.skills[0]?.shortId;
- const hint = ("external" as string) === 'ant' && !isDemoEnv && firstId ? ` · /skill-feedback ${firstId} 1=wrong 2=noisy 3=good [comment]` : '';
+ const hint = (process.env.USER_TYPE) === 'ant' && !isDemoEnv && firstId ? ` · /skill-feedback ${firstId} 1=wrong 2=noisy 3=good [comment]` : '';
return
{attachment.skills.length} relevant{' '}
{plural(attachment.skills.length, 'skill')}: {names}
diff --git a/src/components/permissions/BashPermissionRequest/bashToolUseOptions.tsx b/src/components/permissions/BashPermissionRequest/bashToolUseOptions.tsx
index 36170b8..f1f7c4a 100644
--- a/src/components/permissions/BashPermissionRequest/bashToolUseOptions.tsx
+++ b/src/components/permissions/BashPermissionRequest/bashToolUseOptions.tsx
@@ -112,7 +112,7 @@ export function bashToolUseOptions({
// Skip when the editable prefix option is already shown — they serve the
// same role and having two identical-looking "don't ask again" inputs is confusing.
const editablePrefixShown = options.some(o => o.value === 'yes-prefix-edited');
- if (("external" as string) === 'ant' && !editablePrefixShown && isClassifierPermissionsEnabled() && onClassifierDescriptionChange && !initialClassifierDescriptionEmpty && !descriptionAlreadyExists(classifierDescription ?? '', existingAllowDescriptions) && decisionReason?.type !== 'classifier') {
+ if ((process.env.USER_TYPE) === 'ant' && !editablePrefixShown && isClassifierPermissionsEnabled() && onClassifierDescriptionChange && !initialClassifierDescriptionEmpty && !descriptionAlreadyExists(classifierDescription ?? '', existingAllowDescriptions) && decisionReason?.type !== 'classifier') {
options.push({
type: 'input',
label: 'Yes, and don\u2019t ask again for',
diff --git a/src/components/tasks/taskStatusUtils.tsx b/src/components/tasks/taskStatusUtils.tsx
index 8a113b2..a70cbd6 100644
--- a/src/components/tasks/taskStatusUtils.tsx
+++ b/src/components/tasks/taskStatusUtils.tsx
@@ -96,7 +96,7 @@ export function shouldHideTasksFooter(tasks: {
if (!showSpinnerTree) return false;
let hasVisibleTask = false;
for (const t of Object.values(tasks) as TaskState[]) {
- if (!isBackgroundTask(t) || ("external" as string) === 'ant' && isPanelAgentTask(t)) {
+ if (!isBackgroundTask(t) || (process.env.USER_TYPE) === 'ant' && isPanelAgentTask(t)) {
continue;
}
hasVisibleTask = true;
diff --git a/src/main.tsx b/src/main.tsx
index 27cd8a3..ccb6097 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -262,13 +262,10 @@ function isBeingDebugged() {
}
}
-// Exit if we detect node debugging or inspection
-if (("external" as string) !== 'ant' && isBeingDebugged()) {
- // Use process.exit directly here since we're in the top-level code before imports
- // and gracefulShutdown is not yet available
- // eslint-disable-next-line custom-rules/no-top-level-side-effects
- process.exit(1);
-}
+// Anti-debugging check disabled for local development
+// if ((process.env.USER_TYPE) !== 'ant' && isBeingDebugged()) {
+// process.exit(1);
+// }
/**
* Per-session skill/plugin telemetry. Called from both the interactive path
@@ -337,7 +334,7 @@ function runMigrations(): void {
if (feature('TRANSCRIPT_CLASSIFIER')) {
resetAutoModeOptInForDefaultOffer();
}
- if (("external" as string) === 'ant') {
+ if ((process.env.USER_TYPE) === 'ant') {
migrateFennecToOpus();
}
saveGlobalConfig(prev => prev.migrationVersion === CURRENT_MIGRATION_VERSION ? prev : {
@@ -425,7 +422,7 @@ export function startDeferredPrefetches(): void {
}
// Event loop stall detector — logs when the main thread is blocked >500ms
- if (("external" as string) === 'ant') {
+ if ((process.env.USER_TYPE) === 'ant') {
void import('./utils/eventLoopStallDetector.js').then(m => m.startEventLoopStallDetector());
}
}
@@ -1134,11 +1131,11 @@ async function run(): Promise {
const disableSlashCommands = options.disableSlashCommands || false;
// Extract tasks mode options (ant-only)
- const tasksOption = ("external" as string) === 'ant' && (options as {
+ 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 (("external" as string) === 'ant' && taskListId) {
+ if ((process.env.USER_TYPE) === 'ant' && taskListId) {
process.env.CLAUDE_CODE_TASK_LIST_ID = taskListId;
}
@@ -1528,7 +1525,7 @@ async function run(): Promise {
};
// Store the explicit CLI flag so teammates can inherit it
setChromeFlagOverride(chromeOpts.chrome);
- const enableClaudeInChrome = shouldEnableClaudeInChrome(chromeOpts.chrome) && (("external" as string) === 'ant' || isClaudeAISubscriber());
+ const enableClaudeInChrome = shouldEnableClaudeInChrome(chromeOpts.chrome) && ((process.env.USER_TYPE) === 'ant' || isClaudeAISubscriber());
const autoEnableClaudeInChrome = !enableClaudeInChrome && shouldAutoEnableClaudeInChrome();
if (enableClaudeInChrome) {
const platform = getPlatform();
@@ -1760,7 +1757,7 @@ async function run(): Promise {
} = initResult;
// Handle overly broad shell allow rules for ant users (Bash(*), PowerShell(*))
- if (("external" as string) === 'ant' && overlyBroadBashPermissions.length > 0) {
+ 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}`);
}
@@ -2010,7 +2007,7 @@ async function run(): Promise {
// - 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 (("external" as string) === 'ant' && explicitModel && explicitModel !== 'default' && !hasGrowthBookEnvOverride('tengu_ant_model_override') && getGlobalConfig().cachedGrowthBookFeatures?.['tengu_ant_model_override'] == null) {
+ if ((process.env.USER_TYPE) === 'ant' && explicitModel && explicitModel !== 'default' && !hasGrowthBookEnvOverride('tengu_ant_model_override') && getGlobalConfig().cachedGrowthBookFeatures?.['tengu_ant_model_override'] == null) {
await initializeGrowthBook();
}
@@ -2156,7 +2153,7 @@ async function run(): Promise {
// Log agent memory loaded event for tmux teammates
if (customAgent.memory) {
logEvent('tengu_agent_memory_loaded', {
- ...(("external" as string) === 'ant' && {
+ ...((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,
@@ -2220,7 +2217,7 @@ async function run(): Promise {
getFpsMetrics = ctx.getFpsMetrics;
stats = ctx.stats;
// Install asciicast recorder before Ink mounts (ant-only, opt-in via CLAUDE_CODE_TERMINAL_RECORDING=1)
- if (("external" as string) === 'ant') {
+ if ((process.env.USER_TYPE) === 'ant') {
installAsciicastRecorder();
}
const {
@@ -2816,7 +2813,7 @@ async function run(): Promise {
if (!isBareMode()) {
startDeferredPrefetches();
void import('./utils/backgroundHousekeeping.js').then(m => m.startBackgroundHousekeeping());
- if (("external" as string) === 'ant') {
+ if ((process.env.USER_TYPE) === 'ant') {
void import('./utils/sdkHeapDumpMonitor.js').then(m => m.startSdkMemoryMonitor());
}
}
@@ -3061,7 +3058,7 @@ async function run(): Promise {
// - 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 = ("external" as string) === 'ant' ? import('./utils/sessionDataUploader.js') : null;
+ 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).
@@ -3578,7 +3575,7 @@ async function run(): Promise {
}
}
}
- if (("external" as string) === 'ant') {
+ 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 {
@@ -3813,7 +3810,7 @@ async function run(): Promise {
if (canUserConfigureAdvisor()) {
program.addOption(new Option('--advisor ', 'Enable the server-side advisor tool with the specified model (alias or full ID).').hideHelp());
}
- if (("external" as string) === 'ant') {
+ if ((process.env.USER_TYPE) === 'ant') {
program.addOption(new Option('--delegate-permissions', '[ANT-ONLY] Alias for --permission-mode auto.').implies({
permissionMode: 'auto'
}));
@@ -4367,7 +4364,7 @@ async function run(): Promise {
});
// claude up — run the project's CLAUDE.md "# claude up" setup instructions.
- if (("external" as string) === 'ant') {
+ 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
@@ -4378,7 +4375,7 @@ async function run(): Promise {
// claude rollback (ant-only)
// Rolls back to previous releases
- if (("external" as string) === 'ant') {
+ 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;
@@ -4402,7 +4399,7 @@ async function run(): Promise {
});
// ant-only commands
- if (("external" as string) === 'ant') {
+ if ((process.env.USER_TYPE) === 'ant') {
const validateLogId = (value: string) => {
const maybeSessionId = validateUuid(value);
if (maybeSessionId) return maybeSessionId;
@@ -4436,7 +4433,7 @@ Examples:
} = await import('./cli/handlers/ant.js');
await exportHandler(source, outputFile);
});
- if (("external" as string) === 'ant') {
+ 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;
@@ -4595,7 +4592,7 @@ async function logTenguInit({
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,
- ...(("external" as string) === 'ant' ? (() => {
+ ...((process.env.USER_TYPE) === 'ant' ? (() => {
const cwd = getCwd();
const gitRoot = findGitRoot(cwd);
const rp = gitRoot ? relative(gitRoot, cwd) || '.' : undefined;
diff --git a/src/screens/REPL.tsx b/src/screens/REPL.tsx
index 858b2f9..8abe3a4 100644
--- a/src/screens/REPL.tsx
+++ b/src/screens/REPL.tsx
@@ -104,13 +104,13 @@ const VoiceKeybindingHandler: typeof import('../hooks/useVoiceIntegration.js').V
// Frustration detection is ant-only (dogfooding). Conditional require so external
// builds eliminate the module entirely (including its two O(n) useMemos that run
// on every messages change, plus the GrowthBook fetch).
-const useFrustrationDetection: typeof import('../components/FeedbackSurvey/useFrustrationDetection.js').useFrustrationDetection = ("external" as string) === 'ant' ? require('../components/FeedbackSurvey/useFrustrationDetection.js').useFrustrationDetection : () => ({
+const useFrustrationDetection: typeof import('../components/FeedbackSurvey/useFrustrationDetection.js').useFrustrationDetection = (process.env.USER_TYPE) === 'ant' ? require('../components/FeedbackSurvey/useFrustrationDetection.js').useFrustrationDetection : () => ({
state: 'closed',
handleTranscriptSelect: () => {}
});
// Ant-only org warning. Conditional require so the org UUID list is
// eliminated from external builds (one UUID is on excluded-strings).
-const useAntOrgWarningNotification: typeof import('../hooks/notifs/useAntOrgWarningNotification.js').useAntOrgWarningNotification = ("external" as string) === 'ant' ? require('../hooks/notifs/useAntOrgWarningNotification.js').useAntOrgWarningNotification : () => {};
+const useAntOrgWarningNotification: typeof import('../hooks/notifs/useAntOrgWarningNotification.js').useAntOrgWarningNotification = (process.env.USER_TYPE) === 'ant' ? require('../hooks/notifs/useAntOrgWarningNotification.js').useAntOrgWarningNotification : () => {};
// Dead code elimination: conditional import for coordinator mode
const getCoordinatorUserContext: (mcpClients: ReadonlyArray<{
name: string;
@@ -219,9 +219,9 @@ import { EffortCallout, shouldShowEffortCallout } from '../components/EffortCall
import type { EffortValue } from '../utils/effort.js';
import { RemoteCallout } from '../components/RemoteCallout.js';
/* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
-const AntModelSwitchCallout = ("external" as string) === 'ant' ? require('../components/AntModelSwitchCallout.js').AntModelSwitchCallout : null;
-const shouldShowAntModelSwitch = ("external" as string) === 'ant' ? require('../components/AntModelSwitchCallout.js').shouldShowModelSwitchCallout : (): boolean => false;
-const UndercoverAutoCallout = ("external" as string) === 'ant' ? require('../components/UndercoverAutoCallout.js').UndercoverAutoCallout : null;
+const AntModelSwitchCallout = (process.env.USER_TYPE) === 'ant' ? require('../components/AntModelSwitchCallout.js').AntModelSwitchCallout : null;
+const shouldShowAntModelSwitch = (process.env.USER_TYPE) === 'ant' ? require('../components/AntModelSwitchCallout.js').shouldShowModelSwitchCallout : (): boolean => false;
+const UndercoverAutoCallout = (process.env.USER_TYPE) === 'ant' ? require('../components/UndercoverAutoCallout.js').UndercoverAutoCallout : null;
/* eslint-enable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
import { activityManager } from '../utils/activityManager.js';
import { createAbortController } from '../utils/abortController.js';
@@ -602,7 +602,7 @@ export function REPL({
// Env-var gates hoisted to mount-time — isEnvTruthy does toLowerCase+trim+
// includes, and these were on the render path (hot during PageUp spam).
const titleDisabled = useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_TERMINAL_TITLE), []);
- const moreRightEnabled = useMemo(() => ("external" as string) === 'ant' && isEnvTruthy(process.env.CLAUDE_MORERIGHT), []);
+ const moreRightEnabled = useMemo(() => (process.env.USER_TYPE) === 'ant' && isEnvTruthy(process.env.CLAUDE_MORERIGHT), []);
const disableVirtualScroll = useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_VIRTUAL_SCROLL), []);
const disableMessageActions = feature('MESSAGE_ACTIONS') ?
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
@@ -734,7 +734,7 @@ export function REPL({
const [showIdeOnboarding, setShowIdeOnboarding] = useState(false);
// Dead code elimination: model switch callout state (ant-only)
const [showModelSwitchCallout, setShowModelSwitchCallout] = useState(() => {
- if (("external" as string) === 'ant') {
+ if ((process.env.USER_TYPE) === 'ant') {
return shouldShowAntModelSwitch();
}
return false;
@@ -1013,7 +1013,7 @@ export function REPL({
}, []);
const [showUndercoverCallout, setShowUndercoverCallout] = useState(false);
useEffect(() => {
- if (("external" as string) === 'ant') {
+ if ((process.env.USER_TYPE) === 'ant') {
void (async () => {
// Wait for repo classification to settle (memoized, no-op if primed).
const {
@@ -2045,10 +2045,10 @@ export function REPL({
if (allowDialogsWithAnimation && showIdeOnboarding) return 'ide-onboarding';
// Model switch callout (ant-only, eliminated from external builds)
- if (("external" as string) === 'ant' && allowDialogsWithAnimation && showModelSwitchCallout) return 'model-switch';
+ if ((process.env.USER_TYPE) === 'ant' && allowDialogsWithAnimation && showModelSwitchCallout) return 'model-switch';
// Undercover auto-enable explainer (ant-only, eliminated from external builds)
- if (("external" as string) === 'ant' && allowDialogsWithAnimation && showUndercoverCallout) return 'undercover-callout';
+ if ((process.env.USER_TYPE) === 'ant' && allowDialogsWithAnimation && showUndercoverCallout) return 'undercover-callout';
// Effort callout (shown once for Opus 4.6 users when effort is enabled)
if (allowDialogsWithAnimation && showEffortCallout) return 'effort-callout';
@@ -2486,7 +2486,7 @@ export function REPL({
dynamicSkillDirTriggers: new Set(),
discoveredSkillNames: discoveredSkillNamesRef.current,
setResponseLength,
- pushApiMetricsEntry: ("external" as string) === 'ant' ? (ttftMs: number) => {
+ pushApiMetricsEntry: (process.env.USER_TYPE) === 'ant' ? (ttftMs: number) => {
const now = Date.now();
const baseline = responseLengthRef.current;
apiMetricsRef.current.push({
@@ -2815,7 +2815,7 @@ export function REPL({
// Capture ant-only API metrics before resetLoadingState clears the ref.
// For multi-request turns (tool use loops), compute P50 across all requests.
- if (("external" as string) === 'ant' && apiMetricsRef.current.length > 0) {
+ if ((process.env.USER_TYPE) === 'ant' && apiMetricsRef.current.length > 0) {
const entries = apiMetricsRef.current;
const ttfts = entries.map(e => e.ttftMs);
// Compute per-request OTPS using only active streaming time and
@@ -2943,7 +2943,7 @@ export function REPL({
// minutes — wiping the session made the pill disappear entirely, forcing
// the user to re-invoke Tmux just to peek. Skip on abort so the panel
// stays open for inspection (matches the turn-duration guard below).
- if (("external" as string) === 'ant' && !abortController.signal.aborted) {
+ if ((process.env.USER_TYPE) === 'ant' && !abortController.signal.aborted) {
setAppState(prev => {
if (prev.tungstenActiveSession === undefined) return prev;
if (prev.tungstenPanelAutoHidden === true) return prev;
@@ -3066,7 +3066,7 @@ export function REPL({
}
// Atomically: clear initial message, set permission mode and rules, and store plan for verification
- const shouldStorePlanForVerification = initialMsg.message.planContent && ("external" as string) === 'ant' && isEnvTruthy(undefined);
+ const shouldStorePlanForVerification = initialMsg.message.planContent && (process.env.USER_TYPE) === 'ant' && isEnvTruthy(undefined);
setAppState(prev => {
// Build and apply permission updates (mode + allowedPrompts rules)
let updatedToolPermissionContext = initialMsg.mode ? applyPermissionUpdates(prev.toolPermissionContext, buildPermissionUpdates(initialMsg.mode, initialMsg.allowedPrompts)) : prev.toolPermissionContext;
@@ -3599,7 +3599,7 @@ export function REPL({
// Handler for when user presses 1 on survey thanks screen to share details
const handleSurveyRequestFeedback = useCallback(() => {
- const command = ("external" as string) === 'ant' ? '/issue' : '/feedback';
+ const command = (process.env.USER_TYPE) === 'ant' ? '/issue' : '/feedback';
onSubmit(command, {
setCursorOffset: () => {},
clearBuffer: () => {},
@@ -4060,7 +4060,7 @@ export function REPL({
// - Workers receive permission responses via mailbox messages
// - Leaders receive permission requests via mailbox messages
- if (("external" as string) === 'ant') {
+ if ((process.env.USER_TYPE) === 'ant') {
// Tasks mode: watch for tasks and auto-process them
// eslint-disable-next-line react-hooks/rules-of-hooks
// biome-ignore lint/correctness/useHookAtTopLevel: conditional for dead code elimination in external builds
@@ -4169,7 +4169,7 @@ export function REPL({
// Fall back to default behavior
const hookType = currentHooks[0]?.data.hookEvent === 'SubagentStop' ? 'subagent stop' : 'stop';
- if (("external" as string) === 'ant') {
+ if ((process.env.USER_TYPE) === 'ant') {
const cmd = currentHooks[completedCount]?.data.command;
const label = cmd ? ` '${truncateToWidth(cmd, 40)}'` : '';
return total === 1 ? `running ${hookType} hook${label}` : `running ${hookType} hook${label}\u2026 ${completedCount}/${total}`;
@@ -4578,7 +4578,7 @@ export function REPL({
{toolJSX && !(toolJSX.isLocalJSXCommand && toolJSX.isImmediate) && !toolJsxCentered &&
{toolJSX.jsx}
}
- {("external" as string) === 'ant' && }
+ {(process.env.USER_TYPE) === 'ant' && }
{feature('WEB_BROWSER_TOOL') ? WebBrowserPanelModule && : null}
{showSpinner && 0} leaderIsIdle={!isLoading} />}
@@ -4801,7 +4801,7 @@ export function REPL({
});
}} />}
{focusedInputDialog === 'ide-onboarding' && setShowIdeOnboarding(false)} installationStatus={ideInstallationStatus} />}
- {("external" as string) === 'ant' && focusedInputDialog === 'model-switch' && AntModelSwitchCallout && {
+ {(process.env.USER_TYPE) === 'ant' && focusedInputDialog === 'model-switch' && AntModelSwitchCallout && {
setShowModelSwitchCallout(false);
if (selection === 'switch' && modelAlias) {
setAppState(prev => ({
@@ -4811,7 +4811,7 @@ export function REPL({
}));
}
}} />}
- {("external" as string) === 'ant' && focusedInputDialog === 'undercover-callout' && UndercoverAutoCallout && setShowUndercoverCallout(false)} />}
+ {(process.env.USER_TYPE) === 'ant' && focusedInputDialog === 'undercover-callout' && UndercoverAutoCallout && setShowUndercoverCallout(false)} />}
{focusedInputDialog === 'effort-callout' && {
setShowEffortCallout(false);
if (selection !== 'dismiss') {
@@ -4894,7 +4894,7 @@ export function REPL({
{/* Frustration-triggered transcript sharing prompt */}
{frustrationDetection.state !== 'closed' && {}} handleTranscriptSelect={frustrationDetection.handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} />}
{/* Skill improvement survey - appears when improvements detected (ant-only) */}
- {("external" as string) === 'ant' && skillImprovementSurvey.suggestion && }
+ {(process.env.USER_TYPE) === 'ant' && skillImprovementSurvey.suggestion && }
{showIssueFlagBanner && }
{}
}
- {("external" as string) === 'ant' && }
+ {(process.env.USER_TYPE) === 'ant' && }
{feature('BUDDY') && !(companionNarrow && isFullscreenEnvEnabled()) && companionVisible ? : null}
} />
diff --git a/src/tools/AgentTool/AgentTool.tsx b/src/tools/AgentTool/AgentTool.tsx
index 0851421..709f31e 100644
--- a/src/tools/AgentTool/AgentTool.tsx
+++ b/src/tools/AgentTool/AgentTool.tsx
@@ -96,7 +96,7 @@ const fullInputSchema = lazySchema(() => {
mode: permissionModeSchema().optional().describe('Permission mode for spawned teammate (e.g., "plan" to require plan approval).')
});
return baseInputSchema().merge(multiAgentInputSchema).extend({
- isolation: (("external" as string) === 'ant' ? z.enum(['worktree', 'remote']) : z.enum(['worktree'])).optional().describe(("external" as string) === 'ant' ? 'Isolation mode. "worktree" creates a temporary git worktree so the agent works on an isolated copy of the repo. "remote" launches the agent in a remote CCR environment (always runs in background).' : 'Isolation mode. "worktree" creates a temporary git worktree so the agent works on an isolated copy of the repo.'),
+ isolation: ((process.env.USER_TYPE) === 'ant' ? z.enum(['worktree', 'remote']) : z.enum(['worktree'])).optional().describe((process.env.USER_TYPE) === 'ant' ? 'Isolation mode. "worktree" creates a temporary git worktree so the agent works on an isolated copy of the repo. "remote" launches the agent in a remote CCR environment (always runs in background).' : 'Isolation mode. "worktree" creates a temporary git worktree so the agent works on an isolated copy of the repo.'),
cwd: z.string().optional().describe('Absolute path to run the agent in. Overrides the working directory for all filesystem and shell operations within this agent. Mutually exclusive with isolation: "worktree".')
});
});
@@ -432,7 +432,7 @@ export const AgentTool = buildTool({
// Remote isolation: delegate to CCR. Gated ant-only — the guard enables
// dead code elimination of the entire block for external builds.
- if (("external" as string) === 'ant' && effectiveIsolation === 'remote') {
+ if ((process.env.USER_TYPE) === 'ant' && effectiveIsolation === 'remote') {
const eligibility = await checkRemoteAgentEligibility();
if (!eligibility.eligible) {
const reasons = (eligibility as { eligible: false; errors: Parameters[0][] }).errors.map(formatPreconditionError).join('\n');
@@ -522,7 +522,7 @@ export const AgentTool = buildTool({
// Log agent memory loaded event for subagents
if (selectedAgent.memory) {
logEvent('tengu_agent_memory_loaded', {
- ...(("external" as string) === 'ant' && {
+ ...((process.env.USER_TYPE) === 'ant' && {
agent_type: selectedAgent.agentType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
}),
scope: selectedAgent.memory as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
@@ -1284,7 +1284,7 @@ export const AgentTool = buildTool({
// Only route through auto mode classifier when in auto mode
// In all other modes, auto-approve sub-agent generation
// Note: "external" === 'ant' guard enables dead code elimination for external builds
- if (("external" as string) === 'ant' && appState.toolPermissionContext.mode === 'auto') {
+ if ((process.env.USER_TYPE) === 'ant' && appState.toolPermissionContext.mode === 'auto') {
return {
behavior: 'passthrough',
message: 'Agent tool requires permission to spawn sub-agents.'
diff --git a/src/tools/AgentTool/UI.tsx b/src/tools/AgentTool/UI.tsx
index 368b35f..ff0eb63 100644
--- a/src/tools/AgentTool/UI.tsx
+++ b/src/tools/AgentTool/UI.tsx
@@ -99,7 +99,7 @@ type ProcessedMessage = {
*/
function processProgressMessages(messages: ProgressMessage