@spardutti/claude-skills 1.26.0 → 1.27.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
@@ -101,24 +101,29 @@ The CLI will:
101
101
 
102
102
  After installing skills, the CLI asks if you want to set up automatic skill evaluation. If you say yes, it will:
103
103
 
104
- - **Create a hook** at `.claude/hooks/skill-forced-eval-hook.sh` that runs on every prompt
105
- - **Update your `CLAUDE.md`** with a `skill_evaluation` rule
104
+ - **Create a PreToolUse gate hook** at `.claude/hooks/skill-gate.sh`
105
+ - **Update your `CLAUDE.md`** with the skill-evaluation rule
106
106
 
107
- This forces Claude to explicitly evaluate every installed skill before writing code listing each skill as ACTIVATE or SKIP with a reason, then calling the relevant ones. Without this, Claude may silently ignore your skills.
107
+ The gate hard-blocks `Write`, `Edit`, and `MultiEdit` tool calls until Claude has evaluated the available skills and emitted the literal token `[skills-checked]` in its response. Once emitted, every subsequent edit in that turn passes through. The next user prompt resets the gate.
108
+
109
+ Unlike a soft reminder injected into context (which Claude can ignore), the gate denies the tool call outright — so the only path forward is to actually evaluate skills.
110
+
111
+ The gate auto-passes when the project has no skills installed, so it's safe to leave on globally.
108
112
 
109
113
  ### What gets created
110
114
 
111
- **`.claude/settings.json`** — Registers the hook:
115
+ **`.claude/settings.json`** — Registers the gate on file-writing tools:
112
116
 
113
117
  ```json
114
118
  {
115
119
  "hooks": {
116
- "UserPromptSubmit": [
120
+ "PreToolUse": [
117
121
  {
122
+ "matcher": "Write|Edit|MultiEdit",
118
123
  "hooks": [
119
124
  {
120
125
  "type": "command",
121
- "command": ".claude/hooks/skill-forced-eval-hook.sh"
126
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/skill-gate.sh"
122
127
  }
123
128
  ]
124
129
  }
@@ -127,19 +132,7 @@ This forces Claude to explicitly evaluate every installed skill before writing c
127
132
  }
128
133
  ```
129
134
 
130
- **`CLAUDE.md`** — Appends the evaluation rule:
131
-
132
- ```yaml
133
- skill_evaluation:
134
- mandatory: true
135
- rule: |
136
- BEFORE writing ANY code, you MUST:
137
- 1. List EVERY skill from the system-reminder's available skills section
138
- 2. For each skill, write: [skill-name] → ACTIVATE / SKIP — [one-line reason]
139
- 3. Call Skill(name) for every skill marked ACTIVATE
140
- 4. Only THEN proceed to implementation
141
- If you skip this evaluation, your response is INCOMPLETE and WRONG.
142
- ```
135
+ **`CLAUDE.md`** — Appends the evaluation rule, including the `[skills-checked]` sentinel that the gate looks for.
143
136
 
144
137
  ## Manual Install
145
138
 
@@ -8,9 +8,10 @@ BEFORE writing ANY code, you MUST:
8
8
  1. List EVERY skill available: check \`.claude/skills/\` (project) and \`~/.claude/skills/\` (global). The system-reminder's available-skills section is a hint, not the source of truth — if it's missing or empty, still check the directories.
9
9
  2. For each skill, write: [skill-name] → ACTIVATE / SKIP — [one-line reason]
10
10
  3. Call Skill(name) for every skill marked ACTIVATE
11
- 4. Only THEN proceed to implementation
11
+ 4. Emit the literal token \`[skills-checked]\` on its own line
12
+ 5. Only THEN proceed to implementation
12
13
 
13
- If you skip this evaluation, your response is INCOMPLETE and WRONG.`;
14
+ A PreToolUse gate hook blocks Write/Edit/MultiEdit until the \`[skills-checked]\` token appears in your response since the most recent user prompt. The gate fires once per turn — the first blocked edit is the signal to evaluate skills, then retry. If you skip the evaluation, your response is INCOMPLETE and WRONG.`;
14
15
 
15
16
  const FILE_SIZE_BODY = `## File Size Enforcement
16
17
 
@@ -1,95 +1,99 @@
1
1
  import { mkdir, writeFile, readFile, chmod } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
3
 
4
- const HOOK_SCRIPT = `#!/bin/bash
5
- # UserPromptSubmit hook that forces explicit skill evaluation
4
+ // PreToolUse gate on Write|Edit|MultiEdit. Blocks the tool call unless
5
+ // the assistant has emitted the literal sentinel [skills-checked] since
6
+ // the most recent user prompt. Resets every user turn, runs once per
7
+ // turn (subsequent edits in the same turn pass through).
8
+ //
9
+ // Pass-through cases:
10
+ // - project has no .claude/skills/*/SKILL.md files
11
+ // - transcript_path missing or unreadable
12
+ // - no user prompt found in transcript
13
+ //
14
+ // tool_result lines (which include the gate's own deny message) are
15
+ // filtered out of the sentinel scan to prevent self-satisfaction.
16
+ const GATE_SCRIPT = `#!/bin/bash
17
+ # PreToolUse gate: forces skill evaluation before file-writing tools run.
18
+
19
+ INPUT=$(cat)
6
20
 
7
- cat > /dev/null
8
-
9
- # Derive project root from the hook's own location
10
- # .claude/hooks/script.sh → go up two levels → project root
11
21
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
12
- DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
13
-
14
- # Build skill list from project skills
15
- SKILL_LIST=""
16
- while IFS= read -r skillfile; do
17
- name=$(grep -m1 '^name:' "$skillfile" 2>/dev/null | sed 's/^name: *//' | sed 's/^"//' | sed 's/"$//')
18
- desc=$(grep -m1 '^description:' "$skillfile" 2>/dev/null | sed 's/^description: *//' | sed 's/^"//' | sed 's/"$//')
19
- if [ -n "$name" ] && [ -n "$desc" ]; then
20
- SKILL_LIST="\${SKILL_LIST} - \${name}: \${desc}\\\\n"
21
- fi
22
- done < <(find "$DIR" -path '*/.claude/skills/*/SKILL.md' 2>/dev/null | sort -u)
23
-
24
- INSTRUCTION="INSTRUCTION: MANDATORY SKILL ACTIVATION SEQUENCE\\\\n\\\\n"
25
- INSTRUCTION+="<available_skills>\\\\n"
26
- INSTRUCTION+="System skills (from system-reminder):\\\\n - Check system-reminder for built-in skills\\\\n"
27
-
28
- if [ -n "$SKILL_LIST" ]; then
29
- INSTRUCTION+="Project skills:\\\\n\${SKILL_LIST}"
22
+ PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
23
+
24
+ if ! find "$PROJECT_DIR" -path '*/.claude/skills/*/SKILL.md' 2>/dev/null | grep -q .; then
25
+ exit 0
26
+ fi
27
+
28
+ TRANSCRIPT=$(printf '%s' "$INPUT" | grep -o '"transcript_path":"[^"]*"' | head -1 | sed 's/"transcript_path":"//; s/"$//')
29
+ if [ -z "$TRANSCRIPT" ] || [ ! -f "$TRANSCRIPT" ]; then
30
+ exit 0
30
31
  fi
31
32
 
32
- INSTRUCTION+="</available_skills>\\\\n\\\\n"
33
- INSTRUCTION+="Step 1 - EVALUATE (do this in your response):\\\\n"
34
- INSTRUCTION+="For each skill in <available_skills>, state: [skill-name] - YES/NO - [reason]\\\\n\\\\n"
35
- INSTRUCTION+="Step 2 - ACTIVATE (do this immediately after Step 1):\\\\n"
36
- INSTRUCTION+="IF any skills are YES -> Use Skill(skill-name) tool for EACH relevant skill NOW\\\\n"
37
- INSTRUCTION+="IF no skills are YES -> State 'No skills needed' and proceed\\\\n\\\\n"
38
- INSTRUCTION+="Step 3 - IMPLEMENT:\\\\n"
39
- INSTRUCTION+="Only after Step 2 is complete, proceed with implementation.\\\\n\\\\n"
40
- INSTRUCTION+="CRITICAL: You MUST call Skill() tool in Step 2. Do NOT skip to implementation."
41
-
42
- printf '{"additionalContext": "%s"}\\n' "$INSTRUCTION"
33
+ LAST_PROMPT=$(grep -n '"type":"user"' "$TRANSCRIPT" 2>/dev/null | grep -v 'tool_use_id' | tail -1 | cut -d: -f1)
34
+ if [ -z "$LAST_PROMPT" ]; then
35
+ exit 0
36
+ fi
37
+
38
+ if tail -n +"$LAST_PROMPT" "$TRANSCRIPT" | grep -v 'tool_use_id' | grep -qF '[skills-checked]'; then
39
+ exit 0
40
+ fi
41
+
42
+ cat <<'EOF'
43
+ {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Skill evaluation required before writing or editing code. List each available skill as ACTIVATE or SKIP with a one-line reason, call Skill() for any ACTIVATE entries, then emit the literal token [skills-checked] (square brackets included) on its own line. Then retry the tool call."}}
44
+ EOF
43
45
  exit 0
44
46
  `;
45
47
 
46
- const HOOK_FILENAME = "skill-forced-eval-hook.sh";
48
+ const GATE_FILENAME = "skill-gate.sh";
49
+ const LEGACY_EVAL_FILENAME = "skill-forced-eval-hook.sh";
47
50
 
48
51
  export async function setupHook(targetDir = process.cwd()) {
49
52
  const resolved = resolve(targetDir);
50
53
  const hooksDir = join(resolved, ".claude", "hooks");
51
- const hookPath = join(hooksDir, HOOK_FILENAME);
54
+ const gatePath = join(hooksDir, GATE_FILENAME);
52
55
  const settingsPath = join(resolved, ".claude", "settings.json");
53
56
 
54
- // Write the UserPromptSubmit hook script
55
57
  await mkdir(hooksDir, { recursive: true });
56
- await writeFile(hookPath, HOOK_SCRIPT, { mode: 0o755 });
57
- await chmod(hookPath, 0o755);
58
+ await writeFile(gatePath, GATE_SCRIPT, { mode: 0o755 });
59
+ await chmod(gatePath, 0o755);
58
60
 
59
- // Merge into existing settings.json (don't clobber other config)
60
61
  let settings = {};
61
62
  try {
62
- const raw = await readFile(settingsPath, "utf-8");
63
- settings = JSON.parse(raw);
63
+ settings = JSON.parse(await readFile(settingsPath, "utf-8"));
64
64
  } catch {
65
- // File doesn't exist or is invalid — start fresh
65
+ // missing or invalid — start fresh
66
66
  }
67
+ if (!settings.hooks) settings.hooks = {};
67
68
 
68
- if (!settings.hooks) {
69
- settings.hooks = {};
69
+ // Clean up legacy UserPromptSubmit eval hook (replaced by the gate).
70
+ if (Array.isArray(settings.hooks.UserPromptSubmit)) {
71
+ settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(
72
+ (entry) => !entry.hooks?.some((h) => h.command?.endsWith(LEGACY_EVAL_FILENAME))
73
+ );
74
+ if (settings.hooks.UserPromptSubmit.length === 0) {
75
+ delete settings.hooks.UserPromptSubmit;
76
+ }
70
77
  }
71
78
 
72
- // --- UserPromptSubmit hook (forced eval via command) ---
73
- const hookCommand = `$CLAUDE_PROJECT_DIR/.claude/hooks/${HOOK_FILENAME}`;
74
- const promptHookEntry = {
75
- hooks: [{ type: "command", command: hookCommand }],
79
+ // Register PreToolUse gate.
80
+ const gateCommand = `$CLAUDE_PROJECT_DIR/.claude/hooks/${GATE_FILENAME}`;
81
+ const gateEntry = {
82
+ matcher: "Write|Edit|MultiEdit",
83
+ hooks: [{ type: "command", command: gateCommand }],
76
84
  };
77
85
 
78
- if (Array.isArray(settings.hooks.UserPromptSubmit)) {
79
- const alreadyInstalled = settings.hooks.UserPromptSubmit.some((entry) =>
80
- entry.hooks?.some((h) => h.command?.endsWith(HOOK_FILENAME))
86
+ if (Array.isArray(settings.hooks.PreToolUse)) {
87
+ const exists = settings.hooks.PreToolUse.some((entry) =>
88
+ entry.hooks?.some((h) => h.command?.endsWith(GATE_FILENAME))
81
89
  );
82
- if (!alreadyInstalled) {
83
- settings.hooks.UserPromptSubmit.push(promptHookEntry);
84
- }
90
+ if (!exists) settings.hooks.PreToolUse.push(gateEntry);
85
91
  } else {
86
- settings.hooks.UserPromptSubmit = [promptHookEntry];
92
+ settings.hooks.PreToolUse = [gateEntry];
87
93
  }
88
94
 
89
- await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", {
90
- mode: 0o644,
91
- });
95
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", { mode: 0o644 });
92
96
 
93
- console.log(` Hook installed: .claude/hooks/${HOOK_FILENAME}`);
97
+ console.log(` Hook installed: .claude/hooks/${GATE_FILENAME}`);
94
98
  console.log(` Settings updated: .claude/settings.json`);
95
99
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spardutti/claude-skills",
3
- "version": "1.26.0",
3
+ "version": "1.27.0",
4
4
  "description": "CLI to install Claude Code skills from the claude-skills collection",
5
5
  "type": "module",
6
6
  "bin": {