@spardutti/claude-skills 2.8.1 → 2.9.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
@@ -47,7 +47,7 @@ The CLI will:
47
47
  2. Let you pick skills to install → `.claude/skills/`
48
48
  3. Let you pick commands to install → `.claude/commands/`
49
49
  4. Auto-install any subagents the selected commands declare → `.claude/agents/`
50
- 5. Optionally set up the **skill-evaluation hook** (recommended — see [How It Works](#how-it-works))
50
+ 5. Optionally set up the **hooks** — skill evaluation before edits, verification after (recommended — see [How It Works](#how-it-works))
51
51
 
52
52
  ## Skill Catalog
53
53
 
@@ -132,11 +132,17 @@ npx @spardutti/claude-skills --sync
132
132
 
133
133
  After installing skills, the CLI offers to set up a hook that **guarantees** Claude evaluates your skills before writing code — instead of a soft reminder it can ignore.
134
134
 
135
- It installs two hooks and appends a rule to your `CLAUDE.md`:
135
+ It installs four hooks and appends a rule to your `CLAUDE.md`. Three guard the
136
+ **front** — nothing is written until your skills are considered:
136
137
 
137
138
  - `skill-gate.sh` — a `PreToolUse` gate on `Write|Edit|MultiEdit`
139
+ - `skill-application-gate.sh` — a second `PreToolUse` gate requiring each loaded skill to be applied, not just read
138
140
  - `skill-gate-automark.sh` — a `PostToolUse` hook on `Skill` that clears the gate
139
141
 
142
+ The fourth guards the **back** — nothing finishes until the code passes:
143
+
144
+ - `gauntlet.sh` — a `Stop` hook running the fast gates on your changed files (see [Automatic Verification](#automatic-verification-gauntlet))
145
+
140
146
  <details>
141
147
  <summary>How the gate works</summary>
142
148
 
@@ -151,7 +157,9 @@ It registers in `.claude/settings.json`:
151
157
  "hooks": {
152
158
  "PreToolUse": [
153
159
  { "matcher": "Write|Edit|MultiEdit", "hooks": [
154
- { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/skill-gate.sh" } ] }
160
+ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/skill-gate.sh" } ] },
161
+ { "matcher": "Write|Edit|MultiEdit", "hooks": [
162
+ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/skill-application-gate.sh" } ] }
155
163
  ],
156
164
  "PostToolUse": [
157
165
  { "matcher": "Skill", "hooks": [
@@ -163,6 +171,70 @@ It registers in `.claude/settings.json`:
163
171
 
164
172
  </details>
165
173
 
174
+ ### Automatic Verification (Gauntlet)
175
+
176
+ The same install adds `gauntlet.sh` — a `Stop` hook that runs the cheap, deterministic
177
+ gates on your changed files every time Claude finishes a turn. Green is silent; red
178
+ blocks the turn and hands the failure back to Claude to fix before you ever see it.
179
+
180
+ Only fast gates live here (target: under ~30s). Mutation testing and deep review stay
181
+ manual — they belong to `/test-review` and `/deep-review`, not to every turn.
182
+
183
+ At install time the CLI reports what it found, so a silent no-op is never a surprise:
184
+
185
+ ```text
186
+ Gauntlet detected tsc + vitest — it will run those on changed files.
187
+ ```
188
+
189
+ If no test runner is detected it says so, and offers to write a `.claude/gauntlet.conf`
190
+ pointing at your test command. Detection covers vitest, jest, pytest, `tsc`, and `mypy`;
191
+ everything else needs that one config line.
192
+
193
+ <details>
194
+ <summary>How the gauntlet hook works</summary>
195
+
196
+ It runs a skip ladder first, cheapest check first, and quits at the first "no":
197
+
198
+ 1. no changed files (planning turns, questions)
199
+ 2. no changed **code** files (docs/config only)
200
+ 3. this exact diff already passed — the hash of a green run is cached per session
201
+ 4. no test runner detected in the repo
202
+
203
+ Rule 3 is what makes repeated `/ship` runs free: the gates run once per distinct diff.
204
+
205
+ The stack is auto-detected (vitest / jest / pytest, `tsc` / `mypy`). Override anything in
206
+ a config file — two are read, in this order, and the project's wins key by key:
207
+
208
+ ```text
209
+ ~/.claude/gauntlet.conf your defaults for every project
210
+ <project>/.claude/gauntlet.conf overrides for this one
211
+ ```
212
+
213
+ ```bash
214
+ GAUNTLET_OFF=1 # disable entirely
215
+ GAUNTLET_TYPECHECK='npm run check' # "" to skip the gate
216
+ GAUNTLET_TEST='npm test -- $FILES' # $FILES = changed code files
217
+ GAUNTLET_CODE_EXT='ts|tsx|py' # what counts as code
218
+ ```
219
+
220
+ Set your house rules once in `~/.claude/gauntlet.conf`; only reach for the project file
221
+ where a repo does something unusual (or set `GAUNTLET_OFF=1` there to opt one out).
222
+
223
+ It registers in `.claude/settings.json`:
224
+
225
+ ```json
226
+ {
227
+ "hooks": {
228
+ "Stop": [
229
+ { "hooks": [
230
+ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/gauntlet.sh" } ] }
231
+ ]
232
+ }
233
+ }
234
+ ```
235
+
236
+ </details>
237
+
166
238
  <details>
167
239
  <summary>Manual install (without the CLI)</summary>
168
240
 
@@ -186,6 +258,7 @@ skills/ Skill playbooks — some are bundles (SKILL.md + on-demand refere
186
258
  commands/ Slash commands installed to .claude/commands/
187
259
  agents/ Subagent definitions — commands declare which they need via requires-agents
188
260
  scripts/ validate-skills.mjs — checks skill length caps and reference integrity
261
+ gauntlet.sh — the Stop-hook verification gates, embedded by the CLI
189
262
  cli/ The npm installer (npx @spardutti/claude-skills); version in cli/package.json
190
263
  .husky/ pre-push hook running the skill validator
191
264
  package.json Private dev-tooling package (claude-skills-dev) — not the published one
@@ -200,6 +273,19 @@ Skills live in `skills/<name>/SKILL.md`. Authoring conventions are in [CLAUDE.md
200
273
  - References are one level deep — `SKILL.md` links them, they don't link each other.
201
274
  - `npm run validate-skills` enforces this; it also runs on `pre-push`.
202
275
 
276
+ To get the validator on **every turn** instead of only on push, drop this into
277
+ this repo's `.claude/gauntlet.conf` — `.claude/` is gitignored, so each contributor adds it
278
+ locally. It has to be the project file, not `~/.claude/gauntlet.conf`: these settings are
279
+ only right for a skills repo:
280
+
281
+ ```bash
282
+ GAUNTLET_CODE_EXT='md|mjs|js|json|sh' # markdown IS the product here
283
+ GAUNTLET_TYPECHECK=''
284
+ GAUNTLET_TEST='node scripts/validate-skills.mjs'
285
+ ```
286
+
287
+ Without the `CODE_EXT` line the hook treats a skill edit as a docs-only change and skips.
288
+
203
289
  ## License
204
290
 
205
291
  MIT
package/bin/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { confirm } from "@inquirer/prompts";
3
+ import { confirm, input } from "@inquirer/prompts";
4
4
  import chalk from "chalk";
5
5
  import { readFileSync } from "node:fs";
6
6
  import { fileURLToPath } from "node:url";
@@ -8,7 +8,7 @@ import { dirname, join } from "node:path";
8
8
  import { fetchSkills, fetchCommands, fetchAgents } from "../lib/github.mjs";
9
9
  import { promptSkillSelection, promptCommandSelection, promptRemoval } from "../lib/prompt.mjs";
10
10
  import { installSkills, installCommands, installRequiredAgents } from "../lib/install.mjs";
11
- import { setupHook } from "../lib/setup-hook.mjs";
11
+ import { setupHook, detectStack, writeGauntletConf } from "../lib/setup-hook.mjs";
12
12
  import { setupClaudeMd } from "../lib/setup-claude-md.mjs";
13
13
  import {
14
14
  readManifest, writeManifest, computeOrphans, computeRemovals, scanInstalled, removeArtifacts,
@@ -143,17 +143,40 @@ async function main() {
143
143
  process.exit(0);
144
144
  }
145
145
 
146
- // --- Hook setup (only if skills were installed) ---
147
- if (selectedSkills.length > 0) {
146
+ // --- Hook setup ---
147
+ // Offered whenever anything was installed: the skill gates need skills, but the
148
+ // gauntlet guards any repo with a test runner, commands-only installs included.
149
+ if (selectedSkills.length > 0 || selectedCommands.length > 0) {
148
150
  console.log();
149
151
  const shouldSetup = await confirm({
150
- message: "Set up skill evaluation hook + CLAUDE.md rule? (Recommended)",
152
+ message: "Set up hooks — skill evaluation before edits, verification after? (Recommended)",
151
153
  default: true,
152
154
  });
153
155
  if (shouldSetup) {
154
156
  console.log();
155
157
  await setupHook();
156
158
  await setupClaudeMd();
159
+
160
+ // Report what the gauntlet will actually run, so a silent no-op is visible.
161
+ const { test, typecheck } = await detectStack(CWD);
162
+ if (test) {
163
+ const found = [typecheck, test].filter(Boolean).join(" + ");
164
+ console.log(` ${chalk.dim(`Gauntlet detected ${found} — it will run those on changed files.`)}`);
165
+ } else {
166
+ console.log(` ${chalk.yellow("!")} ${chalk.dim("Gauntlet found no test runner here — it will stay asleep.")}`);
167
+ console.log();
168
+ const wantConf = await confirm({
169
+ message: "Point it at your test command now?",
170
+ default: false,
171
+ });
172
+ if (wantConf) {
173
+ const cmd = (await input({ message: "Test command:" })).trim();
174
+ if (cmd) {
175
+ const written = await writeGauntletConf(CWD, cmd);
176
+ console.log(` Config written: ${written.replace(CWD + "/", "")}`);
177
+ }
178
+ }
179
+ }
157
180
  }
158
181
  }
159
182
 
@@ -1,5 +1,5 @@
1
1
  import { mkdir, writeFile, readFile, chmod, unlink } from "node:fs/promises";
2
- import { join, resolve } from "node:path";
2
+ import { join, resolve, dirname } from "node:path";
3
3
 
4
4
  // PreToolUse gate on Write|Edit|MultiEdit. Blocks the tool call unless
5
5
  // a per-session marker file exists at /tmp/claude-skill-gate-<SESSION_ID>.
@@ -155,18 +155,215 @@ printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision"
155
155
  exit 0
156
156
  `;
157
157
 
158
+ // Stop hook: fast deterministic gates on the changed diff. Silent when green,
159
+ // blocks the turn when a gate is red so the model fixes it before the user sees
160
+ // it. Canonical source: scripts/gauntlet.sh in the claude-skills repo.
161
+ const GAUNTLET_SCRIPT = `#!/usr/bin/env bash
162
+ # gauntlet.sh — Stop hook: fast deterministic gates on the changed diff.
163
+ #
164
+ # Runs when Claude finishes a turn. Silent when green. Blocks the turn when a
165
+ # gate is red, handing the failure back to Claude to fix before the user sees it.
166
+ #
167
+ # Only cheap gates belong here (target: under ~30s). Mutation testing, deep
168
+ # review, and the spec checklist are the /gauntlet command's job, not this hook's.
169
+ #
170
+ # Skip ladder, cheapest first — quits at the first "no":
171
+ # 1. no changed files
172
+ # 2. no changed *code* files (docs/config only)
173
+ # 3. this exact diff already passed
174
+ # 4. no test runner detected
175
+ #
176
+ # Config, both optional and both KEY=value files that get sourced in this order:
177
+ # ~/.claude/gauntlet.conf your defaults for every project
178
+ # <project>/.claude/gauntlet.conf overrides for this one; wins key by key
179
+ # GAUNTLET_OFF=1 disable entirely
180
+ # GAUNTLET_TYPECHECK="cmd" override the typecheck command ("" to skip)
181
+ # GAUNTLET_TEST="cmd" override the test command; $FILES = changed code files
182
+ # GAUNTLET_CODE_EXT="ts|tsx|py" override which extensions count as code
183
+
184
+ set -uo pipefail
185
+
186
+ INPUT=$(cat 2>/dev/null || true)
187
+
188
+ # Never re-block a turn we already blocked — this would loop forever.
189
+ case "$INPUT" in
190
+ *'"stop_hook_active":true'*) exit 0 ;;
191
+ esac
192
+
193
+ PROJECT_DIR="\${CLAUDE_PROJECT_DIR:-}"
194
+ if [ -z "$PROJECT_DIR" ]; then PROJECT_DIR="$PWD"; fi
195
+ cd "$PROJECT_DIR" 2>/dev/null || exit 0
196
+
197
+ git rev-parse --git-dir >/dev/null 2>&1 || exit 0
198
+
199
+ GAUNTLET_OFF=""
200
+ GAUNTLET_TYPECHECK="__auto__"
201
+ GAUNTLET_TEST="__auto__"
202
+ GAUNTLET_CODE_EXT="ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|c|h|cpp|hpp|cs|swift|sql"
203
+ # Global defaults first, then the project's on top — project wins, key by key.
204
+ if [ -n "\${HOME:-}" ] && [ -f "$HOME/.claude/gauntlet.conf" ]; then . "$HOME/.claude/gauntlet.conf"; fi
205
+ if [ -f "$PROJECT_DIR/.claude/gauntlet.conf" ]; then . "$PROJECT_DIR/.claude/gauntlet.conf"; fi
206
+ if [ -n "$GAUNTLET_OFF" ]; then exit 0; fi
207
+
208
+ # ---------------------------------------------------------------- skip rule 1
209
+ CHANGED=$( { git diff --name-only HEAD 2>/dev/null; \\
210
+ git ls-files --others --exclude-standard 2>/dev/null; } | sort -u )
211
+ if [ -z "$CHANGED" ]; then exit 0; fi
212
+
213
+ # ---------------------------------------------------------------- skip rule 2
214
+ FILES=$(printf '%s\\n' "$CHANGED" | grep -E "\\.($GAUNTLET_CODE_EXT)$" || true)
215
+ if [ -z "$FILES" ]; then exit 0; fi
216
+
217
+ # ---------------------------------------------------------------- skip rule 3
218
+ DIFF_HASH=$( { git diff HEAD 2>/dev/null; printf '%s\\n' "$CHANGED"; } \\
219
+ | git hash-object --stdin 2>/dev/null )
220
+ SESSION_ID=$(printf '%s' "$INPUT" | grep -o '"session_id":"[^"]*"' | head -1 \\
221
+ | sed 's/"session_id":"//; s/"$//')
222
+ if [ -z "$SESSION_ID" ]; then SESSION_ID="nosession"; fi
223
+ MARKER="/tmp/claude-gauntlet-$SESSION_ID"
224
+ if [ -f "$MARKER" ] && [ "$(cat "$MARKER")" = "$DIFF_HASH" ]; then exit 0; fi
225
+
226
+ # ---------------------------------------------------------------- skip rule 4
227
+ # Detect the stack once. Auto-detection is a default, not a decision — the conf
228
+ # file wins whenever a repo does something unusual.
229
+ detect() {
230
+ if [ "$GAUNTLET_TEST" != "__auto__" ]; then return; fi
231
+ GAUNTLET_TEST=""
232
+ GAUNTLET_TYPECHECK=""
233
+ if [ -f package.json ]; then
234
+ if grep -q '"vitest"' package.json; then
235
+ GAUNTLET_TEST='npx --no-install vitest related --run $FILES'
236
+ elif grep -q '"jest"' package.json; then
237
+ GAUNTLET_TEST='npx --no-install jest --findRelatedTests $FILES --passWithNoTests'
238
+ fi
239
+ if [ -f tsconfig.json ]; then
240
+ GAUNTLET_TYPECHECK='npx --no-install tsc --noEmit'
241
+ fi
242
+ fi
243
+ if [ -z "$GAUNTLET_TEST" ] && { [ -f pyproject.toml ] || [ -f pytest.ini ] || [ -f setup.cfg ]; }; then
244
+ GAUNTLET_TEST='python -m pytest -q'
245
+ if [ -f mypy.ini ] || grep -qs 'tool.mypy' pyproject.toml; then
246
+ GAUNTLET_TYPECHECK='python -m mypy .'
247
+ fi
248
+ fi
249
+ }
250
+ detect
251
+ if [ -z "$GAUNTLET_TEST" ]; then exit 0; fi
252
+
253
+ # ------------------------------------------------------------------ the gates
254
+ # Cheapest first, stop at the first red. Most failures land in the first gate,
255
+ # so the expensive ones are usually never paid for.
256
+ export FILES=$(printf '%s' "$FILES" | tr '\\n' ' ')
257
+ FAILED=""
258
+ OUTPUT=""
259
+
260
+ run_gate() {
261
+ GATE_NAME="$1"
262
+ GATE_CMD="$2"
263
+ if [ -z "$GATE_CMD" ]; then return 0; fi
264
+ GATE_OUT=$(eval "$GATE_CMD" 2>&1)
265
+ if [ $? -ne 0 ]; then
266
+ FAILED="$GATE_NAME"
267
+ OUTPUT="$GATE_OUT"
268
+ return 1
269
+ fi
270
+ return 0
271
+ }
272
+
273
+ run_gate "typecheck" "$GAUNTLET_TYPECHECK" && run_gate "tests" "$GAUNTLET_TEST"
274
+
275
+ if [ -z "$FAILED" ]; then
276
+ printf '%s' "$DIFF_HASH" > "$MARKER"
277
+ exit 0
278
+ fi
279
+
280
+ # ------------------------------------------------------------------ block red
281
+ # Last 60 lines only — the hook's reason is read by a model, not archived.
282
+ TAIL=$(printf '%s\\n' "$OUTPUT" | tail -60)
283
+ MSG="GAUNTLET FAILED: the $FAILED gate is red on the changed files.
284
+
285
+ Fix it before ending the turn. Do not weaken or delete a test to make this pass.
286
+ If the failure is unrelated to your change, say so explicitly and stop.
287
+
288
+ Changed code files:
289
+ $FILES
290
+
291
+ --- $FAILED output (last 60 lines) ---
292
+ $TAIL"
293
+
294
+ json_escape() {
295
+ local s="$1"
296
+ s="\${s//\\\\/\\\\\\\\}"
297
+ s="\${s//\\"/\\\\\\"}"
298
+ s="\${s//$'\\n'/\\\\n}"
299
+ s="\${s//$'\\r'/\\\\r}"
300
+ s="\${s//$'\\t'/\\\\t}"
301
+ printf '"%s"' "$s"
302
+ }
303
+
304
+ printf '{"decision":"block","reason":%s}\\n' "$(json_escape "$MSG")"
305
+ exit 0
306
+ `;
307
+
158
308
  const GATE_FILENAME = "skill-gate.sh";
159
309
  const AUTO_MARK_FILENAME = "skill-gate-automark.sh";
160
310
  const APPLICATION_GATE_FILENAME = "skill-application-gate.sh";
311
+ const GAUNTLET_FILENAME = "gauntlet.sh";
161
312
  const LEGACY_EVAL_FILENAME = "skill-forced-eval-hook.sh";
162
313
  const LEGACY_AUDIT_RUNNER_FILENAME = "skill-audit-runner.sh";
163
314
 
315
+ // Mirrors the detect() in scripts/gauntlet.sh. Kept in sync by hand: this one
316
+ // only reports what the hook will find, it never decides anything.
317
+ export async function detectStack(targetDir = process.cwd()) {
318
+ const dir = resolve(targetDir);
319
+ const has = async (f) => {
320
+ try { await readFile(join(dir, f), "utf-8"); return true; } catch { return false; }
321
+ };
322
+ const read = async (f) => {
323
+ try { return await readFile(join(dir, f), "utf-8"); } catch { return ""; }
324
+ };
325
+
326
+ let test = null;
327
+ let typecheck = null;
328
+
329
+ const pkg = await read("package.json");
330
+ if (pkg) {
331
+ if (pkg.includes('"vitest"')) test = "vitest";
332
+ else if (pkg.includes('"jest"')) test = "jest";
333
+ if (await has("tsconfig.json")) typecheck = "tsc";
334
+ }
335
+ if (!test && (await has("pyproject.toml") || await has("pytest.ini") || await has("setup.cfg"))) {
336
+ test = "pytest";
337
+ if (await has("mypy.ini") || (await read("pyproject.toml")).includes("tool.mypy")) {
338
+ typecheck = "mypy";
339
+ }
340
+ }
341
+ return { test, typecheck };
342
+ }
343
+
344
+ // Written only when detection finds nothing — otherwise the hook auto-detects and
345
+ // no config file is needed.
346
+ export async function writeGauntletConf(targetDir, testCommand) {
347
+ const confPath = join(resolve(targetDir), ".claude", "gauntlet.conf");
348
+ await mkdir(dirname(confPath), { recursive: true });
349
+ await writeFile(confPath, [
350
+ "# Gauntlet config. Read after ~/.claude/gauntlet.conf, and wins over it.",
351
+ "# GAUNTLET_OFF=1 disables the hook in this project.",
352
+ "",
353
+ "GAUNTLET_TYPECHECK=''",
354
+ `GAUNTLET_TEST='${testCommand.replace(/'/g, "'\\''")}'`,
355
+ "",
356
+ ].join("\n"), { mode: 0o644 });
357
+ return confPath;
358
+ }
359
+
164
360
  export async function setupHook(targetDir = process.cwd()) {
165
361
  const resolved = resolve(targetDir);
166
362
  const hooksDir = join(resolved, ".claude", "hooks");
167
363
  const gatePath = join(hooksDir, GATE_FILENAME);
168
364
  const autoMarkPath = join(hooksDir, AUTO_MARK_FILENAME);
169
365
  const applicationGatePath = join(hooksDir, APPLICATION_GATE_FILENAME);
366
+ const gauntletPath = join(hooksDir, GAUNTLET_FILENAME);
170
367
  const settingsPath = join(resolved, ".claude", "settings.json");
171
368
 
172
369
  await mkdir(hooksDir, { recursive: true });
@@ -176,6 +373,8 @@ export async function setupHook(targetDir = process.cwd()) {
176
373
  await chmod(autoMarkPath, 0o755);
177
374
  await writeFile(applicationGatePath, APPLICATION_GATE_SCRIPT, { mode: 0o755 });
178
375
  await chmod(applicationGatePath, 0o755);
376
+ await writeFile(gauntletPath, GAUNTLET_SCRIPT, { mode: 0o755 });
377
+ await chmod(gauntletPath, 0o755);
179
378
 
180
379
  // Remove the legacy audit-runner hook file from prior installs (2.3.x).
181
380
  try {
@@ -252,10 +451,24 @@ export async function setupHook(targetDir = process.cwd()) {
252
451
  settings.hooks.PostToolUse = [autoMarkEntry];
253
452
  }
254
453
 
454
+ // Register Stop gauntlet hook.
455
+ const gauntletCommand = `$CLAUDE_PROJECT_DIR/.claude/hooks/${GAUNTLET_FILENAME}`;
456
+ const gauntletEntry = { hooks: [{ type: "command", command: gauntletCommand }] };
457
+
458
+ if (Array.isArray(settings.hooks.Stop)) {
459
+ const exists = settings.hooks.Stop.some((entry) =>
460
+ entry.hooks?.some((h) => h.command?.endsWith(GAUNTLET_FILENAME))
461
+ );
462
+ if (!exists) settings.hooks.Stop.push(gauntletEntry);
463
+ } else {
464
+ settings.hooks.Stop = [gauntletEntry];
465
+ }
466
+
255
467
  await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", { mode: 0o644 });
256
468
 
257
469
  console.log(` Hook installed: .claude/hooks/${GATE_FILENAME}`);
258
470
  console.log(` Hook installed: .claude/hooks/${AUTO_MARK_FILENAME}`);
259
471
  console.log(` Hook installed: .claude/hooks/${APPLICATION_GATE_FILENAME}`);
472
+ console.log(` Hook installed: .claude/hooks/${GAUNTLET_FILENAME}`);
260
473
  console.log(` Settings updated: .claude/settings.json`);
261
474
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spardutti/claude-skills",
3
- "version": "2.8.1",
3
+ "version": "2.9.0",
4
4
  "description": "CLI to install Claude Code skills from the claude-skills collection",
5
5
  "type": "module",
6
6
  "bin": {