@spardutti/claude-skills 2.3.0 → 2.4.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/lib/setup-hook.mjs +87 -51
- package/package.json +1 -1
package/lib/setup-hook.mjs
CHANGED
|
@@ -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:
|
|
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
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
|
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
|
|
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(
|
|
145
|
-
await chmod(
|
|
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
|
|
182
|
-
const
|
|
183
|
-
const
|
|
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:
|
|
221
|
+
hooks: [{ type: "command", command: applicationCommand }],
|
|
186
222
|
};
|
|
187
|
-
const
|
|
188
|
-
entry.hooks?.some((h) => h.command?.endsWith(
|
|
223
|
+
const applicationAlreadyRegistered = settings.hooks.PreToolUse.some((entry) =>
|
|
224
|
+
entry.hooks?.some((h) => h.command?.endsWith(APPLICATION_GATE_FILENAME))
|
|
189
225
|
);
|
|
190
|
-
if (!
|
|
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/${
|
|
248
|
+
console.log(` Hook installed: .claude/hooks/${APPLICATION_GATE_FILENAME}`);
|
|
213
249
|
console.log(` Settings updated: .claude/settings.json`);
|
|
214
250
|
}
|