#!/usr/bin/env bash # Bring up the headless display, then hand over. # # Xvfb and openbox are started as children of PID 1 (tini), NOT of the agent's # shell, so they outlive any single command. The RE container learned this the # hard way: a display owned by a shell gets reaped when that shell exits, which # reads as "Xvfb dies on its own every few minutes". set -euo pipefail : "${DISPLAY:=:97}" : "${SCREEN_GEOMETRY:=1280x720x24}" if ! xdpyinfo -display "$DISPLAY" >/dev/null 2>&1; then Xvfb "$DISPLAY" -screen 0 "$SCREEN_GEOMETRY" -nolisten tcp & for _ in $(seq 50); do xdpyinfo -display "$DISPLAY" >/dev/null 2>&1 && break sleep 0.1 done openbox >/dev/null 2>&1 & fi echo "[entrypoint] display $DISPLAY ready ($SCREEN_GEOMETRY)" if [ -d /reborn ]; then echo "[entrypoint] /reborn mounted read-only — HANDOFF.md is the contract" fi # Seed ~/.claude.json from the host's read-only copy, then stamp onboarding as # complete. Claude Code re-runs its first-run wizard whenever # lastOnboardingVersion differs from the installed version, so a container with a # newer Claude than the host stops on the theme picker -- no error, no log line, # and an unattended agent sits there forever. if [ -f "$HOME/.claude.host.json" ] && [ ! -s "$HOME/.claude.json" ]; then cp "$HOME/.claude.host.json" "$HOME/.claude.json" 2>/dev/null || true fi # Same reason as .claude.json above: `credential.helper=store` rewrites this # file by rename-over-target, which fails with EBUSY on a bind mount. Copy it to # a writable path; nothing is ever written back to the host's file. if [ -f "$HOME/.git-credentials.host" ]; then cp "$HOME/.git-credentials.host" "$HOME/.git-credentials" 2>/dev/null || true chmod 600 "$HOME/.git-credentials" 2>/dev/null || true fi CLAUDE_VER=$(claude --version 2>/dev/null | grep -oE '^[0-9][0-9.]*' || echo 0.0.0) python3 /usr/local/bin/seed-claude-config.py "$HOME/.claude.json" "$CLAUDE_VER" \ "$PWD" "${PROJECT_DIR:-/work}" "$HOME" || true chmod 600 "$HOME/.claude.json" 2>/dev/null || true # ── Claude Code ────────────────────────────────────────────────────────────── # Without this the loop prompt is handed to `exec` as a command, and the whole # markdown file is tried as a filename: exit 126, "File name too long". if [ "${SYLPH_AUTONOMOUS:-0}" = "1" ]; then # Drop the image's default CMD first, or `claude` is handed the literal string # "bash" as its prompt and answers a question nobody asked. if [ "$#" -eq 1 ] && [ "$1" = "bash" ]; then set -- fi # Remote Control registers the session with the account so the agent can be # reached from claude.ai -- the point of a detached run being that nobody is # sitting in front of it. The name is passed EXPLICITLY: the flag's value is # optional, so a bare --remote-control swallows the /loop prompt after it. if [ "${SYLPH_REMOTE:-1}" != "0" ]; then set -- --remote-control "${SYLPH_REMOTE_NAME:-sylpheed-port}" "$@" echo "[entrypoint] Remote Control as '${SYLPH_REMOTE_NAME:-sylpheed-port}'" fi # claude-autonomous wraps `claude --dangerously-skip-permissions` in a pty and # answers the one-time first-run gates. The Bypass Permissions disclaimer has # no config key that skips it, so unattended it hangs forever. set -- claude-autonomous "$@" echo "[entrypoint] starting Claude Code in $(pwd)" fi exec "$@"