@spardutti/claude-skills 2.26.0 → 2.28.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 +1 -1
- package/lib/setup-hook.mjs +74 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -133,7 +133,7 @@ Portable slash commands installed to `.claude/commands/`. Some orchestrate paral
|
|
|
133
133
|
|---------|--------------|
|
|
134
134
|
| `/ship` | Unified delivery pipeline — commit → gate → PR → merge → release. The gate is the one enforcement moment: it checks the diff's file lengths, audits it against the skills installed in the project, and mutation-tests the changed lines to prove the tests would catch a break — **fixing what it finds** rather than handing you a list. It runs before the **PR**, not before every commit: its scope is the whole branch, so gating each commit re-mutated every file the branch had ever touched. `--force` skips it. No argument steps through interactively; `/ship pr` runs through PR creation; `/ship release` runs the full pipeline |
|
|
135
135
|
| `/discover` | Find the right problem before deciding what to build — diverges first: generates competing framings through blind subagents under forced constraints, stress-tests the winner with a blind critic, and refuses to converge until every open question is answered or deferred, then drafts scope, non-goals, edge cases and success criteria for you to correct. Run before `/plan-feature` |
|
|
136
|
-
| `/plan-feature` | Integration-first feature planning — 3 parallel subagents scan for reusable code, patterns
|
|
136
|
+
| `/plan-feature` | Integration-first feature planning, then building — 3 parallel subagents scan for reusable code, patterns and touch points, and the plan lands with a contract and a per-worker file split; on your go it dispatches parallel implementation workers that each load their own skills and report back as one diff |
|
|
137
137
|
| `/refactor` | Detect size / complexity / duplication / coupling issues via 4 parallel subagents, then refactor |
|
|
138
138
|
| `/deep-review` | Multi-agent deep code review — 5 parallel subagents catch guard bypasses, lost async state, wrong-table queries, dead references, protocol violations |
|
|
139
139
|
| `/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 |
|
package/lib/setup-hook.mjs
CHANGED
|
@@ -4,7 +4,10 @@ import { spawn } from "node:child_process";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
|
|
6
6
|
// PreToolUse gate on Write|Edit|MultiEdit. Blocks the tool call unless
|
|
7
|
-
// a
|
|
7
|
+
// a marker file exists at /tmp/claude-skill-gate-<KEY>, where KEY is the
|
|
8
|
+
// subagent's agent_id when the call comes from one, and the session_id
|
|
9
|
+
// otherwise — a subagent reports the parent's session_id, so keying on that
|
|
10
|
+
// alone let a worker inherit the main thread's cleared gate.
|
|
8
11
|
// Per-session (not per-prompt) so simple confirmations like "yes" don't
|
|
9
12
|
// re-lock the gate after evaluation has already happened in the session.
|
|
10
13
|
// The PostToolUse hook on Skill creates the marker automatically; for
|
|
@@ -30,10 +33,25 @@ if [ -z "$SESSION_ID" ]; then
|
|
|
30
33
|
exit 0
|
|
31
34
|
fi
|
|
32
35
|
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
|
|
36
|
+
# A subagent reports the PARENT's session_id — measured, not assumed: a probe
|
|
37
|
+
# hook dumped its own stdin from the main thread and from inside a subagent and
|
|
38
|
+
# the two ids were identical. So a worker spawned to implement part of a plan
|
|
39
|
+
# inherited the main thread's cleared markers and edited with none of the
|
|
40
|
+
# skills loaded in its own context.
|
|
41
|
+
#
|
|
42
|
+
# agent_id is present only inside a subagent, and holds steady across that
|
|
43
|
+
# subagent's turns, so keying on it gives each worker its own namespace and
|
|
44
|
+
# costs it one Skill call per skill, not one per turn.
|
|
45
|
+
AGENT_ID=$(printf '%s' "$INPUT" | grep -o '"agent_id":"[^"]*"' | head -1 | sed 's/"agent_id":"//; s/"$//')
|
|
46
|
+
KEY="$SESSION_ID"
|
|
47
|
+
if [ -n "$AGENT_ID" ]; then
|
|
48
|
+
KEY=$(printf '%s' "$AGENT_ID" | tr -cd 'A-Za-z0-9_-')
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# The marker is checked at the very bottom now, not here. A skill the edited
|
|
52
|
+
# file's own stack demands is not something an already-cleared gate excuses,
|
|
53
|
+
# so the required-skill scan below runs first.
|
|
54
|
+
MARKER="/tmp/claude-skill-gate-$KEY"
|
|
37
55
|
|
|
38
56
|
# Write|Edit|MultiEdit is not the only way to change a file. A session that edits
|
|
39
57
|
# through \`python3 - <<'PY'\` in Bash walked past this gate entirely — twelve
|
|
@@ -57,7 +75,9 @@ if [ "$TOOL" != "Bash" ]; then
|
|
|
57
75
|
TARGET=$(printf '%s' "$INPUT" | grep -o '"file_path":[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:[[:space:]]*"//; s/"$//')
|
|
58
76
|
case "$TARGET" in
|
|
59
77
|
# The settings file that configures the escape hatch cannot sit behind the
|
|
60
|
-
# gate, or a denied ack has no way to be un-denied.
|
|
78
|
+
# gate, or a denied ack has no way to be un-denied. The bare relative form
|
|
79
|
+
# matters: a skill claiming **/* gated it, and nothing could re-open it.
|
|
80
|
+
.claude/settings.json|.claude/settings.local.json) exit 0 ;;
|
|
61
81
|
*/.claude/settings.json|*/.claude/settings.local.json) exit 0 ;;
|
|
62
82
|
*.*) printf '%s' "$TARGET" | grep -qiE "\.($PROSE_EXT)$" && exit 0 ;;
|
|
63
83
|
esac
|
|
@@ -90,8 +110,9 @@ if [ "$TOOL" = "Bash" ]; then
|
|
|
90
110
|
*"<<"*|*python*" -c"*|*node*" -e"*|*perl*" -e"*|*ruby*" -e"*) WRITES=1 ;;
|
|
91
111
|
esac
|
|
92
112
|
[ -z "$WRITES" ] && exit 0
|
|
93
|
-
# A Bash write names its files in the command itself.
|
|
94
|
-
|
|
113
|
+
# A Bash write names its files in the command itself. Dockerfile and
|
|
114
|
+
# .dockerignore carry no extension, so they are matched by name.
|
|
115
|
+
TARGETS=$(printf '%s' "$CMD" | grep -oiE "[A-Za-z0-9_./-]+\.($CODE_EXT)|[A-Za-z0-9_./-]*(Dockerfile[A-Za-z0-9_.-]*|\.dockerignore)" 2>/dev/null)
|
|
95
116
|
fi
|
|
96
117
|
|
|
97
118
|
# --- mandatory skills -------------------------------------------------------
|
|
@@ -106,18 +127,26 @@ fi
|
|
|
106
127
|
# without both fields keep the old behaviour, so this tightens nothing that was
|
|
107
128
|
# not deliberately declared.
|
|
108
129
|
MISSING=""
|
|
130
|
+
MANIFESTS=""
|
|
109
131
|
if [ -n "$TARGETS" ]; then
|
|
110
132
|
MANIFESTS=$(find "$PROJECT_DIR" -maxdepth 4 \\( -name node_modules -o -name .git -o -name .venv -o -name dist \\) -prune -o \\( -name package.json -o -name pyproject.toml -o -name requirements.txt \\) -print 2>/dev/null)
|
|
111
133
|
fi
|
|
112
|
-
if [ -n "$TARGETS" ]
|
|
134
|
+
if [ -n "$TARGETS" ]; then
|
|
113
135
|
for SKILL_FILE in $(find "$PROJECT_DIR" -path '*/.claude/skills/*/SKILL.md' 2>/dev/null); do
|
|
114
|
-
# paths
|
|
136
|
+
# Read gate-paths from under metadata:, never a bare paths: key. A bare
|
|
137
|
+
# paths: made Claude Code drop the skill from its registry outright —
|
|
138
|
+
# every Skill(name) call returned "Unknown skill" and, with no SKIP for a
|
|
139
|
+
# mandatory skill, the session could not edit code at all. metadata is
|
|
140
|
+
# documented as free-form and ignored by Claude Code, and the docs say in
|
|
141
|
+
# so many words not to reuse paths as a key inside it.
|
|
142
|
+
#
|
|
143
|
+
# The value must be quoted in YAML either way: a scalar opening with * is
|
|
144
|
+
# an alias indicator.
|
|
115
145
|
UNQUOTE='s/^["'"'"']//; s/["'"'"']$//'
|
|
116
|
-
SPATHS=$(sed -n 's/^paths:[[:space:]]*//p' "$SKILL_FILE" | head -1 | sed "$UNQUOTE")
|
|
146
|
+
SPATHS=$(sed -n 's/^[[:space:]]*gate-paths:[[:space:]]*//p' "$SKILL_FILE" | head -1 | sed "$UNQUOTE")
|
|
117
147
|
STRACKS=$(sed -n 's/^tracks:[[:space:]]*//p' "$SKILL_FILE" | head -1 | sed "$UNQUOTE")
|
|
118
148
|
SNAME=$(sed -n 's/^name:[[:space:]]*//p' "$SKILL_FILE" | head -1 | sed "$UNQUOTE")
|
|
119
149
|
[ -z "$SPATHS" ] && continue
|
|
120
|
-
[ -z "$STRACKS" ] && continue
|
|
121
150
|
[ -z "$SNAME" ] && continue
|
|
122
151
|
|
|
123
152
|
HIT=""
|
|
@@ -134,9 +163,14 @@ if [ -n "$TARGETS" ] && [ -n "$MANIFESTS" ]; then
|
|
|
134
163
|
|
|
135
164
|
# A .tsx file is not proof of React — it could be Astro, Solid or Preact.
|
|
136
165
|
# The packages the skill tracks are the proof, so the manifests decide.
|
|
166
|
+
#
|
|
167
|
+
# Some skills need no such proof: a .sql file is SQL and a Dockerfile is a
|
|
168
|
+
# Dockerfile, and no package declares either. A skill that names paths and
|
|
169
|
+
# no tracks is taken at its word.
|
|
137
170
|
PY=""
|
|
138
171
|
case "$STRACKS" in *pypi*) PY=1 ;; esac
|
|
139
172
|
DEP=""
|
|
173
|
+
[ -z "$STRACKS" ] && DEP=1
|
|
140
174
|
for P in $(printf '%s' "$STRACKS" | tr ',' ' '); do
|
|
141
175
|
case "$P" in *@*) ;; *) continue ;; esac
|
|
142
176
|
PKG=$(printf '%s' "$P" | sed 's/@[^@]*$//')
|
|
@@ -152,7 +186,7 @@ if [ -n "$TARGETS" ] && [ -n "$MANIFESTS" ]; then
|
|
|
152
186
|
[ -z "$DEP" ] && continue
|
|
153
187
|
|
|
154
188
|
SAFE=$(printf '%s' "$SNAME" | tr -cd 'A-Za-z0-9_-')
|
|
155
|
-
[ -f "/tmp/claude-skill-loaded-$
|
|
189
|
+
[ -f "/tmp/claude-skill-loaded-$KEY-$SAFE" ] && continue
|
|
156
190
|
case " $MISSING " in *" $SNAME "*) ;; *) MISSING="$MISSING $SNAME" ;; esac
|
|
157
191
|
done
|
|
158
192
|
fi
|
|
@@ -172,7 +206,7 @@ fi
|
|
|
172
206
|
|
|
173
207
|
|
|
174
208
|
cat <<EOF
|
|
175
|
-
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"BLOCKED: skill evaluation required before file edits in this session.\\n\\nStep 1 — evaluate every available skill as ACTIVATE or SKIP with a one-line reason.\\n\\nStep 2 — you MUST take EXACTLY ONE of these tool actions to clear the gate. Listing skills in text is NOT enough; retrying the edit without doing one of these will be denied again:\\n (a) If any skill is ACTIVATE → call Skill(name) for it. This auto-clears the gate.\\n (b) If ALL skills are SKIP → run this Bash tool call: touch /tmp/claude-skill-gate-$
|
|
209
|
+
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"BLOCKED: skill evaluation required before file edits in this session.\\n\\nStep 1 — evaluate every available skill as ACTIVATE or SKIP with a one-line reason.\\n\\nStep 2 — you MUST take EXACTLY ONE of these tool actions to clear the gate. Listing skills in text is NOT enough; retrying the edit without doing one of these will be denied again:\\n (a) If any skill is ACTIVATE → call Skill(name) for it. This auto-clears the gate.\\n (b) If ALL skills are SKIP → run this Bash tool call: touch /tmp/claude-skill-gate-$KEY\\n\\nStep 3 — only after Step 2 completes, retry the file edit."}}
|
|
176
210
|
EOF
|
|
177
211
|
exit 0
|
|
178
212
|
`;
|
|
@@ -191,14 +225,24 @@ if [ -z "$SESSION_ID" ]; then
|
|
|
191
225
|
exit 0
|
|
192
226
|
fi
|
|
193
227
|
|
|
194
|
-
|
|
228
|
+
# Keyed exactly as the gate keys it. A subagent carries the parent's
|
|
229
|
+
# session_id, so both scripts fall back to agent_id when one is present — and
|
|
230
|
+
# they must agree, or a loaded skill writes a marker the gate never looks for
|
|
231
|
+
# and every edit is denied forever.
|
|
232
|
+
AGENT_ID=$(printf '%s' "$INPUT" | grep -o '"agent_id":"[^"]*"' | head -1 | sed 's/"agent_id":"//; s/"$//')
|
|
233
|
+
KEY="$SESSION_ID"
|
|
234
|
+
if [ -n "$AGENT_ID" ]; then
|
|
235
|
+
KEY=$(printf '%s' "$AGENT_ID" | tr -cd 'A-Za-z0-9_-')
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
touch "/tmp/claude-skill-gate-$KEY"
|
|
195
239
|
|
|
196
240
|
SKILL_NAME=$(printf '%s' "$INPUT" | grep -o '"skill":"[^"]*"' | head -1 | sed 's/"skill":"//; s/"$//')
|
|
197
241
|
if [ -n "$SKILL_NAME" ]; then
|
|
198
242
|
# Sanitize: only allow [A-Za-z0-9_-] in the marker filename.
|
|
199
243
|
SAFE_NAME=$(printf '%s' "$SKILL_NAME" | tr -cd 'A-Za-z0-9_-')
|
|
200
244
|
if [ -n "$SAFE_NAME" ]; then
|
|
201
|
-
touch "/tmp/claude-skill-loaded-$
|
|
245
|
+
touch "/tmp/claude-skill-loaded-$KEY-$SAFE_NAME"
|
|
202
246
|
fi
|
|
203
247
|
fi
|
|
204
248
|
|
|
@@ -222,8 +266,16 @@ if [ -z "$SESSION_ID" ]; then
|
|
|
222
266
|
exit 0
|
|
223
267
|
fi
|
|
224
268
|
|
|
225
|
-
#
|
|
226
|
-
|
|
269
|
+
# Keyed exactly as the loading gate and the automark key it — see those two.
|
|
270
|
+
# A subagent carries the parent's session_id, so all three must agree.
|
|
271
|
+
AGENT_ID=$(printf '%s' "$INPUT" | grep -o '"agent_id":"[^"]*"' | head -1 | sed 's/"agent_id":"//; s/"$//')
|
|
272
|
+
KEY="$SESSION_ID"
|
|
273
|
+
if [ -n "$AGENT_ID" ]; then
|
|
274
|
+
KEY=$(printf '%s' "$AGENT_ID" | tr -cd 'A-Za-z0-9_-')
|
|
275
|
+
fi
|
|
276
|
+
|
|
277
|
+
# Defer to the loading gate until it's been satisfied for this key.
|
|
278
|
+
if [ ! -f "/tmp/claude-skill-gate-$KEY" ]; then
|
|
227
279
|
exit 0
|
|
228
280
|
fi
|
|
229
281
|
|
|
@@ -274,10 +326,10 @@ fi
|
|
|
274
326
|
|
|
275
327
|
# Collect every loaded-but-unacked skill so a single ack clears them all.
|
|
276
328
|
UNACKED=""
|
|
277
|
-
for marker in /tmp/claude-skill-loaded-$
|
|
329
|
+
for marker in /tmp/claude-skill-loaded-$KEY-*; do
|
|
278
330
|
[ ! -f "$marker" ] && continue
|
|
279
|
-
skill_name="\${marker##/tmp/claude-skill-loaded-$
|
|
280
|
-
if [ ! -f "/tmp/claude-skill-acked-$
|
|
331
|
+
skill_name="\${marker##/tmp/claude-skill-loaded-$KEY-}"
|
|
332
|
+
if [ ! -f "/tmp/claude-skill-acked-$KEY-$skill_name" ]; then
|
|
281
333
|
UNACKED="$UNACKED $skill_name"
|
|
282
334
|
fi
|
|
283
335
|
done
|
|
@@ -293,7 +345,7 @@ ACK_CMD="touch"
|
|
|
293
345
|
NAMES=""
|
|
294
346
|
RULES_BLOCKS=""
|
|
295
347
|
for skill in $UNACKED; do
|
|
296
|
-
ACK_CMD="$ACK_CMD /tmp/claude-skill-acked-$
|
|
348
|
+
ACK_CMD="$ACK_CMD /tmp/claude-skill-acked-$KEY-$skill"
|
|
297
349
|
NAMES="$NAMES, '$skill'"
|
|
298
350
|
SKILL_MD="$PROJECT_DIR/.claude/skills/$skill/SKILL.md"
|
|
299
351
|
RULES=""
|