Skip to content

session-start.sh

.claude/hooks/session-start.sh

SessionStart

Make the Claude Code on-the-web container match CI so the FULL composer ci:check (coverage + mutation, on the right PHP) AND the browser suite (composer test:browser) run locally before anything is pushed. Without this the cloud box is PHP 8.4 + no coverage driver + a partial vendor/ and no Playwright browser, so "green" from the agent can only be partial — and UI fixes would look unverifiable when they aren't. Idempotent + strictly non-fatal: a provisioning hiccup (network/policy) must never block the session — it just falls back to whatever PHP is present and logs the gap to stderr.

Source

bash
#!/usr/bin/env bash
# SessionStart hook — make the Claude Code on-the-web container match CI so the
# FULL `composer ci:check` (coverage + mutation, on the right PHP) AND the
# browser suite (`composer test:browser`) run locally before anything is
# pushed. Without this the cloud box is PHP 8.4 + no coverage driver + a
# partial vendor/ and no Playwright browser, so "green" from the agent can only
# be partial — and UI fixes would look unverifiable when they aren't.
#
# Idempotent + strictly non-fatal: a provisioning hiccup (network/policy) must
# never block the session — it just falls back to whatever PHP is present and
# logs the gap to stderr.
set -uo pipefail

log() { echo "[session-start] $*" >&2; }

cd "${CLAUDE_PROJECT_DIR:-.}" 2>/dev/null || exit 0

# Composer hard-aborts as root unless this is set (and silently skips the pest
# plugins that boot pest-plugin-browser's auto-connect).
export COMPOSER_ALLOW_SUPERUSER=1

# 1. Match CI's runtime: PHP 8.5 + a coverage driver (pcov).
if php -v 2>/dev/null | grep -q 'PHP 8\.5' && php -m 2>/dev/null | grep -qi '^pcov$'; then
    log "PHP 8.5 + pcov already present."
else
    log "Provisioning PHP 8.5 + pcov to match CI…"
    export DEBIAN_FRONTEND=noninteractive
    apt-get update -qq >/dev/null 2>&1 || log "apt update failed (network?) — continuing"
    apt-get install -y -qq \
        php8.5-cli php8.5-mbstring php8.5-xml php8.5-curl php8.5-sqlite3 \
        php8.5-bcmath php8.5-intl php8.5-zip php8.5-gd php8.5-pcov \
        >/dev/null 2>&1 || log "php8.5 install failed (network/policy?) — staying on $(php -v 2>/dev/null | head -1)"

    if command -v php8.5 >/dev/null 2>&1; then
        update-alternatives --set php /usr/bin/php8.5 >/dev/null 2>&1 || true
    fi
    log "php is now: $(php -v 2>/dev/null | head -1)"
fi

# 2. Bootstrap so artisan / pest / the gate can actually run. Always run the
# install — the container can come up with a *partial* vendor/ (issue #411:
# a missing pest-plugin-browser is a collection-time fatal for the WHOLE
# suite), and a complete install is a ~2s no-op.
composer install --no-interaction --prefer-dist --optimize-autoloader >/dev/null 2>&1 || log "composer install failed"
[ -f .env ] || cp .env.example .env 2>/dev/null || true
grep -q '^APP_KEY=base64:' .env 2>/dev/null || php artisan key:generate >/dev/null 2>&1 || true

# 3. Browser-test capability (verified working in this container 2026-06-12 —
# see docs/learnings/2026-06-12-pest-browser-works-in-web-container.md).
# Playwright already launches Chromium with --no-sandbox by default, so root is
# fine; the only real prerequisites are the npm deps, the browser binary, and
# built assets.
[ -d node_modules/playwright-core ] || npm ci >/dev/null 2>&1 || log "npm ci failed"
npx --no-install playwright install chromium >/dev/null 2>&1 || log "playwright install failed"
[ -f public/build/manifest.json ] || npm run build >/dev/null 2>&1 || log "asset build failed"

# Reap any Playwright run-server / endpoint state a prior session left behind —
# stale state wedges the next browser run into a silent hang (bug #501).
php artisan browser:reap >/dev/null 2>&1 || true

log "ready — $(php -v 2>/dev/null | head -1). Full ci:check (coverage + mutation) and composer test:browser are runnable."

# Title the session from the last substantive skill run (recorded by the
# PostToolUse Skill hook), else the work branch. SessionStart stdout is the only
# channel Claude Code reads `sessionTitle` from — see issue #443. Non-fatal.
bash .claude/hooks/session-title.sh emit || true

exit 0