#!/bin/sh # Claude Code adapter: implements the Mosaic adapter contract for the host's # `claude` CLI, for managed sessions (slice 1 S6). Headless only. # # Contract: see adapters/README.md. # stdout = the turn's answer only; stderr = diagnostics; exit 0 on success. # # The layers this relies on are row S0 lines 1-5 (docs/plans/2026-10-04_slice-1.md, # "What S6 can rely on"): the PreToolUse command hook in MOSAIC_CLAUDE_SETTINGS, # wrapped as `timeout -k 2 10 || exit 2`, and the tool limit (--tools). # --bare is never passed: it turns settings hooks off. set -eu need() { eval "v=\${$1:-}" [ -n "$v" ] || { echo "claude adapter: $1 is required" >&2; exit 2; } } need MOSAIC_SYSTEM_PROMPT_FILE need MOSAIC_REQUEST need MOSAIC_WORKSPACE need MOSAIC_SESSION_DIR need MOSAIC_MODEL need MOSAIC_CLAUDE_SETTINGS need MOSAIC_CLAUDE_MCP_CONFIG for f in "$MOSAIC_SYSTEM_PROMPT_FILE" "$MOSAIC_CLAUDE_SETTINGS" "$MOSAIC_CLAUDE_MCP_CONFIG"; do [ -r "$f" ] || { echo "claude adapter: not readable: $f" >&2; exit 2; } done [ "${MOSAIC_INTERACTIVE:-}" != "1" ] || { echo "claude adapter: interactive mode isn't supported" >&2; exit 2; } mkdir -p "$MOSAIC_WORKSPACE" "$MOSAIC_SESSION_DIR" cd "$MOSAIC_WORKSPACE" # Session: the first turn names a new session id, later turns resume it. # The id is kept only after a turn succeeds, so a failed first turn never # leaves a --resume of a session that doesn't exist. ID_FILE="$MOSAIC_SESSION_DIR/claude-session-id" if [ -s "$ID_FILE" ]; then SESSION_FLAG=--resume SESSION_ID=$(cat "$ID_FILE") else SESSION_FLAG=--session-id SESSION_ID=$(cat /proc/sys/kernel/random/uuid) fi # Flags: # -p one-shot: print the answer and exit # --output-format text stdout is the answer only # --system-prompt the generated prompt replaces the default # --restricted no user/project/local settings files; --settings # (the gate hook) still applies; no code-running # tool unless --tools names it; file tools confined # to the working directory; no CLAUDE.md file or # auto-memory in the prompt. The S0 lines above # don't depend on it, but it is what keeps founder # and repository memory out; a test fails without it. # --tools the built-in tool limit (S0 line 5); empty = none # --allowedTools the same tools plus the mosaic MCP server, so # --permission-mode dontAsk nothing waits on a prompt and anything else is denied # --settings the gate hook (S0 lines 1-4) # --strict-mcp-config only the bundle's MCP server, which serves the # --mcp-config typed bus tools # --disable-slash-commands no skills (the bundle lists none) ALLOWED=mcp__mosaic [ -z "${MOSAIC_TOOLS:-}" ] || ALLOWED="$MOSAIC_TOOLS,mcp__mosaic" PROMPT_CONTENT="$(cat "$MOSAIC_SYSTEM_PROMPT_FILE")" claude -p "$MOSAIC_REQUEST" \ --output-format text \ --system-prompt "$PROMPT_CONTENT" \ --model "$MOSAIC_MODEL" \ --restricted \ --tools "${MOSAIC_TOOLS:-}" \ --allowedTools "$ALLOWED" \ --permission-mode dontAsk \ --settings "$MOSAIC_CLAUDE_SETTINGS" \ --strict-mcp-config \ --mcp-config "$MOSAIC_CLAUDE_MCP_CONFIG" \ --disable-slash-commands \ "$SESSION_FLAG" "$SESSION_ID" || exit $? [ "$SESSION_FLAG" = --resume ] || printf '%s\n' "$SESSION_ID" > "$ID_FILE"