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 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
+ [![npm](https://img.shields.io/npm/v/skill-land)](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
- SKILLmama's own `SKILL.md` scores **100 / CRITICAL / DO_NOT_INSTALL**, because its Phase 3.7
125
- DISCARD rules contain the literal phrase *"instructions to bypass safety checks"* and the pattern
126
- matcher reads that as an anti-refusal attack. A security checklist gets flagged for describing
127
- the attacks it checks for.
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.0.0",
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) console.log(SCAN_LINE[scan.status]());
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,