From aa27c42129d30aec5a35dad2653f1ceab06d1278 Mon Sep 17 00:00:00 2001 From: "jason.woltje" Date: Wed, 24 Jun 2026 05:16:46 +0000 Subject: [PATCH] fix(fleet): pre-trust claude agent workdir to clear the folder-trust gate (#644) (#645) --- .../tools/fleet/start-agent-session.sh | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/packages/mosaic/framework/tools/fleet/start-agent-session.sh b/packages/mosaic/framework/tools/fleet/start-agent-session.sh index d85fef8..96aaec8 100755 --- a/packages/mosaic/framework/tools/fleet/start-agent-session.sh +++ b/packages/mosaic/framework/tools/fleet/start-agent-session.sh @@ -122,6 +122,85 @@ fi mkdir -p "$MOSAIC_AGENT_WORKDIR" +# ── Pre-trust the workdir for the Claude runtime ───────────────────────────── +# Claude Code shows a one-time "Is this a project you trust?" folder-trust gate +# the first time it opens a directory. A fleet-launched agent has no human to +# answer it, so the pane stalls forever at the prompt while its heartbeat keeps +# reporting "healthy" (the pane process IS alive — it's just blocked). +# +# IMPORTANT: --dangerously-skip-permissions does NOT bypass this gate, and +# neither does `trustedProjectDirectories` in settings.json (verified empirically +# 2026-06-24). The ONLY thing the gate honors is the per-project record in +# ~/.claude.json: projects[""].hasTrustDialogAccepted == true (exactly what +# answering the prompt writes). So we pre-seed that record here. +# +# Idempotent, atomic, best-effort: any failure is non-fatal (the agent still +# launches — worst case it stalls on the gate, i.e. the pre-fix status quo). +# Only the claude runtime needs this; codex/pi have no such gate. +_ensure_claude_workdir_trusted() { + local workdir="$1" + # The path claude keys on is the resolved cwd it is launched in. + local rp + rp=$(cd "$workdir" 2>/dev/null && pwd -P) || rp="$workdir" + # ~/.claude.json lives next to the claude config dir; honor CLAUDE_CONFIG_DIR. + local claude_json="${MOSAIC_CLAUDE_JSON:-${CLAUDE_CONFIG_DIR:+$CLAUDE_CONFIG_DIR/.claude.json}}" + claude_json="${claude_json:-$HOME/.claude.json}" + + if ! command -v python3 >/dev/null 2>&1; then + echo "WARNING: python3 not found; cannot pre-trust '$rp' for claude (agent may stall on the folder-trust gate)" >&2 + return 1 + fi + + # Serialize concurrent agent launches that share ~/.claude.json (flock if available). + local lock="${claude_json}.mosaic-lock" + _seed() { + MOSAIC_CJ="$claude_json" MOSAIC_TRUST_DIR="$rp" python3 - <<'PY' +import json, os, sys, tempfile +cj = os.environ["MOSAIC_CJ"] +d = os.environ["MOSAIC_TRUST_DIR"] +try: + data = json.load(open(cj)) if os.path.exists(cj) else {} + if not isinstance(data, dict): + data = {} +except Exception: + # Never corrupt an unreadable/partial file — bail without writing. + sys.exit(2) +projects = data.setdefault("projects", {}) +entry = projects.get(d) +if not isinstance(entry, dict): + entry = {} + projects[d] = entry +if entry.get("hasTrustDialogAccepted") is True: + sys.exit(0) # already trusted — nothing to do +entry["hasTrustDialogAccepted"] = True +tmp_dir = os.path.dirname(cj) or "." +fd, tmp = tempfile.mkstemp(dir=tmp_dir, prefix=".claude.json.mosaic.") +try: + with os.fdopen(fd, "w") as f: + json.dump(data, f, indent=2) + os.replace(tmp, cj) # atomic +except Exception: + try: + os.unlink(tmp) + except OSError: + pass + sys.exit(3) +PY + } + if command -v flock >/dev/null 2>&1; then + ( flock 9; _seed ) 9>"$lock" 2>/dev/null || _seed + else + _seed + fi +} + +case "$MOSAIC_AGENT_RUNTIME" in + claude) + _ensure_claude_workdir_trusted "$MOSAIC_AGENT_WORKDIR" \ + || echo "WARNING: could not pre-trust workdir for claude agent $AGENT_NAME" >&2 + ;; +esac + # ── Launch the tmux session (no exec — we continue to wire the heartbeat) ──── _tmux new-session -d -s "$AGENT_NAME" -c "$MOSAIC_AGENT_WORKDIR" \ bash -c "$PANE_SHELL_SNIPPET"