-
Notifications
You must be signed in to change notification settings - Fork 0
Add SessionStart hook installing Swift via swiftly for web sessions #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
5b9f1ad
Add SessionStart hook installing Swift via swiftly for web sessions
claude 9ee5643
Document Linux/SPM-only builds and web-session Swift setup in CLAUDE.md
claude 44bb097
Install mise lint tooling from the SessionStart hook
claude f7223c6
Install lint tools directly and make the SessionStart hook async
claude 275c92f
Set LINUX_SOURCEKIT_LIB_PATH so SwiftLint works in web sessions
claude e39698a
Shorten web-session cold start: bundled swift-format, no periphery
claude 29ff8e3
Drop lint tooling from the SessionStart hook to cut cold start
claude 272e83b
Move the Swift toolchain install into a cached setup script
claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| #!/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. Runs | ||
| # async so the session starts immediately: progress lands in | ||
| # ~/.claude-session-setup.log and ~/.claude-session-setup.done marks the end. | ||
| # | ||
| # The toolchain is all this installs. Lint tooling is deliberately left out to | ||
| # keep cold start short: swift-format ships inside the toolchain, and both | ||
| # SwiftLint and periphery are skipped in web sessions (Scripts/lint.sh omits | ||
| # them when CLAUDE_CODE_REMOTE is set). Run `make lint` locally, where mise | ||
| # provides the pinned versions, to get full coverage. | ||
| # | ||
| # This hook is the second tier of a two-tier setup. The first tier is | ||
| # Scripts/cloud-setup.sh, pasted into the cloud environment's "Setup script" | ||
| # field: it runs once, then the filesystem is snapshotted and later sessions | ||
| # reuse it, so the ~1 GB toolchain download happens once per environment | ||
| # instead of once per container. When that snapshot exists, the `command -v | ||
| # swift` check below short-circuits and this hook finishes in about a second. | ||
| # | ||
| # The hook is still required on every session for two reasons: a snapshot | ||
| # restores files but not environment variables, so PATH has to be re-exported | ||
| # into CLAUDE_ENV_FILE each time; and an environment with no setup script | ||
| # configured (a fresh clone, another contributor) still needs the toolchain | ||
| # installed from here. | ||
| 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}" | ||
|
|
||
| # Make swift reachable for the session up front; an entry pointing at a | ||
| # not-yet-populated directory is 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:\$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. Most are already in the | ||
| # base image, so only reach for apt when something is genuinely missing -- | ||
| # `apt-get update` alone costs ~10s. | ||
| local packages missing pkg | ||
| packages=( | ||
| 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 | ||
| zlib1g-dev | ||
| ) | ||
| missing=() | ||
| for pkg in "${packages[@]}"; do | ||
| if [ "$(dpkg-query -W -f='${db:Status-Status}' "$pkg" 2> /dev/null)" != "installed" ]; then | ||
| missing+=("$pkg") | ||
| fi | ||
| done | ||
|
|
||
| if [ "${#missing[@]}" -gt 0 ]; then | ||
| echo "Installing missing system packages: ${missing[*]}" | ||
| export DEBIAN_FRONTEND=noninteractive | ||
| apt-get update -qq | ||
| apt-get install -y -qq --no-install-recommends "${missing[@]}" | ||
| else | ||
| echo "All system packages already present; skipping apt." | ||
| fi | ||
|
|
||
| # 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 | ||
| } | ||
|
|
||
| # 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)" | ||
| else | ||
| install_swift | ||
| fi | ||
|
|
||
| swift --version | ||
| touch "$SETUP_DONE" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| { | ||
| "hooks": { | ||
| "SessionStart": [ | ||
| { | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh\"" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,187 @@ | ||
| #!/bin/bash | ||
|
|
||
| # Setup script for Claude Code on the web (cloud environments). | ||
| # | ||
| # Paste this into the environment dialog's "Setup script" field at | ||
| # claude.ai/code. It is committed here so the content stays reviewable and | ||
| # versioned, but the platform reads it from that dialog, not from the repo. | ||
| # | ||
| # Why here and not in the SessionStart hook: a setup script runs once per | ||
| # environment, then Anthropic snapshots the filesystem and reuses that snapshot | ||
| # for later sessions, which skip the script entirely. SessionStart hooks re-run | ||
| # on every session and get no such caching. The Swift toolchain is a ~1 GB | ||
| # download, so it belongs in the snapshot. | ||
| # | ||
| # .claude/hooks/session-start.sh stays as the fallback: it installs the same | ||
| # toolchain when an environment has no setup script configured, and on every | ||
| # session it wires PATH into CLAUDE_ENV_FILE (a filesystem snapshot restores | ||
| # files, not environment variables). | ||
| # | ||
| # Requirements this script is written around: | ||
| # * Must exit 0 -- a non-zero exit makes the session fail to start. | ||
| # * Must finish inside ~5 minutes or the environment cache will not build. | ||
| # Measured cold install is ~2 minutes. | ||
| # * Runs as root on Ubuntu 24.04, before Claude Code launches. | ||
| # * Needs download.swift.org on the environment's allowed-domains list | ||
| # (Network access: Custom, with the default package-manager list included). | ||
|
|
||
| # No `set -e`: every failure path has to fall through to `exit 0` so a bad | ||
| # install degrades to the SessionStart hook rather than bricking the session. | ||
| set -uo pipefail | ||
|
|
||
| # Used only when no .swift-version can be found on disk. Keep in sync with the | ||
| # repo's .swift-version. | ||
| FALLBACK_SWIFT_VERSION="6.3.2" | ||
|
|
||
| SWIFTLY_ENV="$HOME/.local/share/swiftly/env.sh" | ||
|
|
||
| log() { | ||
| # stderr, not stdout: resolve_swift_version's value is read via command | ||
| # substitution, so any stdout chatter would be captured into the version. | ||
| echo "[cloud-setup] $*" >&2 | ||
| } | ||
|
|
||
| # The setup script may run before the repository is checked out, and the | ||
| # checkout path is not contractual, so look in the likely places and fall back | ||
| # to the pinned literal above rather than failing. | ||
| resolve_swift_version() { | ||
| local candidate | ||
| for candidate in \ | ||
| "${CLAUDE_PROJECT_DIR:-/nonexistent}/.swift-version" \ | ||
| "$PWD/.swift-version" \ | ||
| /home/user/*/.swift-version \ | ||
| /workspace/*/.swift-version \ | ||
| /root/*/.swift-version; do | ||
| if [ -f "$candidate" ]; then | ||
| local version | ||
| version="$(tr -d '[:space:]' < "$candidate")" | ||
| if [ -n "$version" ]; then | ||
| log "Using Swift $version pinned by $candidate" | ||
| printf '%s' "$version" | ||
| return 0 | ||
| fi | ||
| fi | ||
| done | ||
| log "No .swift-version found; falling back to Swift $FALLBACK_SWIFT_VERSION" | ||
| printf '%s' "$FALLBACK_SWIFT_VERSION" | ||
| } | ||
|
|
||
| # System dependencies for Swift on Ubuntu 24.04 (per swift.org's Linux | ||
| # instructions), plus curl for fetching swiftly. Most are already in the base | ||
| # image, so only reach for apt when something is genuinely missing: apt-get | ||
| # update alone costs ~10s and pulls in unrelated upgrades. | ||
| install_system_packages() { | ||
| local packages missing pkg | ||
| packages=( | ||
| 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 | ||
| zlib1g-dev | ||
| ) | ||
| missing=() | ||
| for pkg in "${packages[@]}"; do | ||
| if [ "$(dpkg-query -W -f='${db:Status-Status}' "$pkg" 2> /dev/null)" != "installed" ]; then | ||
| missing+=("$pkg") | ||
| fi | ||
| done | ||
|
|
||
| if [ "${#missing[@]}" -eq 0 ]; then | ||
| log "All system packages already present; skipping apt." | ||
| return 0 | ||
| fi | ||
|
|
||
| log "Installing missing system packages: ${missing[*]}" | ||
| export DEBIAN_FRONTEND=noninteractive | ||
| apt-get update -qq || return 1 | ||
| apt-get install -y -qq --no-install-recommends "${missing[@]}" || return 1 | ||
| } | ||
|
|
||
| install_swiftly() { | ||
| local workdir | ||
| workdir="$(mktemp -d)" || return 1 | ||
| ( | ||
| cd "$workdir" || exit 1 | ||
| curl -fsSLO "https://download.swift.org/swiftly/linux/swiftly-$(uname -m).tar.gz" || exit 1 | ||
| tar zxf "swiftly-$(uname -m).tar.gz" || exit 1 | ||
| ./swiftly init -y --skip-install || exit 1 | ||
| ) | ||
| local status=$? | ||
| rm -rf "$workdir" | ||
| return "$status" | ||
| } | ||
|
|
||
| # Make swift resolvable for plain login shells too. This is a file, so the | ||
| # environment snapshot carries it; the SessionStart hook still handles | ||
| # CLAUDE_ENV_FILE for Claude Code's own process. | ||
| write_profile_entry() { | ||
| cat > /etc/profile.d/swiftly.sh <<'PROFILE' | ||
| # Added by ConfigKeyKit Scripts/cloud-setup.sh | ||
| export SWIFTLY_HOME_DIR="$HOME/.local/share/swiftly" | ||
| export SWIFTLY_BIN_DIR="$HOME/.local/share/swiftly/bin" | ||
| case ":$PATH:" in | ||
| *":$SWIFTLY_BIN_DIR:"*) ;; | ||
| *) export PATH="$SWIFTLY_BIN_DIR:$PATH" ;; | ||
| esac | ||
| PROFILE | ||
| } | ||
|
|
||
| main() { | ||
| if [ -f "$SWIFTLY_ENV" ]; then | ||
| # shellcheck disable=SC1090 | ||
| . "$SWIFTLY_ENV" | ||
| fi | ||
|
|
||
| if command -v swift > /dev/null 2>&1; then | ||
| log "Swift already installed: $(swift --version 2>&1 | head -1)" | ||
| write_profile_entry | ||
| return 0 | ||
| fi | ||
|
|
||
| local version | ||
| version="$(resolve_swift_version)" | ||
|
|
||
| install_system_packages || { | ||
| log "WARNING: system package install failed; continuing anyway." | ||
| } | ||
|
|
||
| install_swiftly || { | ||
| log "ERROR: swiftly install failed." | ||
| return 1 | ||
| } | ||
|
|
||
| # shellcheck disable=SC1090 | ||
| . "$SWIFTLY_ENV" || return 1 | ||
|
|
||
| if ! swiftly install -y "$version"; then | ||
| log "Pinned toolchain $version failed to install; falling back to latest." | ||
| swiftly install -y latest || return 1 | ||
| swiftly use -y latest || return 1 | ||
| else | ||
| swiftly use -y "$version" || return 1 | ||
| fi | ||
|
|
||
| write_profile_entry | ||
| swift --version | ||
| } | ||
|
|
||
| if main; then | ||
| log "Setup complete." | ||
| else | ||
| log "Setup did not complete; the SessionStart hook will install Swift instead." | ||
| fi | ||
|
|
||
| # Always succeed: a non-zero exit here stops the session from starting. | ||
| exit 0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Quote and check the package-directory change.
pushd $PACKAGE_DIRsplits paths containing whitespace. Ifpushdfails, later lint commands can run from the caller directory. Quote the path and stop the script when the directory change fails.Proposed fix
📝 Committable suggestion
🧰 Tools
🪛 Shellcheck (0.11.0)
[warning] 50-50: Use 'pushd ... || exit' or 'pushd ... || return' in case pushd fails.
(SC2164)
[info] 50-50: Double quote to prevent globbing and word splitting.
(SC2086)
🤖 Prompt for AI Agents
Source: Linters/SAST tools