1- import { existsSync , mkdirSync , readdirSync , readFileSync , symlinkSync , writeFileSync } from 'node:fs' ;
1+ import { existsSync , mkdirSync , readFileSync , symlinkSync , writeFileSync } from 'node:fs' ;
22import { homedir , tmpdir } from 'node:os' ;
33import { join , dirname } from 'node:path' ;
44import { randomUUID } from 'node:crypto' ;
@@ -363,24 +363,21 @@ export abstract class BaseAIProvider implements IAIProvider {
363363 }
364364
365365 /**
366- * Set up an isolated home directory for a provider CLI.
366+ * Set up a minimal isolated home directory for a provider CLI.
367367 *
368- * For temp homes (baseDir not provided): mirrors the entire real home top-level
369- * into the isolated dir via symlinks, then replaces only `providerDir` with a real
370- * isolated directory containing auth-file symlinks. This gives the spawned process
371- * access to all user dotfiles, tools, configs, and credentials without needing an
372- * explicit allowlist, while keeping provider session state isolated between parallel
373- * invocations.
368+ * Only used by providers that store session state locally and need isolation
369+ * between parallel invocations (Gemini, Codex). Providers that don't write
370+ * local session state (Claude with -p flag, Mistral via VIBE_HOME) should
371+ * NOT use this — they should run with the real HOME or a targeted env var.
374372 *
375- * For persistent homes (baseDir provided, e.g. Gemini's per-project home): only
376- * creates the provider dir + auth symlinks, plus a small set of essential files.
377- * We don't full-mirror into persistent homes because they live inside project dirs
378- * and should not be filled with real-home symlinks.
373+ * Creates a minimal dir with:
374+ * - an isolated providerDir containing only auth-file symlinks
375+ * - symlinks for .gitconfig, .ssh, and any extra rootFiles
379376 *
380- * @param providerDir Provider-specific config/state dir (e.g. '.claude ', '.config/gcloud')
377+ * @param providerDir Provider-specific config/state dir (e.g. '.gemini ', '.config/gcloud')
381378 * @param authFiles Auth/config filenames to symlink into the isolated providerDir
382- * @param baseDir Optional pre-existing dir to use instead of creating a temp one
383- * @param rootFiles Extra HOME-root files to symlink for persistent homes (ignored for temp homes )
379+ * @param baseDir Optional pre-existing dir to use (e.g. per-project persistent home)
380+ * @param rootFiles Extra HOME-root files to symlink (e.g. ['.npmrc'] )
384381 */
385382 protected setupIsolatedHome ( providerDir : string , authFiles : string [ ] , baseDir ?: string , rootFiles ?: string [ ] ) : string {
386383 const uuid = randomUUID ( ) ;
@@ -392,54 +389,13 @@ export abstract class BaseAIProvider implements IAIProvider {
392389 mkdirSync ( isolatedHome , { recursive : true } ) ;
393390 }
394391
395- // ── Temp home: mirror the real home ──────────────────────────────────────
396- // Symlink every top-level real-home entry except the provider dir (which we
397- // create as a real isolated dir below). For nested provider dirs like
398- // '.config/gcloud', the parent ('.config') is excluded from the mirror and
399- // rebuilt with its own contents minus the specific subdir.
400- if ( ! baseDir ) {
401- const topProviderDir = providerDir . split ( '/' ) [ 0 ] ;
402-
403- try {
404- for ( const entry of readdirSync ( realHome ) ) {
405- if ( entry === topProviderDir ) continue ; // handled separately below
406- const src = join ( realHome , entry ) ;
407- const dest = join ( isolatedHome , entry ) ;
408- if ( ! existsSync ( dest ) ) {
409- try { symlinkSync ( src , dest ) ; } catch { /* best effort */ }
410- }
411- }
412- } catch { /* best effort — don't abort if real home listing fails */ }
413-
414- // For nested paths (e.g. '.config/gcloud'): create the parent dir as a real
415- // dir and symlink everything from the real parent except the specific subdir.
416- const isNested = providerDir . includes ( '/' ) ;
417- if ( isNested ) {
418- const subDir = providerDir . slice ( topProviderDir . length + 1 ) ;
419- const realParentPath = join ( realHome , topProviderDir ) ;
420- const isolatedParentPath = join ( isolatedHome , topProviderDir ) ;
421- mkdirSync ( isolatedParentPath , { recursive : true } ) ;
422- if ( existsSync ( realParentPath ) ) {
423- try {
424- for ( const entry of readdirSync ( realParentPath ) ) {
425- if ( entry === subDir ) continue ;
426- const src = join ( realParentPath , entry ) ;
427- const dest = join ( isolatedParentPath , entry ) ;
428- if ( ! existsSync ( dest ) ) {
429- try { symlinkSync ( src , dest ) ; } catch { /* best effort */ }
430- }
431- }
432- } catch { /* best effort */ }
433- }
434- }
435- }
436-
437- // ── Create isolated provider dir with auth-file symlinks ─────────────────
392+ // Create isolated provider dir with auth-file symlinks
438393 const realProviderPath = join ( realHome , providerDir ) ;
439394 const isolatedProviderPath = join ( isolatedHome , providerDir ) ;
440- mkdirSync ( isolatedProviderPath , { recursive : true } ) ;
441395
442396 if ( existsSync ( realProviderPath ) ) {
397+ mkdirSync ( isolatedProviderPath , { recursive : true } ) ;
398+
443399 for ( const file of authFiles ) {
444400 const src = join ( realProviderPath , file ) ;
445401 const dest = join ( isolatedProviderPath , file ) ;
@@ -458,18 +414,14 @@ export abstract class BaseAIProvider implements IAIProvider {
458414 }
459415 }
460416
461- // ── Persistent home: explicit essential symlinks ──────────────────────────
462- // The full-mirror doesn't apply to persistent homes, so add the minimum
463- // required files explicitly.
464- if ( baseDir ) {
465- [ ...( rootFiles ?? [ ] ) , '.gitconfig' , '.ssh' ] . forEach ( file => {
466- const src = join ( realHome , file ) ;
467- const dest = join ( isolatedHome , file ) ;
468- if ( existsSync ( src ) && ! existsSync ( dest ) ) {
469- try { symlinkSync ( src , dest ) ; } catch { /* best effort */ }
470- }
471- } ) ;
472- }
417+ // Essential HOME-root symlinks so git and SSH work from the isolated home
418+ [ ...( rootFiles ?? [ ] ) , '.gitconfig' , '.ssh' ] . forEach ( file => {
419+ const src = join ( realHome , file ) ;
420+ const dest = join ( isolatedHome , file ) ;
421+ if ( existsSync ( src ) && ! existsSync ( dest ) ) {
422+ try { symlinkSync ( src , dest ) ; } catch { /* best effort */ }
423+ }
424+ } ) ;
473425
474426 } catch ( e ) {
475427 console . warn ( `Failed to set up isolated ${ this . name } home: ${ e } ` ) ;
0 commit comments