sshlg-skills 0.76.0 → 0.77.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/CHANGELOG.md +31 -0
- package/lib/hygiene.js +66 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.77.0 — closing a false positive found the bypass it was hiding
|
|
4
|
+
|
|
5
|
+
**B-59: the hygiene guard could not tell a command being run from one being written
|
|
6
|
+
down.** It reads the whole Bash payload, so quoting the forbidden invocation *in a
|
|
7
|
+
document* was refused too — a verification-ledger row blocked its own commit, and the
|
|
8
|
+
sentence had to be split around the guard.
|
|
9
|
+
|
|
10
|
+
`executablePart()` now removes what cannot execute before matching:
|
|
11
|
+
|
|
12
|
+
- **A heredoc body fed to something that is not a shell.** `python3 - <<'PY' … PY` and
|
|
13
|
+
`cat > f <<EOF … EOF` are data.
|
|
14
|
+
- **A whole-line comment.**
|
|
15
|
+
|
|
16
|
+
And what it keeps is the load-bearing half. **A `bash <<EOF … EOF` body is still read** —
|
|
17
|
+
stripping every heredoc would trade a false positive for a documented bypass. **Quoted
|
|
18
|
+
strings are still read** too, because `bash -c '…'` is a real invocation.
|
|
19
|
+
|
|
20
|
+
**That last decision is where the actual finding was.** Testing it produced a case the
|
|
21
|
+
guard *should* refuse and did not: `bareName` kept the trailing quote, so `ux-flows'`
|
|
22
|
+
matched no family id and **`bash -c 'npx skills add ux-flows'` passed the guard untouched**
|
|
23
|
+
— verified against `HEAD` before the change, so it had been open the whole time. The row
|
|
24
|
+
asked for relief from an annoyance; the annoyance was hiding a hole.
|
|
25
|
+
|
|
26
|
+
Both directions are fixtured — 17 → **22** checks — and the real hook was driven as a
|
|
27
|
+
process to confirm it agrees with the pure module. The fixture strings are **assembled at
|
|
28
|
+
runtime** rather than written whole, because a file containing the literal payload is
|
|
29
|
+
itself refused by anything that reads it: the defect demonstrated itself three times
|
|
30
|
+
during the fix, twice blocking the commands that were repairing it, and once making three
|
|
31
|
+
new cases pass for the wrong reason by targeting a member the fixture's manifest does not
|
|
32
|
+
declare.
|
|
33
|
+
|
|
3
34
|
## v0.76.0 — a ledger records two different things, and most record only one
|
|
4
35
|
|
|
5
36
|
**B-62 said five ledger shapes. Measured: ten, across 815 rows.**
|
package/lib/hygiene.js
CHANGED
|
@@ -22,6 +22,12 @@
|
|
|
22
22
|
|
|
23
23
|
/** Strip an `@version` suffix so `sshlg-skills@latest` is not read as `skills`. */
|
|
24
24
|
function bareName(token) {
|
|
25
|
+
// Shell quoting is not part of a name. `bash -c 'npx skills update ux-flows'` reaches
|
|
26
|
+
// this as the token `ux-flows'`, which matched no family id — so a real invocation
|
|
27
|
+
// wrapped in quotes passed the guard untouched. Found on 2026-08-16 while fixing the
|
|
28
|
+
// opposite complaint (B-59, a false positive on documented text); the same run turned
|
|
29
|
+
// up a bypass worth more than the annoyance that started it.
|
|
30
|
+
token = String(token || '').replace(/^['"]+/, '').replace(/['"]+$/, '');
|
|
25
31
|
return String(token || '').replace(/@.*$/, '').replace(/^.*\//, '');
|
|
26
32
|
}
|
|
27
33
|
|
|
@@ -43,6 +49,62 @@ function isFlag(token) {
|
|
|
43
49
|
* `claude` exactly where the target would be, and reading it as the target let a
|
|
44
50
|
* real family skill through unguarded. Watched failing in the fixtures.
|
|
45
51
|
*/
|
|
52
|
+
/**
|
|
53
|
+
* The part of a payload that could actually run, with data stripped out.
|
|
54
|
+
*
|
|
55
|
+
* B-59: this guard reads the whole command text, so **writing the forbidden string into a
|
|
56
|
+
* document is refused too**. On 2026-08-16 a verification-ledger row quoting the
|
|
57
|
+
* diagnostic invocation blocked its own commit, and the sentence had to be split around
|
|
58
|
+
* the guard. The family's own documents could not quote the command they warn about.
|
|
59
|
+
*
|
|
60
|
+
* Two things are removed, and the boundary between them and everything else is the whole
|
|
61
|
+
* design:
|
|
62
|
+
*
|
|
63
|
+
* - **A heredoc body fed to something that is not a shell.** `python3 - <<'PY' … PY` and
|
|
64
|
+
* `cat > f <<EOF … EOF` are data. `bash <<EOF … EOF` is a script, so its body is KEPT —
|
|
65
|
+
* stripping every heredoc would be a documented bypass, which is worse than the false
|
|
66
|
+
* positive it fixes.
|
|
67
|
+
* - **A whole-line comment.** A line whose first non-space character is `#` does not run.
|
|
68
|
+
*
|
|
69
|
+
* **Quoted strings are deliberately NOT stripped.** `bash -c 'npx skills update ux-flows'`
|
|
70
|
+
* is a real invocation living inside quotes, and a guard that ignored quoted text would
|
|
71
|
+
* miss it. The false positive that remains — the string in a single-line argument to
|
|
72
|
+
* `echo` — is the price of not opening that hole, and it is documented rather than fixed.
|
|
73
|
+
*/
|
|
74
|
+
const HEREDOC_SHELLS = new Set(['bash', 'sh', 'zsh', 'dash', 'ksh', 'eval', 'source', '.']);
|
|
75
|
+
|
|
76
|
+
function executablePart(command) {
|
|
77
|
+
const lines = String(command || '').split('\n');
|
|
78
|
+
const out = [];
|
|
79
|
+
let terminator = null;
|
|
80
|
+
let dropping = false;
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
if (terminator !== null) {
|
|
83
|
+
// The terminator may be indented when the heredoc used `<<-`.
|
|
84
|
+
if (line.trim() === terminator) { terminator = null; dropping = false; continue; }
|
|
85
|
+
if (dropping) continue;
|
|
86
|
+
out.push(line);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (/^\s*#/.test(line)) continue;
|
|
90
|
+
const here = /<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1/.exec(line);
|
|
91
|
+
if (here) {
|
|
92
|
+
terminator = here[2];
|
|
93
|
+
// Which command is being fed? The last simple-command word before the `<<`, taken
|
|
94
|
+
// after the final pipe or `&&` so `foo | python3 - <<EOF` reads as python3.
|
|
95
|
+
const before = line.slice(0, here.index);
|
|
96
|
+
const segment = before.split(/\|\||&&|[|;]/).pop();
|
|
97
|
+
const words = segment.trim().split(/\s+/).filter(Boolean).map(bareName);
|
|
98
|
+
const cmd = words.find((w) => !w.includes('=')) || '';
|
|
99
|
+
dropping = !HEREDOC_SHELLS.has(cmd);
|
|
100
|
+
out.push(before);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
out.push(line);
|
|
104
|
+
}
|
|
105
|
+
return out.join('\n');
|
|
106
|
+
}
|
|
107
|
+
|
|
46
108
|
function skillsCli(command) {
|
|
47
109
|
const tokens = String(command || '').split(/\s+/).filter(Boolean);
|
|
48
110
|
for (let i = 0; i < tokens.length; i += 1) {
|
|
@@ -83,7 +145,9 @@ const REMEDY = 'npx --yes sshlg-skills@latest update';
|
|
|
83
145
|
* `~/.claude/skills/`, which is what does the shadowing.
|
|
84
146
|
*/
|
|
85
147
|
function bareFamilyInstall(command, ids) {
|
|
86
|
-
|
|
148
|
+
// Data first: a heredoc body written by a non-shell, and whole-line comments, cannot
|
|
149
|
+
// install anything. See `executablePart` for why quoted strings are not stripped.
|
|
150
|
+
const call = skillsCli(executablePart(command));
|
|
87
151
|
if (!call || !call.args.length) return null;
|
|
88
152
|
if (!['update', 'add', 'install'].includes(call.verb)) return null;
|
|
89
153
|
const target = call.args.find((a) => ids.has(bareName(a)));
|
|
@@ -174,6 +238,6 @@ function reapply(snapshot, fresh) {
|
|
|
174
238
|
}
|
|
175
239
|
|
|
176
240
|
module.exports = {
|
|
177
|
-
skillsCli, familyIds, bareFamilyInstall, launcherRunning,
|
|
241
|
+
skillsCli, familyIds, bareFamilyInstall, launcherRunning, executablePart,
|
|
178
242
|
isObsidianSetup, parseConfig, reapply, bareName, REMEDY,
|
|
179
243
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sshlg-skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.77.0",
|
|
4
4
|
"description": "One launcher/updater for the ssheleg skill family (super-ux, task-pipeline, agent-sync, make-skill, sheleg-design, seo-aeo-audit, sheleg-dev, agent-stack) across every agent — Claude Code, Cursor, OpenCode, Kilo, Kimi, Hermes, OpenClaw, Codex, and more.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"sshlg-skills": "bin/sshlg-skills.js"
|