From 5b9f1ad8cbb9894240c80b72fc3d0e056bbb3743 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 07:39:49 +0000 Subject: [PATCH 1/6] Add SessionStart hook installing Swift via swiftly for web sessions Installs the swift.org Ubuntu 24.04 system deps, then swiftly and the toolchain pinned by .swift-version, and persists the Swift PATH to CLAUDE_ENV_FILE. Skips entirely when swift is already installed or the session is not remote. Not yet verified end-to-end: download.swift.org is currently blocked by the environment network policy and must be allowlisted first. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012gBcWa6xeuaMwdvzSfruns --- .claude/hooks/session-start.sh | 81 ++++++++++++++++++++++++++++++++++ .claude/settings.json | 14 ++++++ 2 files changed, 95 insertions(+) create mode 100755 .claude/hooks/session-start.sh create mode 100644 .claude/settings.json diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh new file mode 100755 index 0000000..1304d1f --- /dev/null +++ b/.claude/hooks/session-start.sh @@ -0,0 +1,81 @@ +#!/bin/bash +set -euo pipefail + +# SessionStart hook: install a Swift toolchain for Claude Code on the web +# (Linux). Only runs in remote sessions; local sessions are untouched. +if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then + exit 0 +fi + +SWIFTLY_ENV="$HOME/.local/share/swiftly/env.sh" + +persist_path() { + # Make swift available to later Bash commands in the session. + if [ -n "${CLAUDE_ENV_FILE:-}" ]; then + { + echo "export SWIFTLY_HOME_DIR=\"$HOME/.local/share/swiftly\"" + echo "export SWIFTLY_BIN_DIR=\"$HOME/.local/share/swiftly/bin\"" + echo "export PATH=\"$HOME/.local/share/swiftly/bin:\$PATH\"" + } >> "$CLAUDE_ENV_FILE" + fi +} + +# Pick up a swiftly install from a previous (cached) hook run. +if [ -f "$SWIFTLY_ENV" ]; then + # shellcheck disable=SC1090 + . "$SWIFTLY_ENV" +fi + +if command -v swift > /dev/null 2>&1; then + echo "Swift already installed: $(swift --version 2>&1 | head -1)" + if [ -f "$SWIFTLY_ENV" ]; then + persist_path + fi + exit 0 +fi + +# System dependencies for Swift on Ubuntu 24.04 (per swift.org Linux +# instructions), plus curl for fetching swiftly. +export DEBIAN_FRONTEND=noninteractive +apt-get update -qq +apt-get install -y -qq \ + binutils \ + curl \ + git \ + gnupg2 \ + libc6-dev \ + libcurl4-openssl-dev \ + libedit2 \ + libgcc-13-dev \ + libncurses-dev \ + libpython3-dev \ + libsqlite3-0 \ + libstdc++-13-dev \ + libxml2-dev \ + libz3-dev \ + pkg-config \ + tzdata \ + unzip \ + zlib1g-dev + +# Install swiftly non-interactively, then the toolchain pinned by the +# repo's .swift-version (falling back to latest if no pin resolves). +WORKDIR="$(mktemp -d)" +trap 'rm -rf "$WORKDIR"' EXIT +cd "$WORKDIR" +curl -fsSLO "https://download.swift.org/swiftly/linux/swiftly-$(uname -m).tar.gz" +tar zxf "swiftly-$(uname -m).tar.gz" +./swiftly init -y --skip-install + +# shellcheck disable=SC1090 +. "$SWIFTLY_ENV" + +cd "${CLAUDE_PROJECT_DIR:-$PWD}" +if ! swiftly install -y; then + echo "Pinned toolchain install failed; falling back to latest." >&2 + swiftly install -y latest + swiftly use -y latest +fi + +persist_path +swift --version diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..e06b033 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh" + } + ] + } + ] + } +} From 9ee5643121106d4d5474b085b767a33250169839 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 07:48:18 +0000 Subject: [PATCH 2/6] Document Linux/SPM-only builds and web-session Swift setup in CLAUDE.md Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012gBcWa6xeuaMwdvzSfruns --- CLAUDE.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 4b071e9..b00e1cb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,6 +36,10 @@ Both store the same three fields: `baseKey`, a `styles` map (`ConfigKeySource -> - Every source file carries the MIT license header (copyright "Leo Dion" / "BrightDigit"); `Scripts/header.sh` enforces it. New files need it. - `periphery.yml` sets `retain_public: true`, so public API is never flagged as dead code. +## Linux builds + +This repo builds on Linux via SPM only — no Xcode, no Apple SDKs. The `platforms:` list in `Package.swift` applies to Apple platforms only and is ignored on Linux. **No targets are excluded on Linux**: both `ConfigKeyKit` and `ConfigKeyKitTests` build and test there (CI runs them in `swift:` containers). In Claude Code on the web, the SessionStart hook `.claude/hooks/session-start.sh` installs the toolchain via swiftly, pinned by `.swift-version` (requires `download.swift.org` on the environment's network allowlist). Lint tooling (`make lint` via mise) is not installed by the hook. + ## Note `ConfigKeyKit.git/` in the working tree is a bare git repo (a mirror clone), not part of the package — leave it alone. From 44bb0974f2ef4919e99c1065cc650c0d223881b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 07:59:49 +0000 Subject: [PATCH 3/6] Install mise lint tooling from the SessionStart hook Adds a soft-failing mise section after the Swift install: installs mise via mise.run, trusts the repo config, and runs mise install for the pinned swift-format, SwiftLint, and periphery. Lint tooling failure warns loudly but leaves the session able to build and test. Not yet verified end-to-end: mise.run and mise.jdx.dev are blocked by the environment network policy and must be allowlisted first. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012gBcWa6xeuaMwdvzSfruns --- .claude/hooks/session-start.sh | 121 ++++++++++++++++++++------------- CLAUDE.md | 2 +- 2 files changed, 73 insertions(+), 50 deletions(-) diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh index 1304d1f..2f098a7 100755 --- a/.claude/hooks/session-start.sh +++ b/.claude/hooks/session-start.sh @@ -1,25 +1,86 @@ #!/bin/bash set -euo pipefail -# SessionStart hook: install a Swift toolchain for Claude Code on the web -# (Linux). Only runs in remote sessions; local sessions are untouched. +# SessionStart hook: install a Swift toolchain and lint tooling for Claude +# Code on the web (Linux). Only runs in remote sessions; local sessions are +# untouched. if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then exit 0 fi SWIFTLY_ENV="$HOME/.local/share/swiftly/env.sh" +PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}" persist_path() { - # Make swift available to later Bash commands in the session. + # Make swift and mise available to later Bash commands in the session. if [ -n "${CLAUDE_ENV_FILE:-}" ]; then { echo "export SWIFTLY_HOME_DIR=\"$HOME/.local/share/swiftly\"" echo "export SWIFTLY_BIN_DIR=\"$HOME/.local/share/swiftly/bin\"" - echo "export PATH=\"$HOME/.local/share/swiftly/bin:\$PATH\"" + echo "export PATH=\"$HOME/.local/share/swiftly/bin:$HOME/.local/bin:\$PATH\"" } >> "$CLAUDE_ENV_FILE" fi } +install_swift() { + # System dependencies for Swift on Ubuntu 24.04 (per swift.org Linux + # instructions), plus curl for fetching swiftly. + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y -qq \ + binutils \ + curl \ + git \ + gnupg2 \ + libc6-dev \ + libcurl4-openssl-dev \ + libedit2 \ + libgcc-13-dev \ + libncurses-dev \ + libpython3-dev \ + libsqlite3-0 \ + libstdc++-13-dev \ + libxml2-dev \ + libz3-dev \ + pkg-config \ + tzdata \ + unzip \ + zlib1g-dev + + # Install swiftly non-interactively, then the toolchain pinned by the + # repo's .swift-version (falling back to latest if no pin resolves). + local workdir + workdir="$(mktemp -d)" + pushd "$workdir" > /dev/null + curl -fsSLO "https://download.swift.org/swiftly/linux/swiftly-$(uname -m).tar.gz" + tar zxf "swiftly-$(uname -m).tar.gz" + ./swiftly init -y --skip-install + popd > /dev/null + rm -rf "$workdir" + + # shellcheck disable=SC1090 + . "$SWIFTLY_ENV" + + cd "$PROJECT_DIR" + if ! swiftly install -y; then + echo "Pinned toolchain install failed; falling back to latest." >&2 + swiftly install -y latest + swiftly use -y latest + fi +} + +install_lint_tools() { + # Lint tooling (swift-format, SwiftLint, periphery) pinned via mise.toml. + # The spm-backend tools compile from source, so the first run is slow; + # container caching makes later sessions instant. + export PATH="$HOME/.local/bin:$PATH" + if ! command -v mise > /dev/null 2>&1; then + curl -fsSL https://mise.run | sh + fi + mise trust --yes "$PROJECT_DIR/mise.toml" + mise --cd "$PROJECT_DIR" install --yes +} + # Pick up a swiftly install from a previous (cached) hook run. if [ -f "$SWIFTLY_ENV" ]; then # shellcheck disable=SC1090 @@ -28,53 +89,15 @@ fi if command -v swift > /dev/null 2>&1; then echo "Swift already installed: $(swift --version 2>&1 | head -1)" - if [ -f "$SWIFTLY_ENV" ]; then - persist_path - fi - exit 0 +else + install_swift fi -# System dependencies for Swift on Ubuntu 24.04 (per swift.org Linux -# instructions), plus curl for fetching swiftly. -export DEBIAN_FRONTEND=noninteractive -apt-get update -qq -apt-get install -y -qq \ - binutils \ - curl \ - git \ - gnupg2 \ - libc6-dev \ - libcurl4-openssl-dev \ - libedit2 \ - libgcc-13-dev \ - libncurses-dev \ - libpython3-dev \ - libsqlite3-0 \ - libstdc++-13-dev \ - libxml2-dev \ - libz3-dev \ - pkg-config \ - tzdata \ - unzip \ - zlib1g-dev - -# Install swiftly non-interactively, then the toolchain pinned by the -# repo's .swift-version (falling back to latest if no pin resolves). -WORKDIR="$(mktemp -d)" -trap 'rm -rf "$WORKDIR"' EXIT -cd "$WORKDIR" -curl -fsSLO "https://download.swift.org/swiftly/linux/swiftly-$(uname -m).tar.gz" -tar zxf "swiftly-$(uname -m).tar.gz" -./swiftly init -y --skip-install - -# shellcheck disable=SC1090 -. "$SWIFTLY_ENV" - -cd "${CLAUDE_PROJECT_DIR:-$PWD}" -if ! swiftly install -y; then - echo "Pinned toolchain install failed; falling back to latest." >&2 - swiftly install -y latest - swiftly use -y latest +# Lint tooling is secondary to the toolchain: warn loudly on failure but +# leave the session usable for building and testing. +if ! install_lint_tools; then + echo "WARNING: lint tooling install failed; make lint will not work." >&2 + echo "WARNING: swift build/test are unaffected. See errors above." >&2 fi persist_path diff --git a/CLAUDE.md b/CLAUDE.md index b00e1cb..36538f2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,7 +38,7 @@ Both store the same three fields: `baseKey`, a `styles` map (`ConfigKeySource -> ## Linux builds -This repo builds on Linux via SPM only — no Xcode, no Apple SDKs. The `platforms:` list in `Package.swift` applies to Apple platforms only and is ignored on Linux. **No targets are excluded on Linux**: both `ConfigKeyKit` and `ConfigKeyKitTests` build and test there (CI runs them in `swift:` containers). In Claude Code on the web, the SessionStart hook `.claude/hooks/session-start.sh` installs the toolchain via swiftly, pinned by `.swift-version` (requires `download.swift.org` on the environment's network allowlist). Lint tooling (`make lint` via mise) is not installed by the hook. +This repo builds on Linux via SPM only — no Xcode, no Apple SDKs. The `platforms:` list in `Package.swift` applies to Apple platforms only and is ignored on Linux. **No targets are excluded on Linux**: both `ConfigKeyKit` and `ConfigKeyKitTests` build and test there (CI runs them in `swift:` containers). In Claude Code on the web, the SessionStart hook `.claude/hooks/session-start.sh` installs the toolchain via swiftly, pinned by `.swift-version` (requires `download.swift.org` on the environment's network allowlist), plus mise and the pinned lint tools so `make lint` works too. The first container build is slow — swift-format and periphery compile from source — but the result is cached for later sessions. ## Note From f7223c69fd74f7e242b15f7dd7eb2b1021c46bf7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 12:53:11 +0000 Subject: [PATCH 4/6] Install lint tools directly and make the SessionStart hook async mise install cannot run in Claude Code web sessions: the session's GitHub gateway scopes api.github.com to session-attached repos, so mise's release lookups 403 on the tool repos. Install the same pinned versions (parsed from mise.toml) via anonymous public git clones and release-asset downloads instead: SwiftLint as a prebuilt binary, swift-format and periphery built from source. The hook now runs async so the session starts immediately; progress logs to ~/.claude-session-setup.log and ~/.claude-session-setup.done marks completion. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012gBcWa6xeuaMwdvzSfruns --- .claude/hooks/session-start.sh | 95 +++++++++++++++++++++++++++------- CLAUDE.md | 4 +- 2 files changed, 77 insertions(+), 22 deletions(-) diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh index 2f098a7..348a3b7 100755 --- a/.claude/hooks/session-start.sh +++ b/.claude/hooks/session-start.sh @@ -3,24 +3,32 @@ set -euo pipefail # SessionStart hook: install a Swift toolchain and lint tooling for Claude # Code on the web (Linux). Only runs in remote sessions; local sessions are -# untouched. +# untouched. Runs async so the session starts immediately: progress lands in +# ~/.claude-session-setup.log and ~/.claude-session-setup.done marks the end. if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then exit 0 fi +echo '{"async": true, "asyncTimeout": 2400000}' + +SETUP_LOG="$HOME/.claude-session-setup.log" +SETUP_DONE="$HOME/.claude-session-setup.done" +rm -f "$SETUP_DONE" +exec >> "$SETUP_LOG" 2>&1 + SWIFTLY_ENV="$HOME/.local/share/swiftly/env.sh" PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}" +TOOLS_BIN="$HOME/.local/bin" -persist_path() { - # Make swift and mise available to later Bash commands in the session. - if [ -n "${CLAUDE_ENV_FILE:-}" ]; then - { - echo "export SWIFTLY_HOME_DIR=\"$HOME/.local/share/swiftly\"" - echo "export SWIFTLY_BIN_DIR=\"$HOME/.local/share/swiftly/bin\"" - echo "export PATH=\"$HOME/.local/share/swiftly/bin:$HOME/.local/bin:\$PATH\"" - } >> "$CLAUDE_ENV_FILE" - fi -} +# Make swift and the lint tools reachable for the session up front; entries +# pointing at not-yet-populated directories are harmless. +if [ -n "${CLAUDE_ENV_FILE:-}" ]; then + { + echo "export SWIFTLY_HOME_DIR=\"$HOME/.local/share/swiftly\"" + echo "export SWIFTLY_BIN_DIR=\"$HOME/.local/share/swiftly/bin\"" + echo "export PATH=\"$HOME/.local/share/swiftly/bin:$TOOLS_BIN:\$PATH\"" + } >> "$CLAUDE_ENV_FILE" +fi install_swift() { # System dependencies for Swift on Ubuntu 24.04 (per swift.org Linux @@ -69,16 +77,63 @@ install_swift() { fi } +# Read a tool's pinned version out of mise.toml so the pins have one source +# of truth shared with CI and local dev. +mise_pin() { + sed -n "s|.*$1\" *= *\"\([^\"]*\)\".*|\1|p" "$PROJECT_DIR/mise.toml" +} + +# Build a SwiftPM executable from a public GitHub repo at a pinned tag and +# drop the binary into TOOLS_BIN. Web sessions cannot use `mise install` +# for this: the session's GitHub gateway scopes api.github.com to repos +# attached to the session, and mise's version resolution 403s on the tool +# repos. Anonymous public git clones and release-asset downloads do work, +# so the hook installs the same pinned versions through those paths. +build_spm_tool() { + local repo="$1" tag="$2" binary="$3" workdir + workdir="$(mktemp -d)" + git clone --depth 1 --branch "$tag" "https://github.com/$repo.git" "$workdir/src" + swift build --package-path "$workdir/src" -c release --product "$binary" + install -m 755 "$workdir/src/.build/release/$binary" "$TOOLS_BIN/$binary" + rm -rf "$workdir" +} + install_lint_tools() { - # Lint tooling (swift-format, SwiftLint, periphery) pinned via mise.toml. - # The spm-backend tools compile from source, so the first run is slow; - # container caching makes later sessions instant. - export PATH="$HOME/.local/bin:$PATH" - if ! command -v mise > /dev/null 2>&1; then - curl -fsSL https://mise.run | sh + local swiftlint_version swift_format_version periphery_version workdir + swiftlint_version="$(mise_pin 'aqua:realm/SwiftLint')" + swift_format_version="$(mise_pin 'spm:swiftlang/swift-format')" + periphery_version="$(mise_pin 'spm:peripheryapp/periphery')" + mkdir -p "$TOOLS_BIN" + export PATH="$TOOLS_BIN:$PATH" + + if command -v swiftlint > /dev/null 2>&1 \ + && [ "$(swiftlint --version)" = "$swiftlint_version" ]; then + echo "SwiftLint $swiftlint_version already installed." + else + workdir="$(mktemp -d)" + curl -fsSL -o "$workdir/swiftlint.zip" \ + "https://github.com/realm/SwiftLint/releases/download/$swiftlint_version/swiftlint_linux_amd64.zip" + unzip -q -o "$workdir/swiftlint.zip" -d "$workdir" + install -m 755 "$workdir/swiftlint" "$TOOLS_BIN/swiftlint" + rm -rf "$workdir" + echo "SwiftLint $swiftlint_version installed." + fi + + if command -v swift-format > /dev/null 2>&1 \ + && swift-format --version | grep -q "$swift_format_version"; then + echo "swift-format $swift_format_version already installed." + else + build_spm_tool "swiftlang/swift-format" "$swift_format_version" "swift-format" + echo "swift-format $swift_format_version installed." + fi + + if command -v periphery > /dev/null 2>&1 \ + && periphery version | grep -q "$periphery_version"; then + echo "periphery $periphery_version already installed." + else + build_spm_tool "peripheryapp/periphery" "$periphery_version" "periphery" + echo "periphery $periphery_version installed." fi - mise trust --yes "$PROJECT_DIR/mise.toml" - mise --cd "$PROJECT_DIR" install --yes } # Pick up a swiftly install from a previous (cached) hook run. @@ -100,5 +155,5 @@ if ! install_lint_tools; then echo "WARNING: swift build/test are unaffected. See errors above." >&2 fi -persist_path swift --version +touch "$SETUP_DONE" diff --git a/CLAUDE.md b/CLAUDE.md index 36538f2..346ae76 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,7 +14,7 @@ ConfigKeyKit is a tiny, **dependency-free, Foundation-only** Swift 6.2 library ( - `make lint` — runs `Scripts/lint.sh`: swift-format, SwiftLint, license-header check, and `periphery` dead-code scan - `make clean` -Lint/format tooling is pinned via **mise** (`mise.toml`): swift-format 602.0.0, SwiftLint 0.62.2, periphery 3.7.4. Run `mise install` once so `Scripts/lint.sh` can find them outside CI. `Scripts/lint.sh` is env-driven: `LINT_MODE` (`STRICT` adds `--strict`/`--configuration`; `NONE`/`INSTALL` short-circuit), `FORMAT_ONLY=1` skips lint+build, and outside CI it auto-formats in place before linting. +Lint/format tooling is pinned via **mise** (`mise.toml`): swift-format 602.0.0, SwiftLint 0.62.2, periphery 3.7.4. Run `mise install` once so `Scripts/lint.sh` can find them outside CI (not in Claude Code web sessions — there the SessionStart hook installs the same pinned versions directly, see "Linux builds"). `Scripts/lint.sh` is env-driven: `LINT_MODE` (`STRICT` adds `--strict`/`--configuration`; `NONE`/`INSTALL` short-circuit), `FORMAT_ONLY=1` skips lint+build, and outside CI it auto-formats in place before linting. ## Architecture @@ -38,7 +38,7 @@ Both store the same three fields: `baseKey`, a `styles` map (`ConfigKeySource -> ## Linux builds -This repo builds on Linux via SPM only — no Xcode, no Apple SDKs. The `platforms:` list in `Package.swift` applies to Apple platforms only and is ignored on Linux. **No targets are excluded on Linux**: both `ConfigKeyKit` and `ConfigKeyKitTests` build and test there (CI runs them in `swift:` containers). In Claude Code on the web, the SessionStart hook `.claude/hooks/session-start.sh` installs the toolchain via swiftly, pinned by `.swift-version` (requires `download.swift.org` on the environment's network allowlist), plus mise and the pinned lint tools so `make lint` works too. The first container build is slow — swift-format and periphery compile from source — but the result is cached for later sessions. +This repo builds on Linux via SPM only — no Xcode, no Apple SDKs. The `platforms:` list in `Package.swift` applies to Apple platforms only and is ignored on Linux. **No targets are excluded on Linux**: both `ConfigKeyKit` and `ConfigKeyKitTests` build and test there (CI runs them in `swift:` containers). In Claude Code on the web, the SessionStart hook `.claude/hooks/session-start.sh` installs the toolchain via swiftly, pinned by `.swift-version` (requires `download.swift.org` on the environment's network allowlist), plus the lint tools at the versions pinned in `mise.toml` so `make lint` works too. It installs them directly (SwiftLint prebuilt binary; swift-format and periphery built from source) because `mise install` cannot work in web sessions — the session's GitHub gateway scopes `api.github.com` to session-attached repos, so mise's release lookups 403; mise stays the install path for CI and local dev. The hook runs **async**: the session starts immediately while installs continue in the background, so on a brand-new container `swift` and the lint tools can take several minutes to appear — progress is in `~/.claude-session-setup.log`, and `~/.claude-session-setup.done` marks completion; wait for it before treating a missing tool as an error. Cached containers have everything instantly. ## Note From 275c92fd3ce473b9443b365856d8be7e84a7379a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 13:19:54 +0000 Subject: [PATCH 5/6] Set LINUX_SOURCEKIT_LIB_PATH so SwiftLint works in web sessions SwiftLint dlopens libsourcekitdInProc.so from the Swift toolchain and locates it via LINUX_SOURCEKIT_LIB_PATH; without it, swiftlint crashes on Linux. Resolve the toolchain lib dir after install and persist the export to the session env file. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012gBcWa6xeuaMwdvzSfruns --- .claude/hooks/session-start.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh index 348a3b7..45e8e9c 100755 --- a/.claude/hooks/session-start.sh +++ b/.claude/hooks/session-start.sh @@ -155,5 +155,13 @@ if ! install_lint_tools; then echo "WARNING: swift build/test are unaffected. See errors above." >&2 fi +# SwiftLint on Linux dlopens libsourcekitdInProc.so and finds it through +# LINUX_SOURCEKIT_LIB_PATH; resolve it now that the toolchain exists. +sourcekit_lib="$(find "$HOME/.local/share/swiftly/toolchains" \ + -name libsourcekitdInProc.so -exec dirname {} \; 2> /dev/null | head -1)" +if [ -n "$sourcekit_lib" ] && [ -n "${CLAUDE_ENV_FILE:-}" ]; then + echo "export LINUX_SOURCEKIT_LIB_PATH=\"$sourcekit_lib\"" >> "$CLAUDE_ENV_FILE" +fi + swift --version touch "$SETUP_DONE" From e39698a00712efe67809597a59fbf64d9d87507e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 13:39:27 +0000 Subject: [PATCH 6/6] Shorten web-session cold start: bundled swift-format, no periphery Stop compiling swift-format and periphery from source in the SessionStart hook (~15 min of the cold start). SwiftLint stays eager via its prebuilt release binary; swift-format now resolves to the binary bundled with the Swift toolchain via the swiftly shims; the periphery scan is skipped in web sessions by Scripts/lint.sh (CLAUDE_CODE_REMOTE guard) since it has no Linux binaries and is not worth building there. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012gBcWa6xeuaMwdvzSfruns --- .claude/hooks/session-start.sh | 44 ++++++++-------------------------- CLAUDE.md | 4 ++-- Scripts/lint.sh | 7 +++++- 3 files changed, 18 insertions(+), 37 deletions(-) diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh index 45e8e9c..fd9c989 100755 --- a/.claude/hooks/session-start.sh +++ b/.claude/hooks/session-start.sh @@ -83,26 +83,18 @@ mise_pin() { sed -n "s|.*$1\" *= *\"\([^\"]*\)\".*|\1|p" "$PROJECT_DIR/mise.toml" } -# Build a SwiftPM executable from a public GitHub repo at a pinned tag and -# drop the binary into TOOLS_BIN. Web sessions cannot use `mise install` -# for this: the session's GitHub gateway scopes api.github.com to repos -# attached to the session, and mise's version resolution 403s on the tool -# repos. Anonymous public git clones and release-asset downloads do work, -# so the hook installs the same pinned versions through those paths. -build_spm_tool() { - local repo="$1" tag="$2" binary="$3" workdir - workdir="$(mktemp -d)" - git clone --depth 1 --branch "$tag" "https://github.com/$repo.git" "$workdir/src" - swift build --package-path "$workdir/src" -c release --product "$binary" - install -m 755 "$workdir/src/.build/release/$binary" "$TOOLS_BIN/$binary" - rm -rf "$workdir" -} - +# Install SwiftLint from its prebuilt Linux release binary. Web sessions +# cannot use `mise install` for this: the session's GitHub gateway scopes +# api.github.com to repos attached to the session, and mise's version +# resolution 403s on the tool repos. Anonymous release-asset downloads do +# work, so the hook installs the same pinned version through that path. +# The other lint tools are deliberately NOT installed here: swift-format +# ships inside the Swift toolchain (swiftly proxies it), and periphery is +# skipped in web sessions entirely (Scripts/lint.sh omits the scan when +# CLAUDE_CODE_REMOTE is set), keeping session cold-start fast. install_lint_tools() { - local swiftlint_version swift_format_version periphery_version workdir + local swiftlint_version workdir swiftlint_version="$(mise_pin 'aqua:realm/SwiftLint')" - swift_format_version="$(mise_pin 'spm:swiftlang/swift-format')" - periphery_version="$(mise_pin 'spm:peripheryapp/periphery')" mkdir -p "$TOOLS_BIN" export PATH="$TOOLS_BIN:$PATH" @@ -118,22 +110,6 @@ install_lint_tools() { rm -rf "$workdir" echo "SwiftLint $swiftlint_version installed." fi - - if command -v swift-format > /dev/null 2>&1 \ - && swift-format --version | grep -q "$swift_format_version"; then - echo "swift-format $swift_format_version already installed." - else - build_spm_tool "swiftlang/swift-format" "$swift_format_version" "swift-format" - echo "swift-format $swift_format_version installed." - fi - - if command -v periphery > /dev/null 2>&1 \ - && periphery version | grep -q "$periphery_version"; then - echo "periphery $periphery_version already installed." - else - build_spm_tool "peripheryapp/periphery" "$periphery_version" "periphery" - echo "periphery $periphery_version installed." - fi } # Pick up a swiftly install from a previous (cached) hook run. diff --git a/CLAUDE.md b/CLAUDE.md index 346ae76..25340ad 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,7 +14,7 @@ ConfigKeyKit is a tiny, **dependency-free, Foundation-only** Swift 6.2 library ( - `make lint` — runs `Scripts/lint.sh`: swift-format, SwiftLint, license-header check, and `periphery` dead-code scan - `make clean` -Lint/format tooling is pinned via **mise** (`mise.toml`): swift-format 602.0.0, SwiftLint 0.62.2, periphery 3.7.4. Run `mise install` once so `Scripts/lint.sh` can find them outside CI (not in Claude Code web sessions — there the SessionStart hook installs the same pinned versions directly, see "Linux builds"). `Scripts/lint.sh` is env-driven: `LINT_MODE` (`STRICT` adds `--strict`/`--configuration`; `NONE`/`INSTALL` short-circuit), `FORMAT_ONLY=1` skips lint+build, and outside CI it auto-formats in place before linting. +Lint/format tooling is pinned via **mise** (`mise.toml`): swift-format 602.0.0, SwiftLint 0.62.2, periphery 3.7.4. Run `mise install` once so `Scripts/lint.sh` can find them outside CI (not in Claude Code web sessions — see "Linux builds" for how tooling works there). `Scripts/lint.sh` is env-driven: `LINT_MODE` (`STRICT` adds `--strict`/`--configuration`; `NONE`/`INSTALL` short-circuit), `FORMAT_ONLY=1` skips lint+build, and outside CI it auto-formats in place before linting. ## Architecture @@ -38,7 +38,7 @@ Both store the same three fields: `baseKey`, a `styles` map (`ConfigKeySource -> ## Linux builds -This repo builds on Linux via SPM only — no Xcode, no Apple SDKs. The `platforms:` list in `Package.swift` applies to Apple platforms only and is ignored on Linux. **No targets are excluded on Linux**: both `ConfigKeyKit` and `ConfigKeyKitTests` build and test there (CI runs them in `swift:` containers). In Claude Code on the web, the SessionStart hook `.claude/hooks/session-start.sh` installs the toolchain via swiftly, pinned by `.swift-version` (requires `download.swift.org` on the environment's network allowlist), plus the lint tools at the versions pinned in `mise.toml` so `make lint` works too. It installs them directly (SwiftLint prebuilt binary; swift-format and periphery built from source) because `mise install` cannot work in web sessions — the session's GitHub gateway scopes `api.github.com` to session-attached repos, so mise's release lookups 403; mise stays the install path for CI and local dev. The hook runs **async**: the session starts immediately while installs continue in the background, so on a brand-new container `swift` and the lint tools can take several minutes to appear — progress is in `~/.claude-session-setup.log`, and `~/.claude-session-setup.done` marks completion; wait for it before treating a missing tool as an error. Cached containers have everything instantly. +This repo builds on Linux via SPM only — no Xcode, no Apple SDKs. The `platforms:` list in `Package.swift` applies to Apple platforms only and is ignored on Linux. **No targets are excluded on Linux**: both `ConfigKeyKit` and `ConfigKeyKitTests` build and test there (CI runs them in `swift:` containers). In Claude Code on the web, the SessionStart hook `.claude/hooks/session-start.sh` installs the toolchain via swiftly, pinned by `.swift-version` (requires `download.swift.org` on the environment's network allowlist), so `make lint` works too — with web-specific tooling: SwiftLint comes from its prebuilt Linux binary at the `mise.toml` pin (`mise install` cannot work in web sessions — the session's GitHub gateway scopes `api.github.com` to session-attached repos, so mise's release lookups 403; mise stays the install path for CI and local dev); swift-format is the one bundled with the Swift toolchain (its version tracks the toolchain, not the `mise.toml` pin — CI strict-lints with the pin, so if formatting disagrees with CI, that drift is why); periphery is not installed, and `Scripts/lint.sh` skips its scan when `CLAUDE_CODE_REMOTE` is set — run periphery locally to catch dead code. The hook runs **async**: the session starts immediately while installs continue in the background, so on a brand-new container `swift` can take a few minutes to appear — progress is in `~/.claude-session-setup.log`, and `~/.claude-session-setup.done` marks completion; wait for it before treating a missing tool as an error. Cached containers have everything instantly. ## Note diff --git a/Scripts/lint.sh b/Scripts/lint.sh index e2e602b..322167c 100755 --- a/Scripts/lint.sh +++ b/Scripts/lint.sh @@ -52,8 +52,13 @@ fi $PACKAGE_DIR/Scripts/header.sh -d $PACKAGE_DIR/Sources -c "Leo Dion" -o "BrightDigit" -p "ConfigKeyKit" -if [ -z "$CI" ]; then +# Periphery does not run in Claude Code web sessions: it would have to be +# built from source there (no Linux binaries, and the session's GitHub +# gateway rules out mise), which is not worth the cold-start cost. +if [ -z "$CI" ] && [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then run_command periphery scan $PERIPHERY_OPTIONS --disable-update-check +elif [ "${CLAUDE_CODE_REMOTE:-}" = "true" ]; then + echo "Skipping periphery scan (Claude Code web session)." fi popd