skill-land 1.0.0 → 1.1.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 +54 -4
- package/package.json +4 -1
- package/skill-land.mjs +65 -1
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ npx skill-land Magithar/SKILLmama --for codex,antigravity
|
|
|
8
8
|
npx skill-land Magithar/SKILLmama --for codex --verify # audit an existing install
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
[](https://www.npmjs.com/package/skill-land)
|
|
12
|
+
|
|
11
13
|
## Why this exists
|
|
12
14
|
|
|
13
15
|
`npx skills add -a <agent> -g` reports success while writing to the wrong directory
|
|
@@ -110,6 +112,32 @@ directory an agent auto-loads. So it scans before writing, delegating to
|
|
|
110
112
|
uv tool install git+https://github.com/NVIDIA/skillspector.git
|
|
111
113
|
```
|
|
112
114
|
|
|
115
|
+
### What always runs: disclosure
|
|
116
|
+
|
|
117
|
+
A skill is instructions an agent executes with full permissions, so before writing anything
|
|
118
|
+
`skill-land` shows what is actually in the file. No dependencies, so it runs for everyone:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
contains
|
|
122
|
+
3 shell commands curl, rm, git
|
|
123
|
+
1 network hosts attacker.example.com
|
|
124
|
+
5 credential refs AWS_SECRET_ACCESS_KEY, ~/.ssh, id_rsa, ~/.aws
|
|
125
|
+
2 destructive commands rm -rf, git push --force
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
A skill with nothing notable says so:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
contains no shell commands, network calls or credential references
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**This is disclosure, not a verdict.** It counts and names; it never scores and never prints
|
|
135
|
+
"safe". That distinction is deliberate. A built-in matcher that declared a skill safe would be
|
|
136
|
+
worse than no check, and the numbers below show why. Counting stays honest at any false-positive
|
|
137
|
+
rate because it makes no claim.
|
|
138
|
+
|
|
139
|
+
### What runs when available: SkillSpector
|
|
140
|
+
|
|
113
141
|
**It reports, it does not block.** That is a measured decision, not a shortcut.
|
|
114
142
|
|
|
115
143
|
Scanning 18 known-good skills already installed on a dev machine (2026-08-08, SkillSpector
|
|
@@ -121,10 +149,16 @@ v2.8.1, `--no-llm`):
|
|
|
121
149
|
| CAUTION | 9 |
|
|
122
150
|
| **DO_NOT_INSTALL** | **8 (44%)** |
|
|
123
151
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
152
|
+
The failure mode is worth understanding, because it is not random. A skill that *documents* attack
|
|
153
|
+
patterns gets flagged for *containing* them: the static matcher has no negation handling, so a rule
|
|
154
|
+
saying "reject skills that bypass safety checks" reads identically to a skill saying "bypass safety
|
|
155
|
+
checks".
|
|
156
|
+
|
|
157
|
+
[SKILLmama](https://github.com/Magithar/SKILLmama) was a worked example. Its security checklist
|
|
158
|
+
scored `CRITICAL` until four DISCARD criteria were reworded to say the same thing in different
|
|
159
|
+
words; it now scores `7 / LOW / SAFE`. Its behaviour never changed, only its vocabulary. Adding an
|
|
160
|
+
explicit user-consent section was also tested and made the score **worse**, because describing a
|
|
161
|
+
safeguard trips the same patterns as describing the danger.
|
|
128
162
|
|
|
129
163
|
A gate that blocks 44% of legitimate skills does not make anyone safer; it teaches people to pass
|
|
130
164
|
the override flag reflexively. So findings are surfaced with file and line, and the decision stays
|
|
@@ -176,6 +210,22 @@ $ skill-land product-on-purpose/pm-skills --for codex
|
|
|
176
210
|
...
|
|
177
211
|
```
|
|
178
212
|
|
|
213
|
+
## Releasing
|
|
214
|
+
|
|
215
|
+
Published from CI by [trusted publishing](https://docs.npmjs.com/trusted-publishers), so there is
|
|
216
|
+
no `NPM_TOKEN` in this repo and the npm account keeps 2FA on write actions. npm mints a
|
|
217
|
+
short-lived token from the GitHub identity of `.github/workflows/publish-skill-land.yml`.
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
# bump version in package.json, commit, then:
|
|
221
|
+
git tag skill-land-v1.0.2
|
|
222
|
+
git push origin skill-land-v1.0.2
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
CI runs the tests, refuses to publish a version that already exists, checks the tag matches
|
|
226
|
+
`package.json`, and publishes. The tag is package-scoped so other tools in this repo can release
|
|
227
|
+
independently.
|
|
228
|
+
|
|
179
229
|
## Adding an agent
|
|
180
230
|
|
|
181
231
|
Edit `agents.json`. `broken: true` marks agents where the upstream CLI misroutes a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skill-land",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Install an agent skill to the directory the agent actually reads, then verify it landed. Exits non-zero if it didn't.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-skills",
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
"bin": {
|
|
28
28
|
"skill-land": "skill-land.mjs"
|
|
29
29
|
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"test": "node test.mjs"
|
|
32
|
+
},
|
|
30
33
|
"files": [
|
|
31
34
|
"skill-land.mjs",
|
|
32
35
|
"agents.json",
|
package/skill-land.mjs
CHANGED
|
@@ -141,6 +141,57 @@ function parseFrontmatter(text) {
|
|
|
141
141
|
|
|
142
142
|
// ---------------------------------------------------------------- security
|
|
143
143
|
|
|
144
|
+
// A skill is instructions an agent executes with full permissions, so the one
|
|
145
|
+
// check worth always running is showing what is actually in the file.
|
|
146
|
+
//
|
|
147
|
+
// This is disclosure, not a verdict: it counts and quotes, it never scores.
|
|
148
|
+
// That distinction is the point. A built-in pattern-matcher that printed
|
|
149
|
+
// "SAFE" would be worse than nothing, and we measured why: SkillSpector's
|
|
150
|
+
// static mode calls 44% of known-good skills DO_NOT_INSTALL. Counting is
|
|
151
|
+
// honest at any false-positive rate because it makes no claim.
|
|
152
|
+
//
|
|
153
|
+
// SkillSpector, when installed, does the judging. This does the showing, and
|
|
154
|
+
// it has no dependencies so it runs for everyone.
|
|
155
|
+
// Each entry reduces its matches to a short, stable label (the binary, the
|
|
156
|
+
// host) rather than quoting raw text, so the output stays readable and does
|
|
157
|
+
// not leak half-sentences from prose that happens to contain a keyword.
|
|
158
|
+
const DISCLOSE = [
|
|
159
|
+
{
|
|
160
|
+
label: "shell commands",
|
|
161
|
+
re: /\b(sudo|rm|mv|cp|chmod|chown|curl|wget|git|npm|npx|pip|uv|docker|kubectl|ssh|scp|eval|bash|sh)\s+(?:-{1,2}[\w-]+|[\w./~$@-]+)/g,
|
|
162
|
+
key: (m) => m[1],
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
label: "network hosts",
|
|
166
|
+
re: /https?:\/\/([^\s/"'`)>\]]+)/g,
|
|
167
|
+
key: (m) => m[1],
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
label: "credential refs",
|
|
171
|
+
re: /(~\/\.ssh|~\/\.aws|\.env(?:\.[a-z]+)?|id_rsa|[A-Z][A-Z_]*(?:API_KEY|SECRET|TOKEN|PASSWORD)[A-Z_]*)/g,
|
|
172
|
+
key: (m) => m[1],
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
label: "destructive commands",
|
|
176
|
+
re: /(rm\s+-[rf]{1,2}|git\s+push\s+--force|push\s+-f\b|DROP\s+(?:TABLE|DATABASE)|TRUNCATE)/gi,
|
|
177
|
+
key: (m) => m[1].replace(/\s+/g, " ").toLowerCase(),
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
function disclose(text) {
|
|
182
|
+
const out = [];
|
|
183
|
+
for (const { label, re, key } of DISCLOSE) {
|
|
184
|
+
const keys = [...text.matchAll(re)].map(key).filter(Boolean);
|
|
185
|
+
if (!keys.length) continue;
|
|
186
|
+
// Rank by frequency so the most-used thing is what you see first.
|
|
187
|
+
const freq = new Map();
|
|
188
|
+
for (const k of keys) freq.set(k, (freq.get(k) || 0) + 1);
|
|
189
|
+
const top = [...freq.entries()].sort((a, b) => b[1] - a[1]).map(([k]) => k);
|
|
190
|
+
out.push({ label, count: keys.length, distinct: top.length, sample: top.slice(0, 4) });
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
|
|
144
195
|
// Skills run with full agent permissions, and this tool writes a file fetched
|
|
145
196
|
// from a URL into a directory an agent auto-loads. So scan before writing.
|
|
146
197
|
//
|
|
@@ -304,6 +355,7 @@ try {
|
|
|
304
355
|
|
|
305
356
|
// Scan before any write. A blocked skill must never reach disk.
|
|
306
357
|
const scan = skipSecurity ? { status: "skipped" } : securityScan(skillPath);
|
|
358
|
+
const contents = disclose(rawBody.toString("utf-8"));
|
|
307
359
|
const findings = (scan.top || []).length
|
|
308
360
|
? "\n" + scan.top.map((t) => ` ${t}`).join("\n") : "";
|
|
309
361
|
// Reported, never applied. See securityScan().
|
|
@@ -323,7 +375,19 @@ try {
|
|
|
323
375
|
console.log(`\n source ${src.label}`);
|
|
324
376
|
console.log(` skill ${fm.name || "(no frontmatter name)"} [${rawBody.length} bytes, sha ${expected.hash.slice(0,12)}]`);
|
|
325
377
|
console.log(` from ${skillPath.replace(root, ".")}`);
|
|
326
|
-
if (!verifyOnly)
|
|
378
|
+
if (!verifyOnly) {
|
|
379
|
+
// Always shown, with or without a scanner. Counts and quotes only.
|
|
380
|
+
if (contents.length) {
|
|
381
|
+
console.log(" contains");
|
|
382
|
+
for (const c of contents) {
|
|
383
|
+
const more = c.distinct > c.sample.length ? `, +${c.distinct - c.sample.length} more` : "";
|
|
384
|
+
console.log(` ${String(c.count).padStart(3)} ${c.label.padEnd(21)} ${c.sample.join(", ")}${more}`.slice(0, 100));
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
console.log(" contains no shell commands, network calls or credential references");
|
|
388
|
+
}
|
|
389
|
+
console.log(SCAN_LINE[scan.status]());
|
|
390
|
+
}
|
|
327
391
|
console.log("");
|
|
328
392
|
|
|
329
393
|
// Refuse to write a skill the scanner flagged. --force is an explicit,
|