@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 +12 -19
- package/lib/setup-claude-md.mjs +3 -2
- package/lib/setup-hook.mjs +65 -61
- package/package.json +1 -1
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-
|
|
105
|
-
- **Update your `CLAUDE.md`** with
|
|
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
|
-
|
|
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
|
|
115
|
+
**`.claude/settings.json`** — Registers the gate on file-writing tools:
|
|
112
116
|
|
|
113
117
|
```json
|
|
114
118
|
{
|
|
115
119
|
"hooks": {
|
|
116
|
-
"
|
|
120
|
+
"PreToolUse": [
|
|
117
121
|
{
|
|
122
|
+
"matcher": "Write|Edit|MultiEdit",
|
|
118
123
|
"hooks": [
|
|
119
124
|
{
|
|
120
125
|
"type": "command",
|
|
121
|
-
"command": "
|
|
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
|
|
package/lib/setup-claude-md.mjs
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
package/lib/setup-hook.mjs
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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
|
|
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(
|
|
57
|
-
await chmod(
|
|
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
|
-
|
|
63
|
-
settings = JSON.parse(raw);
|
|
63
|
+
settings = JSON.parse(await readFile(settingsPath, "utf-8"));
|
|
64
64
|
} catch {
|
|
65
|
-
//
|
|
65
|
+
// missing or invalid — start fresh
|
|
66
66
|
}
|
|
67
|
+
if (!settings.hooks) settings.hooks = {};
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
//
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
|
|
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.
|
|
79
|
-
const
|
|
80
|
-
entry.hooks?.some((h) => h.command?.endsWith(
|
|
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 (!
|
|
83
|
-
settings.hooks.UserPromptSubmit.push(promptHookEntry);
|
|
84
|
-
}
|
|
90
|
+
if (!exists) settings.hooks.PreToolUse.push(gateEntry);
|
|
85
91
|
} else {
|
|
86
|
-
settings.hooks.
|
|
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/${
|
|
97
|
+
console.log(` Hook installed: .claude/hooks/${GATE_FILENAME}`);
|
|
94
98
|
console.log(` Settings updated: .claude/settings.json`);
|
|
95
99
|
}
|