@spardutti/claude-skills 2.16.1 → 2.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/lib/setup-hook.mjs +198 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -164,6 +164,10 @@ The last two guard the **back** — nothing finishes or ships until the code pas
|
|
|
164
164
|
- `gauntlet.sh` — a `Stop` hook running the fast gates on your changed files (see [Automatic Verification](#automatic-verification-gauntlet))
|
|
165
165
|
- `ship-gate-hook.sh` — a `PreToolUse` gate on `Bash` that refuses `git commit` and `git push` without a receipt from `ship-gate.sh`
|
|
166
166
|
|
|
167
|
+
One more hook stays out of the way entirely:
|
|
168
|
+
|
|
169
|
+
- `version-check.sh` — a `SessionStart` hook that mentions a newer catalog at most once a day. It never touches the network in the foreground: it reads a cache the previous session refreshed in the background, so it costs a file read. Silent when there is no cache, no network, or nothing new. `CLAUDE_SKILLS_NO_VERSION_CHECK=1` turns it off.
|
|
170
|
+
|
|
167
171
|
<details>
|
|
168
172
|
<summary>How the gate works</summary>
|
|
169
173
|
|
|
@@ -345,7 +349,8 @@ scripts/ validate-skills.mjs — checks skill length caps and reference in
|
|
|
345
349
|
gauntlet.sh — the Stop-hook verification gates, embedded by the CLI
|
|
346
350
|
ship-gate.sh — /ship's file-length and mutation checks, behind an exit code
|
|
347
351
|
ship-gate-hook.sh — refuses git commit/push without a ship-gate receipt
|
|
348
|
-
|
|
352
|
+
version-check.sh — SessionStart nudge when a newer catalog is published
|
|
353
|
+
gauntlet-selftest.sh — behavioural tests for the hooks (runs on pre-push)
|
|
349
354
|
gauntlet-survey.sh — what the hook would do in every repo under a directory
|
|
350
355
|
preflight.sh — everything that must pass before a release
|
|
351
356
|
check-freshness.mjs — compares each skill's `tracks:` versions to the registries
|
package/lib/setup-hook.mjs
CHANGED
|
@@ -565,6 +565,8 @@ const SHIP_GATE_SCRIPT = `#!/usr/bin/env bash
|
|
|
565
565
|
# bash .claude/hooks/ship-gate.sh [base-ref]
|
|
566
566
|
# bash .claude/hooks/ship-gate.sh --key print the receipt key and stop
|
|
567
567
|
# bash .claude/hooks/ship-gate.sh --force write a FORCED receipt, run nothing
|
|
568
|
+
# bash .claude/hooks/ship-gate.sh --baseline record today's mutmut survivors as
|
|
569
|
+
# accepted debt, write no receipt
|
|
568
570
|
#
|
|
569
571
|
# On completion it writes a RECEIPT at /tmp/claude-shipgate-<key>, and the
|
|
570
572
|
# PreToolUse hook refuses \`git commit\` / \`git push\` without a matching one. The
|
|
@@ -587,6 +589,8 @@ const SHIP_GATE_SCRIPT = `#!/usr/bin/env bash
|
|
|
587
589
|
# GAUNTLET_SOURCE_EXT=... which extensions count as source. Deliberately NOT
|
|
588
590
|
# GAUNTLET_CODE_EXT: the Stop hook's key answers a
|
|
589
591
|
# different question, and a docs repo sets it to .md
|
|
592
|
+
# GAUNTLET_IGNORE_EXT=... extensions that need no gate at all. An extension in
|
|
593
|
+
# neither list is reported UNPROVEN rather than passed
|
|
590
594
|
# GAUNTLET_MUTATE="cmd" one explicit mutation command for the whole repo,
|
|
591
595
|
# replacing per-project detection. $MUTATE_FLAGS holds
|
|
592
596
|
# the --mutate flags, $FILES the changed files.
|
|
@@ -594,15 +598,33 @@ const SHIP_GATE_SCRIPT = `#!/usr/bin/env bash
|
|
|
594
598
|
set -uo pipefail
|
|
595
599
|
|
|
596
600
|
MODE=""
|
|
597
|
-
case "\${1:-}" in
|
|
601
|
+
case "\${1:-}" in
|
|
602
|
+
--key) MODE=key; shift ;;
|
|
603
|
+
--force) MODE=force; shift ;;
|
|
604
|
+
--baseline) MODE=baseline; shift ;;
|
|
605
|
+
esac
|
|
598
606
|
|
|
599
607
|
PROJECT_DIR="\${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
600
608
|
cd "$PROJECT_DIR" 2>/dev/null || { echo "ship-gate: cannot read $PROJECT_DIR"; exit 1; }
|
|
601
609
|
git rev-parse --git-dir >/dev/null 2>&1 || { echo "ship-gate: not a git repo"; exit 1; }
|
|
602
610
|
|
|
611
|
+
# \`git diff --name-only\` prints paths from the repo ROOT, but CLAUDE_PROJECT_DIR
|
|
612
|
+
# can point at a subdirectory of it. Testing those paths from the subdirectory
|
|
613
|
+
# found no files at all, so the gate said "nothing to check" and wrote a PASS
|
|
614
|
+
# receipt over a diff it never looked at. Work from the root and the paths git
|
|
615
|
+
# prints are the paths this script tests. The receipt key moves with it, and the
|
|
616
|
+
# PreToolUse hook asks this script for the key rather than computing its own, so
|
|
617
|
+
# the two cannot disagree.
|
|
618
|
+
ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
619
|
+
if [ -n "$ROOT" ]; then
|
|
620
|
+
cd "$ROOT" || { echo "ship-gate: cannot read $ROOT"; exit 1; }
|
|
621
|
+
PROJECT_DIR="$ROOT"
|
|
622
|
+
fi
|
|
623
|
+
|
|
603
624
|
GAUNTLET_MAX_LINES=200
|
|
604
625
|
GAUNTLET_MUTATE=""
|
|
605
|
-
GAUNTLET_SOURCE_EXT="ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|c|h|cpp|hpp|cs|swift"
|
|
626
|
+
GAUNTLET_SOURCE_EXT="ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|c|h|cpp|hpp|cs|swift|gd"
|
|
627
|
+
GAUNTLET_IGNORE_EXT="md|mdx|txt|rst|adoc|json|ya?ml|toml|lock|cfg|ini|env|csv|tsv|sql|html|css|scss|svg|png|jpg|jpeg|gif|webp|ico|pdf|woff2?|ttf|otf|mp3|mp4|wav|zip|gz|tres|tscn|import|godot"
|
|
606
628
|
[ -n "\${HOME:-}" ] && [ -f "$HOME/.claude/gauntlet.conf" ] && . "$HOME/.claude/gauntlet.conf"
|
|
607
629
|
[ -f ".claude/gauntlet.conf" ] && . ".claude/gauntlet.conf"
|
|
608
630
|
|
|
@@ -645,6 +667,24 @@ FILES=$(printf '%s\\n' "$CHANGED" | grep -E "\\.($GAUNTLET_SOURCE_EXT)$" \\
|
|
|
645
667
|
done)
|
|
646
668
|
|
|
647
669
|
if [ -z "$FILES" ]; then
|
|
670
|
+
# "I recognised nothing" is not "there is nothing", and the gate used to report
|
|
671
|
+
# both as a PASS. A Godot repo changed only .gd files, which no extension in
|
|
672
|
+
# the list named, so zero source files were found and a PASS receipt was
|
|
673
|
+
# written over work nothing had checked. An extension this gate cannot place is
|
|
674
|
+
# UNPROVEN: it does not block, but it never claims a pass either.
|
|
675
|
+
UNKNOWN=$(printf '%s\\n' "$CHANGED" | grep -E '\\.[A-Za-z0-9]+$' \\
|
|
676
|
+
| grep -viE "\\.($GAUNTLET_SOURCE_EXT)$" \\
|
|
677
|
+
| grep -viE "\\.($GAUNTLET_IGNORE_EXT)$" \\
|
|
678
|
+
| sed 's/.*\\.//' | sort -u | tr '\\n' ' ')
|
|
679
|
+
if [ -n "$UNKNOWN" ]; then
|
|
680
|
+
echo "ship-gate: UNPROVEN — nothing here was checked. These changed files are"
|
|
681
|
+
echo " in a language this gate does not know: $UNKNOWN"
|
|
682
|
+
echo " If they are source, add the extension in .claude/gauntlet.conf:"
|
|
683
|
+
echo " GAUNTLET_SOURCE_EXT=\\"\\$GAUNTLET_SOURCE_EXT|\${UNKNOWN%% *}\\""
|
|
684
|
+
echo " If they are not, add it to GAUNTLET_IGNORE_EXT the same way."
|
|
685
|
+
printf 'UNPROVEN %s unknown-extensions\\n' "$(date -u +%FT%TZ)" > "$RECEIPT"
|
|
686
|
+
exit 2
|
|
687
|
+
fi
|
|
648
688
|
echo "ship-gate: no changed code files — nothing to check"
|
|
649
689
|
printf 'PASS %s no-code-changes\\n' "$(date -u +%FT%TZ)" > "$RECEIPT"
|
|
650
690
|
exit 0
|
|
@@ -788,16 +828,59 @@ for owner in $OWNERS; do
|
|
|
788
828
|
esac
|
|
789
829
|
SURVIVED=$(printf '%s\\n' "$OUT" | grep -E "$PATTERN" \\
|
|
790
830
|
| grep -vE '^[[:space:]]*[#│|+-]|# *surviv' | head -20)
|
|
831
|
+
FIXED=0
|
|
832
|
+
NOTE=""
|
|
833
|
+
|
|
834
|
+
# Stryker takes --mutate per changed hunk, so it already answers "did YOUR
|
|
835
|
+
# change get tested". mutmut cannot be asked that — 3.x dropped 1.x's
|
|
836
|
+
# --use-patch-file and never replaced it — so it mutates all of source_paths
|
|
837
|
+
# and reports the repo's entire backlog. Held to the same bar, the Python half
|
|
838
|
+
# answers "is this repo perfect" instead, which it never is, so it could never
|
|
839
|
+
# go green. The baseline supplies the missing half: survivors recorded once
|
|
840
|
+
# are accepted debt, and only a name that is not in it fails the gate.
|
|
841
|
+
#
|
|
842
|
+
# A mutant name carries an index within its own function, not a line number,
|
|
843
|
+
# so editing elsewhere in the file does not rename it — and editing the
|
|
844
|
+
# function itself DOES, which correctly re-charges its survivors to that edit.
|
|
845
|
+
if [ "$TOOL" = mutmut ]; then
|
|
846
|
+
BL="$base.mutmut-baseline"
|
|
847
|
+
NOWF=$(mktemp)
|
|
848
|
+
printf '%s\\n' "$OUT" \\
|
|
849
|
+
| sed -n 's/^[[:space:]]*\\([^[:space:]]*\\): survived$/\\1/p' | sort -u > "$NOWF"
|
|
850
|
+
if [ "$MODE" = baseline ]; then
|
|
851
|
+
cp "$NOWF" "$BL"
|
|
852
|
+
NOTE=" $label mutmut — baseline set: $(wc -l < "$BL") survivor(s) accepted. Commit $BL."
|
|
853
|
+
SURVIVED=""
|
|
854
|
+
elif [ -f "$BL" ]; then
|
|
855
|
+
# -Fxv against an empty baseline reports every survivor, which is right:
|
|
856
|
+
# an empty baseline means nothing has been accepted.
|
|
857
|
+
SURVIVED=$(grep -Fxv -f "$BL" "$NOWF" | head -20)
|
|
858
|
+
FIXED=$(grep -Fxvc -f "$NOWF" "$BL")
|
|
859
|
+
else
|
|
860
|
+
cp "$NOWF" "$BL"
|
|
861
|
+
NOTE=" $label mutmut — no baseline, so nothing could be compared. Recorded
|
|
862
|
+
$(wc -l < "$BL") existing survivor(s) in $BL — commit it, then run the
|
|
863
|
+
gate again. From here only NEW survivors fail; --baseline accepts more."
|
|
864
|
+
SURVIVED=""
|
|
865
|
+
[ "$STATUS" = 0 ] && STATUS=2
|
|
866
|
+
fi
|
|
867
|
+
rm -f "$NOWF"
|
|
868
|
+
fi
|
|
869
|
+
|
|
791
870
|
if [ -n "$SURVIVED" ]; then
|
|
792
871
|
echo " $label $TOOL — surviving mutants; these lines can break and no test notices:"
|
|
793
872
|
printf '%s\\n' "$SURVIVED" | sed 's/^/ /'
|
|
873
|
+
[ "$FIXED" -gt 0 ] && echo " ($FIXED baselined survivor(s) now killed — --baseline banks them)"
|
|
794
874
|
STATUS=1
|
|
795
875
|
elif [ $RC -ne 0 ]; then
|
|
796
876
|
echo " $label $TOOL — the run failed:"
|
|
797
877
|
printf '%s\\n' "$OUT" | tail -12 | sed 's/^/ /'
|
|
798
878
|
[ "$STATUS" = 0 ] && STATUS=2
|
|
879
|
+
elif [ -n "$NOTE" ]; then
|
|
880
|
+
printf '%s\\n' "$NOTE"
|
|
799
881
|
else
|
|
800
882
|
echo " $label $TOOL — ok, no surviving mutants in the changed lines"
|
|
883
|
+
[ "$FIXED" -gt 0 ] && echo " ($FIXED baselined survivor(s) now killed — --baseline banks them)"
|
|
801
884
|
fi
|
|
802
885
|
done
|
|
803
886
|
|
|
@@ -808,6 +891,12 @@ if [ -n "$MISSING" ]; then
|
|
|
808
891
|
[ "$STATUS" = 0 ] && STATUS=2
|
|
809
892
|
fi
|
|
810
893
|
|
|
894
|
+
if [ "$MODE" = baseline ]; then
|
|
895
|
+
echo
|
|
896
|
+
echo "ship-gate: baselines updated. No receipt written — run the gate for real."
|
|
897
|
+
exit 0
|
|
898
|
+
fi
|
|
899
|
+
|
|
811
900
|
# The receipt is written by this script and nothing else. A hand-rolled check
|
|
812
901
|
# produces no receipt, which is the whole point: substituting a weaker check is
|
|
813
902
|
# a choice that can be argued for, but it cannot be passed off as this one.
|
|
@@ -899,12 +988,105 @@ printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision"
|
|
|
899
988
|
exit 0
|
|
900
989
|
`;
|
|
901
990
|
|
|
991
|
+
// SessionStart: a once-a-day line when a newer catalog has been published.
|
|
992
|
+
// Canonical source: scripts/version-check.sh.
|
|
993
|
+
const VERSION_CHECK_SCRIPT = `#!/usr/bin/env bash
|
|
994
|
+
# version-check.sh — SessionStart. Says once a day when a newer catalog exists.
|
|
995
|
+
#
|
|
996
|
+
# SessionStart stdout is added to the model's context, so this does not draw a
|
|
997
|
+
# banner: it tells the assistant, and the assistant mentions it. That is the
|
|
998
|
+
# whole reach of this hook — it never installs anything and never asks a
|
|
999
|
+
# question the user has to answer before working.
|
|
1000
|
+
#
|
|
1001
|
+
# The rule that keeps it invisible: NEVER touch the network in the foreground.
|
|
1002
|
+
# A session start that waits on npm is a session start that hangs on a bad
|
|
1003
|
+
# connection, and a version nudge is not worth one second of that. So it reads
|
|
1004
|
+
# a cache written by the PREVIOUS session's background refresh, and kicks off
|
|
1005
|
+
# the next refresh detached. The first session after an install therefore says
|
|
1006
|
+
# nothing, and after that the answer is at worst a day old — which is the right
|
|
1007
|
+
# resolution for "there is a new version" anyway.
|
|
1008
|
+
#
|
|
1009
|
+
# Silent when: no manifest, no cache yet, offline, already current, or the user
|
|
1010
|
+
# has already been told today.
|
|
1011
|
+
#
|
|
1012
|
+
# CLAUDE_SKILLS_NO_VERSION_CHECK=1 turns it off entirely
|
|
1013
|
+
|
|
1014
|
+
set -uo pipefail
|
|
1015
|
+
|
|
1016
|
+
[ -n "\${CLAUDE_SKILLS_NO_VERSION_CHECK:-}" ] && exit 0
|
|
1017
|
+
|
|
1018
|
+
PKG="@spardutti/claude-skills"
|
|
1019
|
+
PROJECT_DIR="\${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
1020
|
+
MANIFEST="$PROJECT_DIR/.claude/.claude-skills.json"
|
|
1021
|
+
|
|
1022
|
+
# No manifest means the CLI never installed here, so there is nothing to update.
|
|
1023
|
+
[ -f "$MANIFEST" ] || exit 0
|
|
1024
|
+
|
|
1025
|
+
INSTALLED=$(sed -n 's/.*"catalogVersion"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' "$MANIFEST" | head -1)
|
|
1026
|
+
[ -n "$INSTALLED" ] || exit 0
|
|
1027
|
+
|
|
1028
|
+
# One cache for the whole machine, not one per project: the answer is the same
|
|
1029
|
+
# everywhere, and a developer with ten repos should still cost one request.
|
|
1030
|
+
CACHE_DIR="\${HOME:-/tmp}/.claude"
|
|
1031
|
+
CACHE="$CACHE_DIR/.claude-skills-version"
|
|
1032
|
+
STAMP="$CACHE_DIR/.claude-skills-version-told"
|
|
1033
|
+
mkdir -p "$CACHE_DIR" 2>/dev/null
|
|
1034
|
+
|
|
1035
|
+
NOW=$(date +%s 2>/dev/null || echo 0)
|
|
1036
|
+
DAY=86400
|
|
1037
|
+
|
|
1038
|
+
# Refresh in the background, fully detached. Nothing downstream waits on it: all
|
|
1039
|
+
# three streams are closed, or the session start blocks until curl gives up.
|
|
1040
|
+
refresh() {
|
|
1041
|
+
AGE=$DAY
|
|
1042
|
+
if [ -f "$CACHE" ]; then
|
|
1043
|
+
MTIME=$(date -r "$CACHE" +%s 2>/dev/null || echo 0)
|
|
1044
|
+
AGE=$((NOW - MTIME))
|
|
1045
|
+
fi
|
|
1046
|
+
[ "$AGE" -lt "$DAY" ] && return
|
|
1047
|
+
command -v curl >/dev/null 2>&1 || return
|
|
1048
|
+
( curl -fsS --max-time 5 "https://registry.npmjs.org/$PKG/latest" \\
|
|
1049
|
+
| sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' \\
|
|
1050
|
+
| head -1 > "$CACHE.tmp" 2>/dev/null
|
|
1051
|
+
# Only replace the cache with something that parsed. A truncated reply that
|
|
1052
|
+
# overwrote it would nudge toward a version that does not exist.
|
|
1053
|
+
if [ -s "$CACHE.tmp" ]; then mv "$CACHE.tmp" "$CACHE"; else rm -f "$CACHE.tmp"; fi
|
|
1054
|
+
) </dev/null >/dev/null 2>&1 &
|
|
1055
|
+
}
|
|
1056
|
+
refresh
|
|
1057
|
+
|
|
1058
|
+
[ -f "$CACHE" ] || exit 0
|
|
1059
|
+
LATEST=$(head -1 "$CACHE" | tr -d '[:space:]')
|
|
1060
|
+
[ -n "$LATEST" ] || exit 0
|
|
1061
|
+
[ "$LATEST" = "$INSTALLED" ] && exit 0
|
|
1062
|
+
|
|
1063
|
+
# sort -V puts the newer one last. Equal versions were already handled, so the
|
|
1064
|
+
# only case left to reject is a cache that is BEHIND the install — which happens
|
|
1065
|
+
# normally right after publishing from the machine that publishes.
|
|
1066
|
+
NEWER=$(printf '%s\\n%s\\n' "$INSTALLED" "$LATEST" | sort -V | tail -1)
|
|
1067
|
+
[ "$NEWER" = "$LATEST" ] || exit 0
|
|
1068
|
+
|
|
1069
|
+
# Once a day, however many sessions that day holds.
|
|
1070
|
+
if [ -f "$STAMP" ]; then
|
|
1071
|
+
TOLD=$(date -r "$STAMP" +%s 2>/dev/null || echo 0)
|
|
1072
|
+
[ $((NOW - TOLD)) -lt "$DAY" ] && exit 0
|
|
1073
|
+
fi
|
|
1074
|
+
: > "$STAMP"
|
|
1075
|
+
|
|
1076
|
+
echo "A newer version of the $PKG catalog is available: $INSTALLED installed, $LATEST published."
|
|
1077
|
+
echo "Mention this to the user ONCE, in a single line, and only when the current task is done."
|
|
1078
|
+
echo "Do not interrupt what they are doing, and do not run the installer unless they ask."
|
|
1079
|
+
echo "To update, they run: npx $PKG"
|
|
1080
|
+
exit 0
|
|
1081
|
+
`;
|
|
1082
|
+
|
|
902
1083
|
const GATE_FILENAME = "skill-gate.sh";
|
|
903
1084
|
const AUTO_MARK_FILENAME = "skill-gate-automark.sh";
|
|
904
1085
|
const APPLICATION_GATE_FILENAME = "skill-application-gate.sh";
|
|
905
1086
|
const GAUNTLET_FILENAME = "gauntlet.sh";
|
|
906
1087
|
const SHIP_GATE_FILENAME = "ship-gate.sh";
|
|
907
1088
|
const SHIP_GATE_HOOK_FILENAME = "ship-gate-hook.sh";
|
|
1089
|
+
const VERSION_CHECK_FILENAME = "version-check.sh";
|
|
908
1090
|
const LEGACY_EVAL_FILENAME = "skill-forced-eval-hook.sh";
|
|
909
1091
|
const LEGACY_AUDIT_RUNNER_FILENAME = "skill-audit-runner.sh";
|
|
910
1092
|
|
|
@@ -969,6 +1151,7 @@ export async function setupHook(targetDir = process.cwd()) {
|
|
|
969
1151
|
const gauntletPath = join(hooksDir, GAUNTLET_FILENAME);
|
|
970
1152
|
const shipGatePath = join(hooksDir, SHIP_GATE_FILENAME);
|
|
971
1153
|
const shipGateHookPath = join(hooksDir, SHIP_GATE_HOOK_FILENAME);
|
|
1154
|
+
const versionCheckPath = join(hooksDir, VERSION_CHECK_FILENAME);
|
|
972
1155
|
const settingsPath = join(resolved, ".claude", "settings.json");
|
|
973
1156
|
|
|
974
1157
|
await mkdir(hooksDir, { recursive: true });
|
|
@@ -984,6 +1167,8 @@ export async function setupHook(targetDir = process.cwd()) {
|
|
|
984
1167
|
await chmod(shipGatePath, 0o755);
|
|
985
1168
|
await writeFile(shipGateHookPath, SHIP_GATE_HOOK_SCRIPT, { mode: 0o755 });
|
|
986
1169
|
await chmod(shipGateHookPath, 0o755);
|
|
1170
|
+
await writeFile(versionCheckPath, VERSION_CHECK_SCRIPT, { mode: 0o755 });
|
|
1171
|
+
await chmod(versionCheckPath, 0o755);
|
|
987
1172
|
|
|
988
1173
|
// Remove the legacy audit-runner hook file from prior installs (2.3.x).
|
|
989
1174
|
try {
|
|
@@ -1059,6 +1244,17 @@ export async function setupHook(targetDir = process.cwd()) {
|
|
|
1059
1244
|
settings.hooks.PostToolUse = [autoMarkEntry];
|
|
1060
1245
|
}
|
|
1061
1246
|
|
|
1247
|
+
// Register SessionStart version check. No matcher: startup and resume both
|
|
1248
|
+
// want it, and the script's own daily stamp keeps a burst of resumes quiet.
|
|
1249
|
+
const versionCheckCommand = `$CLAUDE_PROJECT_DIR/.claude/hooks/${VERSION_CHECK_FILENAME}`;
|
|
1250
|
+
const versionCheckEntry = { hooks: [{ type: "command", command: versionCheckCommand }] };
|
|
1251
|
+
|
|
1252
|
+
if (Array.isArray(settings.hooks.SessionStart)) {
|
|
1253
|
+
upsert(settings.hooks.SessionStart, VERSION_CHECK_FILENAME, versionCheckEntry);
|
|
1254
|
+
} else {
|
|
1255
|
+
settings.hooks.SessionStart = [versionCheckEntry];
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1062
1258
|
// Register Stop gauntlet hook.
|
|
1063
1259
|
const gauntletCommand = `$CLAUDE_PROJECT_DIR/.claude/hooks/${GAUNTLET_FILENAME}`;
|
|
1064
1260
|
const gauntletEntry = { hooks: [{ type: "command", command: gauntletCommand }] };
|