@spardutti/claude-skills 2.25.3 → 2.26.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.
Files changed (2) hide show
  1. package/lib/setup-hook.mjs +85 -3
  2. package/package.json +1 -1
@@ -30,10 +30,10 @@ if [ -z "$SESSION_ID" ]; then
30
30
  exit 0
31
31
  fi
32
32
 
33
+ # The session marker is checked at the very bottom now, not here. A skill the
34
+ # edited file's own stack demands is not something an already-cleared gate
35
+ # excuses, so the required-skill scan below runs first.
33
36
  MARKER="/tmp/claude-skill-gate-$SESSION_ID"
34
- if [ -f "$MARKER" ]; then
35
- exit 0
36
- fi
37
37
 
38
38
  # Write|Edit|MultiEdit is not the only way to change a file. A session that edits
39
39
  # through \`python3 - <<'PY'\` in Bash walked past this gate entirely — twelve
@@ -49,6 +49,9 @@ CODE_EXT='ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|c|h|cpp|hpp|cs|swift|sql
49
49
 
50
50
  TOOL=$(printf '%s' "$INPUT" | grep -o '"tool_name":"[^"]*"' | head -1 | sed 's/.*:"//; s/"$//')
51
51
 
52
+ # Every file this call would write, collected for the required-skill scan below.
53
+ TARGETS=""
54
+
52
55
  if [ "$TOOL" != "Bash" ]; then
53
56
  # The structured tools name their target outright.
54
57
  TARGET=$(printf '%s' "$INPUT" | grep -o '"file_path":[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:[[:space:]]*"//; s/"$//')
@@ -58,6 +61,7 @@ if [ "$TOOL" != "Bash" ]; then
58
61
  */.claude/settings.json|*/.claude/settings.local.json) exit 0 ;;
59
62
  *.*) printf '%s' "$TARGET" | grep -qiE "\.($PROSE_EXT)$" && exit 0 ;;
60
63
  esac
64
+ TARGETS="$TARGET"
61
65
  fi
62
66
  if [ "$TOOL" = "Bash" ]; then
63
67
  CMD=$(printf '%s' "$INPUT" | grep -o '"command":[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:[[:space:]]*"//; s/"$//')
@@ -86,6 +90,84 @@ if [ "$TOOL" = "Bash" ]; then
86
90
  *"<<"*|*python*" -c"*|*node*" -e"*|*perl*" -e"*|*ruby*" -e"*) WRITES=1 ;;
87
91
  esac
88
92
  [ -z "$WRITES" ] && exit 0
93
+ # A Bash write names its files in the command itself.
94
+ TARGETS=$(printf '%s' "$CMD" | grep -oiE "[A-Za-z0-9_./-]+\.($CODE_EXT)" 2>/dev/null)
95
+ fi
96
+
97
+ # --- mandatory skills -------------------------------------------------------
98
+ # The generic gate below asks the model to rate every skill ACTIVATE or SKIP,
99
+ # and hands it an all-SKIP escape. A session took that escape, touched the
100
+ # marker, and edited React files with none of the React rules loaded. It had
101
+ # followed the hook exactly: the hook let it decide.
102
+ #
103
+ # So a skill that declares BOTH paths: and tracks: stops getting a vote. If the
104
+ # edited file matches its paths AND this project depends on a package it
105
+ # tracks, that skill is mandatory and only Skill(<name>) clears it. Skills
106
+ # without both fields keep the old behaviour, so this tightens nothing that was
107
+ # not deliberately declared.
108
+ MISSING=""
109
+ if [ -n "$TARGETS" ]; then
110
+ MANIFESTS=$(find "$PROJECT_DIR" -maxdepth 4 \\( -name node_modules -o -name .git -o -name .venv -o -name dist \\) -prune -o \\( -name package.json -o -name pyproject.toml -o -name requirements.txt \\) -print 2>/dev/null)
111
+ fi
112
+ if [ -n "$TARGETS" ] && [ -n "$MANIFESTS" ]; then
113
+ for SKILL_FILE in $(find "$PROJECT_DIR" -path '*/.claude/skills/*/SKILL.md' 2>/dev/null); do
114
+ # paths: must be quoted in YAML — a scalar opening with * is an alias.
115
+ UNQUOTE='s/^["'"'"']//; s/["'"'"']$//'
116
+ SPATHS=$(sed -n 's/^paths:[[:space:]]*//p' "$SKILL_FILE" | head -1 | sed "$UNQUOTE")
117
+ STRACKS=$(sed -n 's/^tracks:[[:space:]]*//p' "$SKILL_FILE" | head -1 | sed "$UNQUOTE")
118
+ SNAME=$(sed -n 's/^name:[[:space:]]*//p' "$SKILL_FILE" | head -1 | sed "$UNQUOTE")
119
+ [ -z "$SPATHS" ] && continue
120
+ [ -z "$STRACKS" ] && continue
121
+ [ -z "$SNAME" ] && continue
122
+
123
+ HIT=""
124
+ for G in $(printf '%s' "$SPATHS" | tr ',' ' '); do
125
+ # Claude Code writes these gitignore-style. A case glob has no **, but *
126
+ # already crosses / here, so dropping the **/ prefix is equivalent.
127
+ G=$(printf '%s' "$G" | sed 's!\\*\\*/!!g')
128
+ case "$G" in \\**|/*) ;; *) G="*$G" ;; esac
129
+ for T in $TARGETS; do
130
+ case "$T" in $G) HIT=1 ;; esac
131
+ done
132
+ done
133
+ [ -z "$HIT" ] && continue
134
+
135
+ # A .tsx file is not proof of React — it could be Astro, Solid or Preact.
136
+ # The packages the skill tracks are the proof, so the manifests decide.
137
+ PY=""
138
+ case "$STRACKS" in *pypi*) PY=1 ;; esac
139
+ DEP=""
140
+ for P in $(printf '%s' "$STRACKS" | tr ',' ' '); do
141
+ case "$P" in *@*) ;; *) continue ;; esac
142
+ PKG=$(printf '%s' "$P" | sed 's/@[^@]*$//')
143
+ [ -z "$PKG" ] && continue
144
+ for M in $MANIFESTS; do
145
+ if [ -n "$PY" ]; then
146
+ grep -qiE "(^|[^A-Za-z0-9_.-])$PKG([^A-Za-z0-9_.-]|$)" "$M" 2>/dev/null && DEP=1
147
+ else
148
+ grep -qs "\\"$PKG\\"" "$M" && DEP=1
149
+ fi
150
+ done
151
+ done
152
+ [ -z "$DEP" ] && continue
153
+
154
+ SAFE=$(printf '%s' "$SNAME" | tr -cd 'A-Za-z0-9_-')
155
+ [ -f "/tmp/claude-skill-loaded-$SESSION_ID-$SAFE" ] && continue
156
+ case " $MISSING " in *" $SNAME "*) ;; *) MISSING="$MISSING $SNAME" ;; esac
157
+ done
158
+ fi
159
+
160
+ if [ -n "$MISSING" ]; then
161
+ LIST=$(printf '%s' "$MISSING" | sed 's/^ //; s/ /, /g')
162
+ FIRST=$(printf '%s' "$MISSING" | awk '{print $1}')
163
+ cat <<EOF
164
+ {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"BLOCKED: this file's stack has mandatory skills that are not loaded: $LIST.\\n\\nThere is no SKIP for these. The file matches each skill's declared paths AND this project depends on a package that skill tracks, so they apply as a matter of fact, not judgement. Touching the gate marker will not clear them.\\n\\nCall Skill($FIRST) now — then every other name in the list — and retry the edit."}}
165
+ EOF
166
+ exit 0
167
+ fi
168
+
169
+ if [ -f "$MARKER" ]; then
170
+ exit 0
89
171
  fi
90
172
 
91
173
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spardutti/claude-skills",
3
- "version": "2.25.3",
3
+ "version": "2.26.0",
4
4
  "description": "Guardrails for Claude Code — best-practice skills, planning commands, and delivery gates.",
5
5
  "type": "module",
6
6
  "repository": {