Skip to content

Commit ddac0c7

Browse files
committed
fix: mount Claude auth as writable copies instead of read-only
Claude Code updates .claude.json at runtime. Read-only mounts cause "configuration file not found" errors. Copy auth files to a temp directory per container so Claude can write freely without affecting the host. Also auto-restore .claude.json from backup if missing.
1 parent c82fc2b commit ddac0c7

1 file changed

Lines changed: 63 additions & 8 deletions

File tree

src/docker.ts

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { execSync, spawn, type ChildProcess } from "node:child_process";
22
import { homedir } from "node:os";
3-
import { existsSync } from "node:fs";
3+
import { existsSync, mkdirSync, cpSync, copyFileSync } from "node:fs";
44
import { join } from "node:path";
5+
import { tmpdir } from "node:os";
56
import type { AgentConfig, RunningAgent } from "./types.js";
67
import { buildBranchName } from "./spawner.js";
78
import { 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+
59111
function 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

Comments
 (0)