@spardutti/claude-skills 2.3.0 → 2.5.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
@@ -97,6 +97,7 @@ Portable slash commands installed to `.claude/commands/`. Some orchestrate paral
97
97
  | `/plan-feature` | Integration-first feature planning — 3 parallel subagents scan for reusable code, patterns, and touch points before producing a short plan |
98
98
  | `/refactor` | Detect size / complexity / duplication / coupling issues via 4 parallel subagents, then refactor |
99
99
  | `/deep-review` | Multi-agent deep code review — 5 parallel subagents catch guard bypasses, lost async state, wrong-table queries, dead references, protocol violations |
100
+ | `/test-review` | Write-then-verify test review — scopes to the diff, runs red-green + mutation gates, then an isolated read-only subagent proves each test would catch a regression instead of rubber-stamping it |
100
101
  | `/lockdown` | Per-repo supply-chain hardening — detects the package managers, Dockerfiles, and CI in use, then guides you through and applies install-time, deploy, and pipeline hardening for npm/pnpm, pip/uv, Docker, and GitHub Actions |
101
102
 
102
103
  ## How It Works
@@ -1,4 +1,4 @@
1
- import { mkdir, writeFile, readFile, chmod } from "node:fs/promises";
1
+ import { mkdir, writeFile, readFile, chmod, unlink } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
3
 
4
4
  // PreToolUse gate on Write|Edit|MultiEdit. Blocks the tool call unless
@@ -39,9 +39,12 @@ EOF
39
39
  exit 0
40
40
  `;
41
41
 
42
- // PostToolUse on Skill: auto-creates the per-session gate marker.
42
+ // PostToolUse on Skill: auto-creates the per-session gate marker AND a
43
+ // per-skill "loaded" marker. The application gate uses the loaded marker
44
+ // to require the model to explicitly apply each loaded skill before the
45
+ // first Write/Edit in this session.
43
46
  const AUTO_MARK_SCRIPT = `#!/bin/bash
44
- # PostToolUse on Skill: auto-marks the skill-gate as satisfied for the session.
47
+ # PostToolUse on Skill: marks gate satisfied + records loaded skill.
45
48
 
46
49
  INPUT=$(cat)
47
50
 
@@ -51,16 +54,25 @@ if [ -z "$SESSION_ID" ]; then
51
54
  fi
52
55
 
53
56
  touch "/tmp/claude-skill-gate-$SESSION_ID"
57
+
58
+ SKILL_NAME=$(printf '%s' "$INPUT" | grep -o '"skill":"[^"]*"' | head -1 | sed 's/"skill":"//; s/"$//')
59
+ if [ -n "$SKILL_NAME" ]; then
60
+ # Sanitize: only allow [A-Za-z0-9_-] in the marker filename.
61
+ SAFE_NAME=$(printf '%s' "$SKILL_NAME" | tr -cd 'A-Za-z0-9_-')
62
+ if [ -n "$SAFE_NAME" ]; then
63
+ touch "/tmp/claude-skill-loaded-$SESSION_ID-$SAFE_NAME"
64
+ fi
65
+ fi
66
+
54
67
  exit 0
55
68
  `;
56
69
 
57
- // PreToolUse audit runner: runs each installed skill's audit.sh against the
58
- // write target. Denies the write if any audit exits 2. Defers (exits 0) when
59
- // the skill-gate marker isn't set yet the gate handles that case.
60
- //
61
- // Honors .claude/skill-audit-ignore (one skill name per line, # comments OK).
62
- const AUDIT_RUNNER_SCRIPT = `#!/bin/bash
63
- # PreToolUse skill-audit runner.
70
+ // PreToolUse application gate: requires the model to explicitly apply each
71
+ // loaded skill before the first Write/Edit. Reads SKILL.md's Rules section
72
+ // and inlines it in the deny message. One ack per skill per session.
73
+ const APPLICATION_GATE_SCRIPT = `#!/bin/bash
74
+ # PreToolUse application gate: blocks file edits until each loaded skill
75
+ # has been explicitly applied (acked) for this session.
64
76
 
65
77
  INPUT=$(cat)
66
78
 
@@ -72,26 +84,51 @@ if [ -z "$SESSION_ID" ]; then
72
84
  exit 0
73
85
  fi
74
86
 
75
- # Defer until the skill-gate has been satisfied in this session.
87
+ # Defer to the loading gate until it's been satisfied this session.
76
88
  if [ ! -f "/tmp/claude-skill-gate-$SESSION_ID" ]; then
77
89
  exit 0
78
90
  fi
79
91
 
80
- FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path":"[^"]*"' | head -1 | sed 's/"file_path":"//; s/"$//')
81
- if [ -z "$FILE_PATH" ]; then
92
+ # Find the first loaded-but-unacked skill (handle one at a time; the next
93
+ # blocked write will surface the next skill).
94
+ UNACKED=""
95
+ for marker in /tmp/claude-skill-loaded-$SESSION_ID-*; do
96
+ [ ! -f "$marker" ] && continue
97
+ skill_name="\${marker##/tmp/claude-skill-loaded-$SESSION_ID-}"
98
+ if [ ! -f "/tmp/claude-skill-acked-$SESSION_ID-$skill_name" ]; then
99
+ UNACKED="$skill_name"
100
+ break
101
+ fi
102
+ done
103
+
104
+ if [ -z "$UNACKED" ]; then
82
105
  exit 0
83
106
  fi
84
107
 
85
- SKILLS_DIR="$PROJECT_DIR/.claude/skills"
86
- [ ! -d "$SKILLS_DIR" ] && exit 0
108
+ SKILL_MD="$PROJECT_DIR/.claude/skills/$UNACKED/SKILL.md"
109
+ RULES=""
110
+ if [ -f "$SKILL_MD" ]; then
111
+ RULES=$(awk '/^## Rules/{flag=1} /^## /{if(flag && !/^## Rules/)exit} flag' "$SKILL_MD")
112
+ fi
113
+ if [ -z "$RULES" ]; then
114
+ RULES="(Rules section not found in $SKILL_MD — refer to the loaded skill content already in context.)"
115
+ fi
87
116
 
88
- IGNORE_FILE="$PROJECT_DIR/.claude/skill-audit-ignore"
89
- is_ignored() {
90
- [ ! -f "$IGNORE_FILE" ] && return 1
91
- grep -qE "^[[:space:]]*\${1}[[:space:]]*(#.*)?$" "$IGNORE_FILE"
92
- }
117
+ MSG="BLOCKED: skill '$UNACKED' was loaded but not yet applied to your work.
118
+
119
+ Before this Write/Edit, you must:
120
+
121
+ 1. State the specific rules from '$UNACKED' that apply to the file you're about to write.
122
+ 2. State how your next write respects each rule.
123
+ 3. Then ack the application by running this Bash tool call:
124
+ touch /tmp/claude-skill-acked-$SESSION_ID-$UNACKED
125
+
126
+ One ack per skill per session. After acking, retry the Write/Edit.
127
+
128
+ Rules from $UNACKED/SKILL.md:
129
+
130
+ $RULES"
93
131
 
94
- # Portable JSON string escape for the deny reason.
95
132
  json_escape() {
96
133
  local s="$1"
97
134
  s="\${s//\\\\/\\\\\\\\}"
@@ -102,38 +139,23 @@ json_escape() {
102
139
  printf '"%s"' "$s"
103
140
  }
104
141
 
105
- for audit in "$SKILLS_DIR"/*/audit.sh; do
106
- [ ! -f "$audit" ] && continue
107
- [ ! -x "$audit" ] && continue
108
- skill_name=$(basename "$(dirname "$audit")")
109
- if is_ignored "$skill_name"; then
110
- continue
111
- fi
112
-
113
- output=$("$audit" "$FILE_PATH" 2>&1 >/dev/null)
114
- rc=$?
115
-
116
- if [ $rc -eq 2 ]; then
117
- reason=$(json_escape "$output")
118
- printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":%s}}\\n' "$reason"
119
- exit 0
120
- fi
121
- done
122
-
142
+ REASON=$(json_escape "$MSG")
143
+ printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":%s}}\\n' "$REASON"
123
144
  exit 0
124
145
  `;
125
146
 
126
147
  const GATE_FILENAME = "skill-gate.sh";
127
148
  const AUTO_MARK_FILENAME = "skill-gate-automark.sh";
128
- const AUDIT_RUNNER_FILENAME = "skill-audit-runner.sh";
149
+ const APPLICATION_GATE_FILENAME = "skill-application-gate.sh";
129
150
  const LEGACY_EVAL_FILENAME = "skill-forced-eval-hook.sh";
151
+ const LEGACY_AUDIT_RUNNER_FILENAME = "skill-audit-runner.sh";
130
152
 
131
153
  export async function setupHook(targetDir = process.cwd()) {
132
154
  const resolved = resolve(targetDir);
133
155
  const hooksDir = join(resolved, ".claude", "hooks");
134
156
  const gatePath = join(hooksDir, GATE_FILENAME);
135
157
  const autoMarkPath = join(hooksDir, AUTO_MARK_FILENAME);
136
- const auditRunnerPath = join(hooksDir, AUDIT_RUNNER_FILENAME);
158
+ const applicationGatePath = join(hooksDir, APPLICATION_GATE_FILENAME);
137
159
  const settingsPath = join(resolved, ".claude", "settings.json");
138
160
 
139
161
  await mkdir(hooksDir, { recursive: true });
@@ -141,8 +163,15 @@ export async function setupHook(targetDir = process.cwd()) {
141
163
  await chmod(gatePath, 0o755);
142
164
  await writeFile(autoMarkPath, AUTO_MARK_SCRIPT, { mode: 0o755 });
143
165
  await chmod(autoMarkPath, 0o755);
144
- await writeFile(auditRunnerPath, AUDIT_RUNNER_SCRIPT, { mode: 0o755 });
145
- await chmod(auditRunnerPath, 0o755);
166
+ await writeFile(applicationGatePath, APPLICATION_GATE_SCRIPT, { mode: 0o755 });
167
+ await chmod(applicationGatePath, 0o755);
168
+
169
+ // Remove the legacy audit-runner hook file from prior installs (2.3.x).
170
+ try {
171
+ await unlink(join(hooksDir, LEGACY_AUDIT_RUNNER_FILENAME));
172
+ } catch {
173
+ // not present — fine
174
+ }
146
175
 
147
176
  let settings = {};
148
177
  try {
@@ -162,6 +191,13 @@ export async function setupHook(targetDir = process.cwd()) {
162
191
  }
163
192
  }
164
193
 
194
+ // Clean up legacy PreToolUse audit-runner hook (2.3.x — removed in 2.4.0).
195
+ if (Array.isArray(settings.hooks.PreToolUse)) {
196
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
197
+ (entry) => !entry.hooks?.some((h) => h.command?.endsWith(LEGACY_AUDIT_RUNNER_FILENAME))
198
+ );
199
+ }
200
+
165
201
  // Register PreToolUse gate.
166
202
  const gateCommand = `$CLAUDE_PROJECT_DIR/.claude/hooks/${GATE_FILENAME}`;
167
203
  const gateEntry = {
@@ -178,16 +214,16 @@ export async function setupHook(targetDir = process.cwd()) {
178
214
  settings.hooks.PreToolUse = [gateEntry];
179
215
  }
180
216
 
181
- // Register PreToolUse audit runner (after the gate).
182
- const auditCommand = `$CLAUDE_PROJECT_DIR/.claude/hooks/${AUDIT_RUNNER_FILENAME}`;
183
- const auditEntry = {
217
+ // Register PreToolUse application gate (after the loading gate).
218
+ const applicationCommand = `$CLAUDE_PROJECT_DIR/.claude/hooks/${APPLICATION_GATE_FILENAME}`;
219
+ const applicationEntry = {
184
220
  matcher: "Write|Edit|MultiEdit",
185
- hooks: [{ type: "command", command: auditCommand }],
221
+ hooks: [{ type: "command", command: applicationCommand }],
186
222
  };
187
- const auditAlreadyRegistered = settings.hooks.PreToolUse.some((entry) =>
188
- entry.hooks?.some((h) => h.command?.endsWith(AUDIT_RUNNER_FILENAME))
223
+ const applicationAlreadyRegistered = settings.hooks.PreToolUse.some((entry) =>
224
+ entry.hooks?.some((h) => h.command?.endsWith(APPLICATION_GATE_FILENAME))
189
225
  );
190
- if (!auditAlreadyRegistered) settings.hooks.PreToolUse.push(auditEntry);
226
+ if (!applicationAlreadyRegistered) settings.hooks.PreToolUse.push(applicationEntry);
191
227
 
192
228
  // Register PostToolUse auto-mark on Skill.
193
229
  const autoMarkCommand = `$CLAUDE_PROJECT_DIR/.claude/hooks/${AUTO_MARK_FILENAME}`;
@@ -209,6 +245,6 @@ export async function setupHook(targetDir = process.cwd()) {
209
245
 
210
246
  console.log(` Hook installed: .claude/hooks/${GATE_FILENAME}`);
211
247
  console.log(` Hook installed: .claude/hooks/${AUTO_MARK_FILENAME}`);
212
- console.log(` Hook installed: .claude/hooks/${AUDIT_RUNNER_FILENAME}`);
248
+ console.log(` Hook installed: .claude/hooks/${APPLICATION_GATE_FILENAME}`);
213
249
  console.log(` Settings updated: .claude/settings.json`);
214
250
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spardutti/claude-skills",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "description": "CLI to install Claude Code skills from the claude-skills collection",
5
5
  "type": "module",
6
6
  "bin": {