Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions .claude/hooks/session-start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#!/bin/bash
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. 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"

# 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
# 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
}

# 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"
}

# 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 workdir
swiftlint_version="$(mise_pin 'aqua:realm/SwiftLint')"
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"
Comment on lines +105 to +110

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)(session-start\.sh|package\.json|Package\.swif|README\.md|\.claude)?' || true

echo "== target lines =="
if [ -f .claude/hooks/session-start.sh ]; then
  nl -ba .claude/hooks/session-start.sh | sed -n '90,120p'
else
  echo "missing .claude/hooks/session-start.sh"
fi

echo "== swiftlint occurrences =="
rg -n "swiftlint|SwiftLint|SWIFTLINT|realm/SwiftLint" . -S || true

echo "== diff summary =="
git diff --stat || true
git diff -- .claude/hooks/session-start.sh | sed -n '1,220p' || true

echo "== package manifests with swiftlint =="
rg -n '"swiftlint"|swiftlint|SwiftLint' -g 'package.json' -g 'Package.swift' . || true

Repository: brightdigit/ConfigKeyKit

Length of output: 2772


🌐 Web query:

SwiftLint official GitHub releases SHA-256 manifest swiftlint_linux_amd64.zip checksum signatures

💡 Result:

The official SwiftLint GitHub releases do not currently provide a dedicated, separate SHA-256 manifest file for the swiftlint_linux_amd64.zip artifact [1][2]. While SwiftLint provides checksums for some Bazel-related files (e.g., bazel.tar.gz.sha256) within their release assets, they do not publish an equivalent file for the Linux binary zip archives [2][3][4]. To verify the integrity of the swiftlint_linux_amd64.zip file, you must compute the SHA-256 checksum manually after downloading and compare it against the expected hash, if available from other trusted infrastructure-as-code or packaging sources that reference these binaries [5][6]. If you are using SwiftLint in an automated environment, the recommended practice for verifying downloaded binaries is to compute the checksum locally using the sha256sum utility [7][8]: sha256sum swiftlint_linux_amd64.zip Note that because the maintainers do not provide an official manifest file for the Linux zip, you cannot verify the file against an official SwiftLint-hosted checksum file [2][3][4]. Some third-party integrations (such as package managers or CI tools) maintain their own lists of known-good hashes for SwiftLint versions [5]. If you are relying on these, you should ensure those sources are trusted. {top_results: [2][3][4][5]}

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target lines =="
python3 - <<'PY' path=.claude/hooks/session-start.sh 120 95 lin=106-110
import sys
path=sys.argv[1]
with open(path, 'r') as f:
    lines=f.read().splitlines()
print(f"file {path} line count {len(lines)}")
for i in range(max(1,len(lines)-125), min(len(lines),130)+1):
    if 90 <= i <= 120:
        print(f"{i:5d}\t{lines[i-1]}")
PY

echo "== swiftlint occurrences =="
rg -n "swiftlint|SwiftLint|SWIFTLINT|realm/SwiftLint|swiftlint_version" . -S || true

echo "== package manifests with swiftlint =="
rg -n '"swiftlint"|SwiftLint|SwiftLint\.Tools|swiftlint' -g 'package.json' -g 'Package.swift' . || true

echo "== tools setup snippets =="
if [ -f .github/actions/setup-tools/action.yml ]; then
  sed -n '1,220p' .github/tasks || sed -n '1,220p' .github/actions/setup-tools/action.yml
fi

echo "== behavioral/static extraction =="
python3 - <<'PY'
import re, pathlib
p=pathlib.Path('.claude/hooks/session-start.sh')
text=p.read_text()
m=re.search(r'swiftlint_version\s*=\s*["'\'']([^"'\'']+)["'\'']', text)
print("swiftlint_version", m.group(1) if m else "NOT FOUND")
asset='swiftlint_linux_amd64.zip'
checksum_tokens=[x for x in ['sha256sum','shasum','sha256']]
found=[]
for tok in checksum_tokens:
    if tok in text.lower():
        found.append(tok)
print("checksum/token", found if found else "NONE")
print("curl asset present", 'curl' in text and asset.replace('_','_') in text)
PY

Repository: brightdigit/ConfigKeyKit

Length of output: 348


Verify the SwiftLint release asset before installation.

The script installs swiftlint_linux_amd64.zip without a checksum or signature check. Add a trusted SHA-256 digest for this release asset and validate the archive before install. If SwiftLint only supports another distribution path, use that path instead of the direct release asset.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/hooks/session-start.sh around lines 105 - 110, Add trusted SHA-256
verification to the SwiftLint download flow in the session-start script: define
the expected digest for the selected swiftlint_version release asset, validate
swiftlint.zip before unzip/install, and abort on mismatch. Keep installation
through the verified archive, or switch to a supported distribution path if the
asset cannot be reliably verified.

echo "SwiftLint $swiftlint_version installed."
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

# 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

# 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"
14 changes: 14 additions & 0 deletions .claude/settings.json
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"

Copy link
Copy Markdown

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 the hook path.

Line 8 expands CLAUDE_PROJECT_DIR without quotes. If the project path contains whitespace, the shell splits the command and the SessionStart hook fails.

Proposed fix
-            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh"
+            "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh\""
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh"
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh\""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/settings.json at line 8, Quote the CLAUDE_PROJECT_DIR-based hook
command in the SessionStart configuration so paths containing whitespace remain
a single shell argument; update the command value without changing the hook
script or surrounding configuration.

}
]
}
]
}
}
6 changes: 5 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 — 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

Expand All @@ -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), 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

`ConfigKeyKit.git/` in the working tree is a bare git repo (a mirror clone), not part of the package — leave it alone.
7 changes: 6 additions & 1 deletion Scripts/lint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading