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"