Skip to content
Merged
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
21 changes: 21 additions & 0 deletions .claude/hooks/post-edit-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# PostToolUse check for Edit and Write. Catches the edits that build clean and fail only at runtime.
set -euo pipefail

path=$(jq -r '.tool_input.file_path // .tool_response.filePath // empty')
[ -n "$path" ] && [ -f "$path" ] || exit 0

case "$path" in
*/composeResources/drawable*/*.xml)
# CMP's vector parser resolves neither; both crash at runtime with a clean build.
if hits=$(grep -nE '\?attr/|@android:' "$path"); then
jq -n --arg r "$path uses ?attr/ or @android: references, which Compose Multiplatform cannot resolve (runtime crash, clean build). Use literal colours such as #FFFFFFFF and let Icon tint from LocalContentColor. Offending lines:
$hits" '{decision: "block", reason: $r}'
fi
;;
*/web/src/commonMain/resources/styles.css | */ui/theme/WebSkin.kt)
# The page colour is duplicated so it paints before any Kotlin runs.
jq -n '{hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: "styles.css duplicates the page colour from WebSkin.kt (it paints before any Kotlin runs). If you changed that colour, change it in the other file too, or every cold load flashes the wrong colour."}}'
;;
esac
exit 0
22 changes: 22 additions & 0 deletions .claude/hooks/pre-commit-ktfmt.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# PreToolUse on `git commit`: CI runs ktfmtCheck, so fail the commit here instead of a CI round trip.
# Only runs Gradle when Kotlin sources are staged.
set -uo pipefail

cd "$CLAUDE_PROJECT_DIR" || exit 0
staged=$(git diff --cached --name-only --diff-filter=ACMR)
grep -Eq '\.kts?$' <<<"$staged" || exit 0

failed=()
if grep -Ev '^build-logic/' <<<"$staged" | grep -Eq '\.kts?$'; then
./gradlew -q ktfmtCheck >/dev/null 2>&1 || failed+=("./gradlew ktfmtFormat")
fi
if grep -Eq '^build-logic/.*\.kts?$' <<<"$staged"; then
./gradlew -q -p build-logic ktfmtCheck >/dev/null 2>&1 || failed+=("./gradlew -p build-logic ktfmtFormat")
fi

if [ ${#failed[@]} -gt 0 ]; then
jq -n --arg r "ktfmtCheck failed on staged Kotlin. Run: ${failed[*]}, then re-stage the files by path and commit again." \
'{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: $r}}'
fi
exit 0
39 changes: 39 additions & 0 deletions .claude/hooks/pre-tool-guard.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# PreToolUse guard for Bash, Edit and Write. Denies the operations this repo documents as never-do,
# each of which fails later and somewhere else rather than at the point of the mistake.
set -euo pipefail

input=$(cat)
tool=$(jq -r '.tool_name // empty' <<<"$input")

deny() {
jq -n --arg r "$1" \
'{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: $r}}'
exit 0
}

case "$tool" in
Bash)
cmd=$(jq -r '.tool_input.command // empty' <<<"$input")
# `git add -A` / `.` / `--all` sweeps Xcode xcuserdata/ on branches below the iOS PR's gitignore.
if grep -Eq '(^|[;&|[:space:]])git[[:space:]]+add([[:space:]]+[^;&|]*)?[[:space:]](-A|--all|\.)([[:space:]]|$|[;&|])' <<<"$cmd"; then
deny "Stage by explicit path, never 'git add -A', '--all' or '.': on some branches the sweep picks up Xcode xcuserdata/. Check 'git status --short' first."
fi
# Stacked PRs: a bare force push can clobber a rebase done elsewhere.
if grep -Eq '(^|[;&|[:space:]])git[[:space:]]+push([[:space:]]|$)' <<<"$cmd" &&
grep -Eq '[[:space:]](--force|-f)([[:space:]]|$)' <<<"$cmd" &&
! grep -q -- '--force-with-lease' <<<"$cmd"; then
deny "Use 'git push --force-with-lease', never a bare --force or -f."
fi
;;
Edit | Write | MultiEdit)
path=$(jq -r '.tool_input.file_path // empty' <<<"$input")
case "$path" in
*/build/*)
deny "$path is build output (Apollo generated code included). Change the source or the .graphql operation instead." ;;
*/kotlin-js-store/*.lock)
deny "Never hand-edit the npm lockfiles. Regenerate both with './gradlew kotlinUpgradeYarnLock kotlinWasmUpgradeYarnLock'." ;;
esac
;;
esac
exit 0
38 changes: 38 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,43 @@
"Bash(adb shell pidof *)",
"Bash(plutil -p *)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/pre-tool-guard.sh",
"timeout": 10
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git commit *)",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/pre-commit-ktfmt.sh",
"timeout": 600,
"statusMessage": "Checking ktfmt on staged Kotlin…"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/post-edit-check.sh",
"timeout": 10
}
]
}
]
}
}
11 changes: 6 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ xcuserdata/
*.xcscmblueprint
.swiftpm/

# Claude Code. `rules/`, `skills/`, `agents/` and `settings.json` are project documentation and
# configuration and are committed — the rules load by path and hold the invariants that fail
# silently, AGENTS.md routes to the skills by task, the agents encode this build's traps and output
# contracts, and all four are reviewable in a PR like any other doc. Everything else
# here — `settings.local.json` above all — is per-user state.
# Claude Code. `rules/`, `hooks/`, `skills/`, `agents/` and `settings.json` are project documentation
# and configuration and are committed — the rules load by path and hold the invariants that fail
# silently, the hooks enforce the never-do rules, AGENTS.md routes to the skills by task, the agents
# encode this build's traps and output contracts, and all five are reviewable in a PR like any other
# doc. Everything else here — `settings.local.json` above all — is per-user state.
.claude/*
!.claude/rules/
!.claude/hooks/
!.claude/skills/
!.claude/agents/
!.claude/settings.json
Loading