@@ -15,6 +15,8 @@ import {
1515 stopInterceptor ,
1616 isInterceptorRunning ,
1717 getProcessName ,
18+ getNetworkExtensionStatus ,
19+ openNetworkExtensionSettings ,
1820} from "../interceptor/mitmproxy-manager.js" ;
1921
2022// ─── Helpers ──────────────────────────────────────────────────────────────────
@@ -154,11 +156,15 @@ export function registerClient(program: Command): void {
154156 console . log ( chalk . green ( "✓ Claude Code configured to route through CC-Router" ) ) ;
155157 console . log ( chalk . gray ( ` ANTHROPIC_BASE_URL → ${ url } ` ) ) ;
156158
157- // 6. Optionally configure Claude Desktop
158- const wantsDesktop = opts ?. desktop ?? (
159- isClaudeDesktopInstalled ( ) &&
160- await confirm ( { message : "Also route Claude Desktop (chat + Cowork) through the proxy?" , default : false } )
161- ) ;
159+ // 6. Optionally configure Claude Desktop (Cowork / Agent mode only)
160+ let wantsDesktop = opts ?. desktop ?? false ;
161+ if ( ! opts ?. desktop && isClaudeDesktopInstalled ( ) ) {
162+ printDesktopSupportExplainer ( ) ;
163+ wantsDesktop = await confirm ( {
164+ message : "Route Claude Desktop's Cowork / Agent-mode traffic through CC-Router?" ,
165+ default : false ,
166+ } ) ;
167+ }
162168
163169 if ( wantsDesktop ) {
164170 await setupDesktopInterception ( url ) ;
@@ -276,15 +282,30 @@ export function registerClient(program: Command): void {
276282 }
277283
278284 // ── Desktop status ─────────────────────────────────────────────────
279- console . log ( chalk . bold ( "\n DESKTOP INTERCEPTOR" ) ) ;
285+ console . log ( chalk . bold ( "\n DESKTOP INTERCEPTOR (Cowork / Agent mode) " ) ) ;
280286 if ( cfg . client . desktopEnabled ) {
281287 const running = await isInterceptorRunning ( ) ;
282- console . log ( ` ${ running ? chalk . green ( "● running" ) : chalk . yellow ( "○ configured but stopped" ) } ` ) ;
283- if ( ! running ) {
288+ if ( running ) {
289+ console . log ( ` ${ chalk . green ( "● running" ) } ` ) ;
290+ } else {
291+ console . log ( ` ${ chalk . yellow ( "○ configured but stopped" ) } ` ) ;
284292 console . log ( chalk . gray ( " Start with: cc-router client start-desktop" ) ) ;
285293 }
294+ // Check Network Extension on macOS
295+ if ( isMacos ( ) ) {
296+ const extStatus = await getNetworkExtensionStatus ( ) ;
297+ if ( extStatus === "waiting" ) {
298+ console . log ( chalk . red ( " ⚠ Network Extension NOT approved — interceptor won't capture traffic!" ) ) ;
299+ console . log ( chalk . gray ( " Fix: System Settings → General → Login Items & Extensions → Network Extensions" ) ) ;
300+ } else if ( extStatus === "not_installed" ) {
301+ console . log ( chalk . yellow ( " ⚠ Network Extension not installed — will be triggered on first start" ) ) ;
302+ } else if ( extStatus === "enabled" ) {
303+ console . log ( ` ${ chalk . green ( "✓" ) } ${ chalk . gray ( "Network Extension: enabled" ) } ` ) ;
304+ }
305+ }
306+ console . log ( chalk . gray ( " Scope: /v1/messages + /v1/models (normal chat NOT routed)" ) ) ;
286307 } else {
287- console . log ( ` ${ chalk . gray ( "not configured" ) } ` ) ;
308+ console . log ( ` ${ chalk . gray ( "not configured — enable with: cc-router client connect --desktop " ) } ` ) ;
288309 }
289310
290311 console . log ( ) ;
@@ -294,17 +315,18 @@ export function registerClient(program: Command): void {
294315 // ── cc-router client start-desktop ──────────────────────────────────────────
295316 client
296317 . command ( "start-desktop" )
297- . description ( "Start mitmproxy interceptor for Claude Desktop" )
318+ . description ( "Start mitmproxy interceptor for Claude Desktop (Cowork / Agent mode) " )
298319 . action ( async ( ) => {
299320 const cfg = readConfig ( ) ;
300321 if ( ! cfg . client ) {
301322 console . error ( chalk . red ( "Not connected. Run: cc-router client connect <url>" ) ) ;
302323 process . exit ( 1 ) ;
303324 }
304325
305- if ( ! await checkMitmproxyInstalled ( ) ) {
306- console . error ( chalk . red ( "mitmproxy not found. Install it first:" ) ) ;
307- console . error ( chalk . yellow ( isMacos ( ) ? " brew install mitmproxy" : " pip install mitmproxy" ) ) ;
326+ if ( ! ( await checkMitmproxyInstalled ( ) ) ) {
327+ console . error ( chalk . red ( "\n✗ mitmproxy not found. Install it first:" ) ) ;
328+ console . error ( chalk . cyan ( isMacos ( ) ? " brew install mitmproxy" : " pip install mitmproxy" ) ) ;
329+ console . error ( ) ;
308330 process . exit ( 1 ) ;
309331 }
310332
@@ -314,15 +336,53 @@ export function registerClient(program: Command): void {
314336 writeConfig ( cfg ) ;
315337 }
316338
339+ // Pre-flight check: verify Network Extension is ready on macOS.
340+ // startInterceptor does the same check and throws; we catch and show
341+ // a friendlier block here with the open-settings shortcut.
342+ if ( isMacos ( ) ) {
343+ const status = await getNetworkExtensionStatus ( ) ;
344+ if ( status === "waiting" ) {
345+ console . error ( chalk . red ( "\n✗ Mitmproxy Network Extension is NOT yet approved.\n" ) ) ;
346+ printNetworkExtensionInstructions ( ) ;
347+ const openNow = await confirm ( {
348+ message : "Open System Settings now?" ,
349+ default : true ,
350+ } ) ;
351+ if ( openNow ) await openNetworkExtensionSettings ( ) ;
352+ console . error ( chalk . yellow ( "\n Re-run `cc-router client start-desktop` after approving.\n" ) ) ;
353+ process . exit ( 1 ) ;
354+ }
355+ if ( status === "not_installed" ) {
356+ console . error ( chalk . yellow ( "\n⚠ Mitmproxy Network Extension is not installed yet." ) ) ;
357+ console . error ( chalk . gray ( " The first mitmdump run will trigger installation." ) ) ;
358+ console . error ( chalk . gray ( " Approve it in System Settings when macOS prompts you, then re-run this command.\n" ) ) ;
359+ }
360+ }
361+
317362 const target = cfg . client . remoteUrl ;
318363 const processName = getProcessName ( ) ;
319364 console . log ( chalk . cyan ( `\nStarting mitmproxy interceptor for "${ processName } "...` ) ) ;
320- console . log ( chalk . gray ( ` Redirecting api.anthropic.com → ${ target } \n` ) ) ;
321-
322- await startInterceptor ( target ) ;
365+ console . log ( chalk . gray ( ` Redirecting api.anthropic.com/v1/messages → ${ target } ` ) ) ;
366+
367+ try {
368+ await startInterceptor ( target ) ;
369+ } catch ( e ) {
370+ console . error ( chalk . red ( `\n✗ Failed to start interceptor:\n` ) ) ;
371+ console . error ( chalk . yellow ( " " + ( e as Error ) . message . split ( "\n" ) . join ( "\n " ) ) ) ;
372+ console . error ( ) ;
373+ process . exit ( 1 ) ;
374+ }
323375
324- console . log ( chalk . green ( "✓ Claude Desktop interceptor running" ) ) ;
325- console . log ( chalk . gray ( " Open Claude Desktop and send a message to test.\n" ) ) ;
376+ console . log ( chalk . green ( "\n✓ Claude Desktop interceptor running" ) ) ;
377+ console . log ( ) ;
378+ console . log ( chalk . bold . yellow ( " Next steps:" ) ) ;
379+ console . log ( " " + chalk . cyan ( "1." ) + " Quit Claude Desktop completely (⌘Q)" ) ;
380+ console . log ( " " + chalk . cyan ( "2." ) + " Reopen Claude Desktop" ) ;
381+ console . log ( " " + chalk . cyan ( "3." ) + " Use Cowork / Agent mode (Claude Code in Desktop)" ) ;
382+ console . log ( ) ;
383+ console . log ( chalk . gray ( " Check routing with: " ) + chalk . cyan ( "cc-router client status" ) ) ;
384+ console . log ( chalk . gray ( " Stop interceptor: " ) + chalk . cyan ( "cc-router client stop-desktop" ) ) ;
385+ console . log ( ) ;
326386 } ) ;
327387
328388 // ── cc-router client stop-desktop ───────────────────────────────────────────
@@ -337,55 +397,162 @@ export function registerClient(program: Command): void {
337397
338398// ─── Desktop setup flow ───────────────────────────────────────────────────────
339399
400+ /**
401+ * Printed before asking the user whether to enable Desktop interception.
402+ * The copy is deliberately explicit about WHAT works and WHAT doesn't — users
403+ * who expect the normal chat to go through CC-Router will hit confusion fast,
404+ * and we can head it off here by framing this as a "Cowork / Agent mode" feature.
405+ */
406+ export function printDesktopSupportExplainer ( ) : void {
407+ console . log ( chalk . bold . cyan ( "\n 🖥 Claude Desktop — what CC-Router can route\n" ) ) ;
408+ console . log (
409+ " Claude Desktop does NOT expose ANTHROPIC_BASE_URL, so CC-Router uses\n" +
410+ " mitmproxy to selectively intercept only the traffic it can handle:\n"
411+ ) ;
412+ console . log ( chalk . green ( " ✓ Cowork / Agent mode " ) + chalk . gray ( "— /v1/messages (this is what gets routed)" ) ) ;
413+ console . log ( chalk . green ( " ✓ Claude Code inside Desktop" ) + chalk . gray ( "— /v1/messages (same as CLI)" ) ) ;
414+ console . log ( chalk . red ( " ✗ Normal chat " ) + chalk . gray ( "— goes to claude.ai webview, NOT redirectable" ) ) ;
415+ console . log ( ) ;
416+ console . log ( chalk . gray (
417+ " TL;DR: Your LLM-heavy workflows (Cowork, agent tasks, in-Desktop\n" +
418+ " Claude Code) will rotate across your Max accounts via CC-Router.\n" +
419+ " The regular chat sidebar keeps going directly through claude.ai."
420+ ) ) ;
421+ console . log ( ) ;
422+ }
423+
424+ /**
425+ * Prints the macOS Network Extension approval walkthrough.
426+ * This is the #1 gotcha — mitmdump starts silently but captures nothing
427+ * until the user flips the toggle in System Settings.
428+ */
429+ export function printNetworkExtensionInstructions ( ) : void {
430+ if ( ! isMacos ( ) ) return ;
431+ console . log ( chalk . bold . yellow ( "\n ⚠ IMPORTANT — macOS Network Extension approval\n" ) ) ;
432+ console . log ( " The first time mitmproxy runs in local mode, macOS installs a" ) ;
433+ console . log ( " Network Extension (" + chalk . cyan ( "Mitmproxy Redirector" ) + ") that must be approved" ) ;
434+ console . log ( " manually. " + chalk . red ( "Without this step, mitmproxy captures ZERO traffic." ) + "\n" ) ;
435+ console . log ( chalk . bold ( " Steps:" ) ) ;
436+ console . log ( " " + chalk . cyan ( "1." ) + " Open " + chalk . bold ( "System Settings" ) ) ;
437+ console . log ( " " + chalk . cyan ( "2." ) + " Go to " + chalk . bold ( "General → Login Items & Extensions" ) ) ;
438+ console . log ( " " + chalk . cyan ( "3." ) + " Scroll to " + chalk . bold ( "Network Extensions" ) + " and click the " + chalk . bold ( "ⓘ" ) + " button" ) ;
439+ console . log ( " " + chalk . cyan ( "4." ) + " Toggle " + chalk . bold ( "Mitmproxy Redirector" ) + " ON" ) ;
440+ console . log ( " " + chalk . cyan ( "5." ) + " Enter your Mac admin password when prompted\n" ) ;
441+ console . log ( chalk . gray ( " You only need to do this ONCE per machine.\n" ) ) ;
442+ }
443+
340444async function setupDesktopInterception ( target : string ) : Promise < void > {
341445 console . log ( chalk . bold ( "\n🖥 Claude Desktop Setup\n" ) ) ;
342446
447+ // 0. Explain what actually works before anything else
448+ printDesktopSupportExplainer ( ) ;
449+ const proceedWithSetup = await confirm ( {
450+ message : "Continue with Cowork / Agent-mode interception setup?" ,
451+ default : true ,
452+ } ) ;
453+ if ( ! proceedWithSetup ) {
454+ console . log ( chalk . gray ( "Skipping Desktop setup. You can run it later with: cc-router client start-desktop\n" ) ) ;
455+ return ;
456+ }
457+
343458 // 1. Check mitmproxy
344- if ( ! await checkMitmproxyInstalled ( ) ) {
345- console . log ( chalk . yellow ( "mitmproxy is required but not installed." ) ) ;
459+ if ( ! ( await checkMitmproxyInstalled ( ) ) ) {
460+ console . log ( chalk . yellow ( "\nmitmproxy is required but not installed." ) ) ;
346461 if ( isMacos ( ) ) {
347- console . log ( chalk . cyan ( " Install: brew install mitmproxy" ) ) ;
462+ console . log ( chalk . cyan ( " Install: brew install mitmproxy" ) ) ;
348463 } else if ( isWindows ( ) ) {
349- console . log ( chalk . cyan ( " Install: pip install mitmproxy" ) ) ;
464+ console . log ( chalk . cyan ( " Install: pip install mitmproxy (or download the installer from mitmproxy.org) " ) ) ;
350465 } else {
351- console . log ( chalk . cyan ( " Install: pip install mitmproxy ( requires kernel ≥ 6.8)" ) ) ;
466+ console . log ( chalk . cyan ( " Install: pip install mitmproxy (Linux local mode requires kernel ≥ 6.8)" ) ) ;
352467 }
353468 console . log ( ) ;
354- const proceed = await confirm ( { message : "Have you installed mitmproxy?" , default : false } ) ;
355- if ( ! proceed || ! await checkMitmproxyInstalled ( ) ) {
356- console . log ( chalk . red ( "mitmproxy not found. Skipping Desktop setup.\n" ) ) ;
469+ const proceed = await confirm ( { message : "Have you installed mitmproxy now?" , default : false } ) ;
470+ if ( ! proceed || ! ( await checkMitmproxyInstalled ( ) ) ) {
471+ console . log ( chalk . red ( "\nmitmproxy still not found. Skipping Desktop setup.\n" ) ) ;
472+ console . log ( chalk . gray ( "Re-run later with: cc-router client start-desktop\n" ) ) ;
357473 return ;
358474 }
359475 }
360476 console . log ( chalk . green ( "✓ mitmproxy found" ) ) ;
361477
362- // 2. Generate CA cert if needed
478+ // 2. Generate CA cert if missing
363479 if ( ! isCaCertInstalled ( ) ) {
364- console . log ( chalk . gray ( "Generating mitmproxy CA certificate..." ) ) ;
365- await generateCaCert ( ) ;
480+ console . log ( chalk . gray ( "Generating mitmproxy CA certificate (one-time)..." ) ) ;
481+ try {
482+ await generateCaCert ( ) ;
483+ console . log ( chalk . green ( "✓ CA certificate generated" ) ) ;
484+ } catch ( e ) {
485+ console . log ( chalk . red ( `✗ CA generation failed: ${ ( e as Error ) . message } ` ) ) ;
486+ return ;
487+ }
488+ } else {
489+ console . log ( chalk . green ( "✓ CA certificate already present" ) ) ;
366490 }
367491
368492 // 3. Install CA cert (requires sudo)
369- console . log ( chalk . yellow ( "\nInstalling the mitmproxy CA certificate requires admin access." ) ) ;
370- console . log ( chalk . gray ( "This is needed so Claude Desktop trusts the local interceptor." ) ) ;
371- const installCa = await confirm ( { message : "Install CA certificate now? (requires password)" , default : true } ) ;
493+ console . log ( ) ;
494+ console . log ( chalk . yellow ( "The mitmproxy CA certificate must be trusted by your OS so that" ) ) ;
495+ console . log ( chalk . yellow ( "Claude Desktop accepts the local interceptor. This requires sudo." ) ) ;
496+ const installCa = await confirm ( { message : "Install CA certificate now? (asks for admin password)" , default : true } ) ;
372497 if ( installCa ) {
373498 const ok = await installCaCert ( ) ;
374499 if ( ok ) {
375- console . log ( chalk . green ( "✓ CA certificate installed" ) ) ;
500+ console . log ( chalk . green ( "✓ CA certificate installed in system trust store " ) ) ;
376501 } else {
377- console . log ( chalk . red ( "✗ CA certificate install failed. You may need to install it manually." ) ) ;
502+ console . log ( chalk . red ( "✗ CA certificate install failed." ) ) ;
503+ console . log ( chalk . gray ( " Install manually later with:" ) ) ;
504+ console . log ( chalk . gray ( " sudo security add-trusted-cert -d -r trustRoot \\" ) ) ;
505+ console . log ( chalk . gray ( " -k /Library/Keychains/System.keychain \\" ) ) ;
506+ console . log ( chalk . gray ( " ~/.mitmproxy/mitmproxy-ca-cert.pem" ) ) ;
378507 }
379508 }
380509
381510 // 4. Write addon script
382511 writeAddonScript ( target ) ;
383512 console . log ( chalk . green ( "✓ Redirect addon configured" ) ) ;
384513
385- // 5. macOS Network Extension note
514+ // 5. macOS Network Extension — THIS is the step people miss
386515 if ( isMacos ( ) ) {
387- console . log ( chalk . yellow ( "\n⚠ On first run, macOS will ask to approve mitmproxy's Network Extension." ) ) ;
388- console . log ( chalk . gray ( " Go to System Settings → General → Login Items & Extensions → Network Extensions" ) ) ;
389- console . log ( chalk . gray ( " and toggle 'Mitmproxy Redirector' on.\n" ) ) ;
516+ printNetworkExtensionInstructions ( ) ;
517+
518+ // Check current status and guide the user if it's not enabled
519+ const status = await getNetworkExtensionStatus ( ) ;
520+
521+ if ( status === "not_installed" ) {
522+ console . log ( chalk . gray (
523+ " The Network Extension hasn't been installed yet — it'll be triggered\n" +
524+ " automatically the first time you run `cc-router client start-desktop`.\n" +
525+ " macOS will show a popup — approve it and follow the steps above.\n"
526+ ) ) ;
527+ } else if ( status === "waiting" ) {
528+ console . log ( chalk . red ( " ⚠ Network Extension is installed but NOT yet approved.\n" ) ) ;
529+ const openNow = await confirm ( {
530+ message : "Open System Settings now so you can approve it?" ,
531+ default : true ,
532+ } ) ;
533+ if ( openNow ) {
534+ await openNetworkExtensionSettings ( ) ;
535+ console . log ( chalk . gray ( "\n System Settings should now be open." ) ) ;
536+ console . log ( chalk . gray ( " Toggle 'Mitmproxy Redirector' ON, then come back here.\n" ) ) ;
537+ await confirm ( { message : "Done? Press Enter when the toggle is ON" , default : true } ) ;
538+ const newStatus = await getNetworkExtensionStatus ( ) ;
539+ if ( newStatus === "enabled" ) {
540+ console . log ( chalk . green ( "✓ Network Extension is enabled" ) ) ;
541+ } else {
542+ console . log ( chalk . yellow ( ` Still not enabled (status: ${ newStatus } )` ) ) ;
543+ console . log ( chalk . gray ( " You can re-check later with: cc-router client status" ) ) ;
544+ }
545+ }
546+ } else if ( status === "enabled" ) {
547+ console . log ( chalk . green ( " ✓ Network Extension is already enabled — you're all set" ) ) ;
548+ }
390549 }
550+
551+ // 6. Remind that Claude Desktop must be restarted for mitmproxy to hook into it
552+ console . log ( ) ;
553+ console . log ( chalk . bold . yellow ( " One more thing:" ) ) ;
554+ console . log ( chalk . gray ( " After starting the interceptor, you must " + chalk . bold ( "quit and relaunch Claude Desktop" ) ) ) ;
555+ console . log ( chalk . gray ( " (⌘Q in Claude Desktop, then reopen it). mitmproxy only captures" ) ) ;
556+ console . log ( chalk . gray ( " traffic from processes started AFTER it begins listening." ) ) ;
557+ console . log ( ) ;
391558}
0 commit comments