@spardutti/claude-skills 2.12.0 → 2.13.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 CHANGED
@@ -142,8 +142,20 @@ After installing skills, the CLI offers to set up a hook that **guarantees** Cla
142
142
  It installs four hooks and appends a rule to your `CLAUDE.md`. Three guard the
143
143
  **front** — nothing is written until your skills are considered:
144
144
 
145
- - `skill-gate.sh` — a `PreToolUse` gate on `Write|Edit|MultiEdit`
145
+ - `skill-gate.sh` — a `PreToolUse` gate on `Write|Edit|MultiEdit|Bash`
146
146
  - `skill-application-gate.sh` — a second `PreToolUse` gate requiring each loaded skill to be applied, not just read
147
+
148
+ Both cover **Bash**, not only the file-writing tools. A session that edits through
149
+ `python3 - <<'PY'` in Bash walked past the old matcher entirely — twelve source files, not
150
+ one prompt — and some harnesses actively tell the model to prefer Bash for edits. Only
151
+ commands that can write are gated (a redirect, `tee`, `sed -i`, `cp`/`mv`, or an
152
+ interpreter given inline code or a heredoc); `git status` and a redirect to `/dev/null`
153
+ pass untouched, and the command that clears the gate is never gated against itself.
154
+
155
+ Neither gate fires on **prose** — `.md`, `.mdx`, `.txt`, `.rst`, `.adoc`, images. Skills
156
+ are about code, and a repo full of `PLAN_*.md` otherwise hits the gate on every write.
157
+ Config files (`.json`, `.yml`, `.toml`) stay gated, because skills do have rules about
158
+ `tsconfig`, `package.json`, and Compose.
147
159
  - `skill-gate-automark.sh` — a `PostToolUse` hook on `Skill` that clears the gate
148
160
 
149
161
  The last two guard the **back** — nothing finishes or ships until the code passes:
@@ -164,9 +176,9 @@ It registers in `.claude/settings.json`:
164
176
  {
165
177
  "hooks": {
166
178
  "PreToolUse": [
167
- { "matcher": "Write|Edit|MultiEdit", "hooks": [
179
+ { "matcher": "Write|Edit|MultiEdit|Bash", "hooks": [
168
180
  { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/skill-gate.sh" } ] },
169
- { "matcher": "Write|Edit|MultiEdit", "hooks": [
181
+ { "matcher": "Write|Edit|MultiEdit|Bash", "hooks": [
170
182
  { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/skill-application-gate.sh" } ] }
171
183
  ],
172
184
  "PostToolUse": [
@@ -34,6 +34,56 @@ if [ -f "$MARKER" ]; then
34
34
  exit 0
35
35
  fi
36
36
 
37
+ # Write|Edit|MultiEdit is not the only way to change a file. A session that edits
38
+ # through \`python3 - <<'PY'\` in Bash walked past this gate entirely — twelve
39
+ # source files, not one prompt — and some harnesses actively tell the model to
40
+ # prefer Bash for edits. So Bash is gated too, but only for commands that can
41
+ # write, and only until the gate is cleared once for the session.
42
+
43
+ # Skills are about code. Blocking a plan document to ask how the React rules
44
+ # apply is pure noise, and a repo full of PLAN_*.md hits it on every write.
45
+ # Config files stay gated — skills do have rules about tsconfig and compose.
46
+ PROSE_EXT='md|mdx|txt|rst|adoc|png|jpg|jpeg|gif|svg|webp|ico'
47
+ CODE_EXT='ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|c|h|cpp|hpp|cs|swift|sql|sh|json|ya?ml|toml'
48
+
49
+ TOOL=$(printf '%s' "$INPUT" | grep -o '"tool_name":"[^"]*"' | head -1 | sed 's/.*:"//; s/"$//')
50
+
51
+ if [ "$TOOL" != "Bash" ]; then
52
+ # The structured tools name their target outright.
53
+ TARGET=$(printf '%s' "$INPUT" | grep -o '"file_path":[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:[[:space:]]*"//; s/"$//')
54
+ case "$TARGET" in
55
+ *.*) printf '%s' "$TARGET" | grep -qiE "\.($PROSE_EXT)$" && exit 0 ;;
56
+ esac
57
+ fi
58
+ if [ "$TOOL" = "Bash" ]; then
59
+ CMD=$(printf '%s' "$INPUT" | grep -o '"command":[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:[[:space:]]*"//; s/"$//')
60
+
61
+ # Never gate the command that clears the gate, or this deadlocks.
62
+ case "$CMD" in
63
+ *claude-skill-gate-*|*claude-skill-acked-*|*claude-skill-loaded-*) exit 0 ;;
64
+ esac
65
+
66
+ # /dev/null redirects are not file writes; drop them before looking for one.
67
+
68
+ # A command that names a prose file and no code file is writing prose.
69
+ if printf '%s' "$CMD" | grep -qiE "\.($PROSE_EXT)([^A-Za-z0-9]|$)" \
70
+ && ! printf '%s' "$CMD" | grep -qiE "\.($CODE_EXT)([^A-Za-z0-9]|$)"; then
71
+ exit 0
72
+ fi
73
+ STRIPPED=$(printf '%s' "$CMD" | sed 's![12]*>>*[[:space:]]*/dev/null!!g')
74
+ WRITES=""
75
+ case "$STRIPPED" in
76
+ *">"*|*"tee "*|*"sed -i"*|*"cp "*|*"mv "*|*"truncate "*|*"dd "*) WRITES=1 ;;
77
+ esac
78
+ # An interpreter given inline code or a heredoc can write anything, and the
79
+ # shell shows no redirect at all — this is the shape that got past the gate.
80
+ case "$CMD" in
81
+ *"<<"*|*python*" -c"*|*node*" -e"*|*perl*" -e"*|*ruby*" -e"*) WRITES=1 ;;
82
+ esac
83
+ [ -z "$WRITES" ] && exit 0
84
+ fi
85
+
86
+
37
87
  cat <<EOF
38
88
  {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"BLOCKED: skill evaluation required before file edits in this session.\\n\\nStep 1 — evaluate every available skill as ACTIVATE or SKIP with a one-line reason.\\n\\nStep 2 — you MUST take EXACTLY ONE of these tool actions to clear the gate. Listing skills in text is NOT enough; retrying the edit without doing one of these will be denied again:\\n (a) If any skill is ACTIVATE → call Skill(name) for it. This auto-clears the gate.\\n (b) If ALL skills are SKIP → run this Bash tool call: touch /tmp/claude-skill-gate-$SESSION_ID\\n\\nStep 3 — only after Step 2 completes, retry the file edit."}}
39
89
  EOF
@@ -90,6 +140,47 @@ if [ ! -f "/tmp/claude-skill-gate-$SESSION_ID" ]; then
90
140
  exit 0
91
141
  fi
92
142
 
143
+ # Same reason as the loading gate: Bash can write files, and a heredoc into an
144
+ # interpreter shows no redirect at all.
145
+
146
+ # Skills are about code. Blocking a plan document to ask how the React rules
147
+ # apply is pure noise, and a repo full of PLAN_*.md hits it on every write.
148
+ # Config files stay gated — skills do have rules about tsconfig and compose.
149
+ PROSE_EXT='md|mdx|txt|rst|adoc|png|jpg|jpeg|gif|svg|webp|ico'
150
+ CODE_EXT='ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|c|h|cpp|hpp|cs|swift|sql|sh|json|ya?ml|toml'
151
+
152
+ TOOL=$(printf '%s' "$INPUT" | grep -o '"tool_name":"[^"]*"' | head -1 | sed 's/.*:"//; s/"$//')
153
+
154
+ if [ "$TOOL" != "Bash" ]; then
155
+ # The structured tools name their target outright.
156
+ TARGET=$(printf '%s' "$INPUT" | grep -o '"file_path":[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:[[:space:]]*"//; s/"$//')
157
+ case "$TARGET" in
158
+ *.*) printf '%s' "$TARGET" | grep -qiE "\.($PROSE_EXT)$" && exit 0 ;;
159
+ esac
160
+ fi
161
+ if [ "$TOOL" = "Bash" ]; then
162
+ CMD=$(printf '%s' "$INPUT" | grep -o '"command":[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:[[:space:]]*"//; s/"$//')
163
+ case "$CMD" in
164
+ *claude-skill-gate-*|*claude-skill-acked-*|*claude-skill-loaded-*) exit 0 ;;
165
+ esac
166
+
167
+ # A command that names a prose file and no code file is writing prose.
168
+ if printf '%s' "$CMD" | grep -qiE "\.($PROSE_EXT)([^A-Za-z0-9]|$)" \
169
+ && ! printf '%s' "$CMD" | grep -qiE "\.($CODE_EXT)([^A-Za-z0-9]|$)"; then
170
+ exit 0
171
+ fi
172
+ STRIPPED=$(printf '%s' "$CMD" | sed 's![12]*>>*[[:space:]]*/dev/null!!g')
173
+ WRITES=""
174
+ case "$STRIPPED" in
175
+ *">"*|*"tee "*|*"sed -i"*|*"cp "*|*"mv "*|*"truncate "*|*"dd "*) WRITES=1 ;;
176
+ esac
177
+ case "$CMD" in
178
+ *"<<"*|*python*" -c"*|*node*" -e"*|*perl*" -e"*|*ruby*" -e"*) WRITES=1 ;;
179
+ esac
180
+ [ -z "$WRITES" ] && exit 0
181
+ fi
182
+
183
+
93
184
  # Collect every loaded-but-unacked skill so a single ack clears them all.
94
185
  UNACKED=""
95
186
  for marker in /tmp/claude-skill-loaded-$SESSION_ID-*; do
@@ -133,8 +224,11 @@ MSG="BLOCKED: loaded skills not yet applied to your work: $NAMES
133
224
 
134
225
  Before this Write/Edit, you must:
135
226
 
136
- 1. For each skill listed above, state the specific rules that apply to the file you're about to write.
137
- 2. State how your next write respects each rule.
227
+ 1. For each skill listed above, say whether it applies to the file you're about to
228
+ write. If it does not — a Python skill and a TypeScript file, say — write
229
+ "does not apply" and move on. Do not invent a connection.
230
+ 2. For the ones that do apply: state the specific rules that bear on this file, and
231
+ how your next write respects each.
138
232
  3. Then ack all of them in a single Bash tool call:
139
233
  $ACK_CMD
140
234
 
@@ -586,6 +680,17 @@ fi
586
680
  # ---------------------------------------------------------- which project owns
587
681
  # A monorepo holds several projects, each with its own runner and its own
588
682
  # mutation tool. Walk up from each changed file to the nearest manifest.
683
+ # mutmut is installed into the project's environment, not onto PATH. Ask the
684
+ # project how to run its own tools before falling back to a bare binary.
685
+ py_mutmut() {
686
+ d="$1"
687
+ # Returned relative to the PROJECT, because the command runs from inside it.
688
+ if [ -x "$d.venv/bin/mutmut" ]; then printf './.venv/bin/mutmut\\n'; return; fi
689
+ if [ -f "$d""uv.lock" ] && command -v uv >/dev/null 2>&1; then printf 'uv run mutmut\\n'; return; fi
690
+ if [ -f "$d""poetry.lock" ] && command -v poetry >/dev/null 2>&1; then printf 'poetry run mutmut\\n'; return; fi
691
+ command -v mutmut >/dev/null 2>&1 && printf 'mutmut\\n'
692
+ }
693
+
589
694
  owner_of() {
590
695
  d=$(dirname "$1")
591
696
  while :; do
@@ -640,15 +745,22 @@ for owner in $OWNERS; do
640
745
  {\\"testRunner\\":\\"vitest\\",\\"plugins\\":[\\"@stryker-mutator/vitest-runner\\"],\\"coverageAnalysis\\":\\"perTest\\"}
641
746
  "
642
747
  continue
643
- elif command -v mutmut >/dev/null 2>&1; then
748
+ elif [ -n "$(py_mutmut "$base")" ]; then
644
749
  TOOL="mutmut"
645
750
  # mutmut takes its scope from [tool.mutmut] in pyproject, not from a flag:
646
751
  # --paths-to-mutate was 2.x, and 3.x filters by fnmatch globs over mutant
647
752
  # NAMES (app.balance.reserve.*). There is no per-line scoping at all.
648
- CMD="\${RUN}mutmut run"
753
+ #
754
+ # \`mutmut run\` prints 🙁 for a survivor and exits 0 either way — parsing it
755
+ # reports clean with survivors sitting there, which is a false green and
756
+ # worse than reporting nothing. \`mutmut results\` is the readable source:
757
+ # it prints "<mutant name>: survived" per survivor.
758
+ M="$(py_mutmut "$base")"
759
+ # One cd for both: the second command runs in the same shell, already there.
760
+ CMD="\${RUN}$M run >/dev/null 2>&1; $M results"
649
761
  else
650
762
  MISSING="$MISSING $label needs mutmut:
651
- pip install mutmut
763
+ uv add --dev mutmut # or: poetry add --group dev mutmut, pip install mutmut
652
764
  then \${base}pyproject.toml:
653
765
  [tool.mutmut]
654
766
  source_paths = [\\"src/\\"]
@@ -889,7 +1001,7 @@ export async function setupHook(targetDir = process.cwd()) {
889
1001
  // Register PreToolUse gate.
890
1002
  const gateCommand = `$CLAUDE_PROJECT_DIR/.claude/hooks/${GATE_FILENAME}`;
891
1003
  const gateEntry = {
892
- matcher: "Write|Edit|MultiEdit",
1004
+ matcher: "Write|Edit|MultiEdit|Bash",
893
1005
  hooks: [{ type: "command", command: gateCommand }],
894
1006
  };
895
1007
 
@@ -905,7 +1017,7 @@ export async function setupHook(targetDir = process.cwd()) {
905
1017
  // Register PreToolUse application gate (after the loading gate).
906
1018
  const applicationCommand = `$CLAUDE_PROJECT_DIR/.claude/hooks/${APPLICATION_GATE_FILENAME}`;
907
1019
  const applicationEntry = {
908
- matcher: "Write|Edit|MultiEdit",
1020
+ matcher: "Write|Edit|MultiEdit|Bash",
909
1021
  hooks: [{ type: "command", command: applicationCommand }],
910
1022
  };
911
1023
  const applicationAlreadyRegistered = settings.hooks.PreToolUse.some((entry) =>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spardutti/claude-skills",
3
- "version": "2.12.0",
3
+ "version": "2.13.0",
4
4
  "description": "CLI to install Claude Code skills from the claude-skills collection",
5
5
  "type": "module",
6
6
  "bin": {