11import { execSync , spawn , type ChildProcess } from "node:child_process" ;
22import { homedir } from "node:os" ;
3- import { existsSync } from "node:fs" ;
3+ import { existsSync , mkdirSync , cpSync , copyFileSync } from "node:fs" ;
44import { join } from "node:path" ;
5+ import { tmpdir } from "node:os" ;
56import type { AgentConfig , RunningAgent } from "./types.js" ;
67import { buildBranchName } from "./spawner.js" ;
78import { buildCommand } from "./command-builder.js" ;
@@ -56,6 +57,57 @@ export function buildImage(dockerfilePath: string): void {
5657/**
5758 * Build docker run arguments for an agent container.
5859 */
60+ /**
61+ * Copy Claude auth files to a temp directory so the container can write to them.
62+ * Claude Code updates .claude.json at runtime; read-only mounts cause failures.
63+ */
64+ function prepareClaudeAuthCopy ( containerName : string ) : string {
65+ const home = homedir ( ) ;
66+ const tmpAuth = join ( tmpdir ( ) , `toban-claude-auth-${ containerName } ` ) ;
67+ mkdirSync ( tmpAuth , { recursive : true } ) ;
68+
69+ // Copy .claude directory (contains settings, backups, etc.)
70+ const claudeDir = join ( home , ".claude" ) ;
71+ const tmpClaudeDir = join ( tmpAuth , ".claude" ) ;
72+ if ( existsSync ( claudeDir ) ) {
73+ cpSync ( claudeDir , tmpClaudeDir , { recursive : true } ) ;
74+ }
75+
76+ // Copy .claude.json — also restore from backup if missing
77+ const claudeJson = join ( home , ".claude.json" ) ;
78+ const tmpClaudeJson = join ( tmpAuth , ".claude.json" ) ;
79+ if ( existsSync ( claudeJson ) ) {
80+ copyFileSync ( claudeJson , tmpClaudeJson ) ;
81+ } else if ( existsSync ( tmpClaudeDir ) ) {
82+ // Try to restore from backup
83+ const backupDir = join ( tmpClaudeDir , "backups" ) ;
84+ if ( existsSync ( backupDir ) ) {
85+ try {
86+ const backups = execSync ( `ls -t "${ backupDir } "/.claude.json.backup.* 2>/dev/null` , {
87+ encoding : "utf-8" ,
88+ timeout : 3000 ,
89+ } ) . trim ( ) . split ( "\n" ) . filter ( Boolean ) ;
90+ if ( backups . length > 0 ) {
91+ copyFileSync ( backups [ 0 ] , tmpClaudeJson ) ;
92+ ui . info ( `[docker] Restored .claude.json from backup` ) ;
93+ }
94+ } catch {
95+ // No backups found
96+ }
97+ }
98+ }
99+
100+ // Copy .config/claude
101+ const configClaude = join ( home , ".config" , "claude" ) ;
102+ const tmpConfigClaude = join ( tmpAuth , ".config" , "claude" ) ;
103+ if ( existsSync ( configClaude ) ) {
104+ mkdirSync ( join ( tmpAuth , ".config" ) , { recursive : true } ) ;
105+ cpSync ( configClaude , tmpConfigClaude , { recursive : true } ) ;
106+ }
107+
108+ return tmpAuth ;
109+ }
110+
59111function buildDockerArgs (
60112 config : AgentConfig ,
61113 worktreePath : string ,
@@ -65,21 +117,24 @@ function buildDockerArgs(
65117 const home = homedir ( ) ;
66118 const containerName = `toban-agent-${ config . name } -${ config . taskId . slice ( 0 , 8 ) } ` ;
67119
120+ // Copy Claude auth files to writable temp directory
121+ const tmpAuth = prepareClaudeAuthCopy ( containerName ) ;
122+
68123 const args : string [ ] = [
69124 "run" ,
70125 "--rm" ,
71126 "--name" , containerName ,
72127 // Mount the worktree as /workspace
73128 "-v" , `${ worktreePath } :/workspace` ,
74- // Mount CLI auth directories as read-only
75- ...( existsSync ( join ( home , ".claude" ) )
76- ? [ "-v" , `${ join ( home , ".claude" ) } :/home/agent/.claude:ro ` ]
129+ // Mount Claude auth as writable copies (Claude updates these at runtime)
130+ ...( existsSync ( join ( tmpAuth , ".claude" ) )
131+ ? [ "-v" , `${ join ( tmpAuth , ".claude" ) } :/home/agent/.claude` ]
77132 : [ ] ) ,
78- ...( existsSync ( join ( home , ".claude.json" ) )
79- ? [ "-v" , `${ join ( home , ".claude.json" ) } :/home/agent/.claude.json:ro ` ]
133+ ...( existsSync ( join ( tmpAuth , ".claude.json" ) )
134+ ? [ "-v" , `${ join ( tmpAuth , ".claude.json" ) } :/home/agent/.claude.json` ]
80135 : [ ] ) ,
81- ...( existsSync ( join ( home , ".config" , "claude" ) )
82- ? [ "-v" , `${ join ( home , ".config" , "claude" ) } :/home/agent/.config/claude:ro ` ]
136+ ...( existsSync ( join ( tmpAuth , ".config" , "claude" ) )
137+ ? [ "-v" , `${ join ( tmpAuth , ".config" , "claude" ) } :/home/agent/.config/claude` ]
83138 : [ ] ) ,
84139 // Gemini CLI auth
85140 ...( existsSync ( join ( home , ".config" , "gemini" ) )
0 commit comments