@mutmutco/claude-plugin 4.0.3 → 4.0.4
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/.claude-plugin/plugin.json +2 -2
- package/bin/mmi-cli +0 -0
- package/bin/mmi-hook +0 -0
- package/bin/mmi-hook-console.cmd +0 -0
- package/package.json +1 -1
- package/scripts/pretooluse-shell-gates.mjs +62 -4
package/bin/mmi-cli
CHANGED
|
File without changes
|
package/bin/mmi-hook
CHANGED
|
File without changes
|
package/bin/mmi-hook-console.cmd
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// then command-ladder. The broad shell-dialect advisory remains retired.
|
|
5
5
|
import { execFileSync } from 'node:child_process';
|
|
6
6
|
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
-
import { resolve } from 'node:path';
|
|
7
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
8
8
|
import { analyze as analyzeSecretEcho } from './secret-echo-lint.mjs';
|
|
9
9
|
import { analyze as analyzeEnvWrite } from './env-write-lint.mjs';
|
|
10
10
|
import { decide as decideCommandLadder, matchedVerb } from './command-ladder-gate.mjs';
|
|
@@ -43,9 +43,45 @@ function preToolUseDeny(reason) {
|
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/** Commands whose heredoc body IS executed, so its text must stay under analysis. Deliberately a
|
|
47
|
+
* small allowlist matched by executable NAME: anything unrecognised counts as an interpreter and
|
|
48
|
+
* keeps its body scanned, so an unknown consumer can never become a bypass (#5266). */
|
|
49
|
+
const HEREDOC_DATA_CONSUMERS = new Set(['git', 'cat', 'tee', 'mmi-cli', 'jerv-cli', 'gh', 'jq', 'grep', 'sed', 'diff', 'sort', 'wc', 'head', 'tail']);
|
|
50
|
+
|
|
51
|
+
/** Blank out heredoc BODIES that are data rather than script (#5266).
|
|
52
|
+
*
|
|
53
|
+
* Segmentation splits on `;|&` and newlines, so a heredoc body — a commit message, a PR body, an
|
|
54
|
+
* issue report — was chopped into pseudo-segments and analysed as if it were a command. Prose that
|
|
55
|
+
* merely QUOTED a test runner was therefore refused as an attempt to run tests, which penalised
|
|
56
|
+
* writing accurate evidence about test behaviour: the exact opposite of what #5264 was fixing.
|
|
57
|
+
*
|
|
58
|
+
* Bodies are replaced with equal-length blanks, never deleted, so every offset the analysers and
|
|
59
|
+
* the segment-ordinal reporting depend on is preserved. A body is only blanked when the command
|
|
60
|
+
* introducing it is a known data consumer; `sh <<EOF … EOF` genuinely executes its body and stays
|
|
61
|
+
* fully analysed. */
|
|
62
|
+
function maskHeredocData(source) {
|
|
63
|
+
const lines = source.split('\n');
|
|
64
|
+
const out = [...lines];
|
|
65
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
66
|
+
const intro = /<<-?\s*(?:'([^']+)'|"([^"]+)"|([A-Za-z_][A-Za-z0-9_]*))/.exec(lines[i]);
|
|
67
|
+
if (!intro) continue;
|
|
68
|
+
const delimiter = intro[1] ?? intro[2] ?? intro[3];
|
|
69
|
+
// The consumer is the command the redirection attaches to — the LAST one before `<<`, not the
|
|
70
|
+
// first on the line. `cd <dir> && git commit -F - <<EOF` is consumed by git, not by cd.
|
|
71
|
+
const consumer = /(?:^|[;|&])\s*([^\s;|&]+)[^;|&]*$/.exec(lines[i].slice(0, intro.index));
|
|
72
|
+
if (!HEREDOC_DATA_CONSUMERS.has(executableName(consumer?.[1]))) continue;
|
|
73
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
74
|
+
if (lines[j].trim() === delimiter) { i = j; break; }
|
|
75
|
+
out[j] = ' '.repeat(lines[j].length);
|
|
76
|
+
if (j === lines.length - 1) i = j;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return out.join('\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
46
82
|
/** Split only on unquoted compound-command operators. Text is retained solely for analyzers and is never surfaced. */
|
|
47
83
|
function boundedShellSegments(command) {
|
|
48
|
-
const source = String(command ?? '');
|
|
84
|
+
const source = maskHeredocData(String(command ?? ''));
|
|
49
85
|
const segments = [];
|
|
50
86
|
let start = 0;
|
|
51
87
|
let quote = null;
|
|
@@ -293,8 +329,24 @@ function git(root, args) {
|
|
|
293
329
|
});
|
|
294
330
|
}
|
|
295
331
|
|
|
332
|
+
/** The directory the COMMAND will run in, when it names one itself (#5264).
|
|
333
|
+
*
|
|
334
|
+
* A compound command routinely opens with `cd <path> && …`, and the host's `cwd` is the session's,
|
|
335
|
+
* not the command's. Judging `cd <other-repo-worktree> && npm test` by the session cwd evaluates a
|
|
336
|
+
* DIFFERENT repository — its test-policy.json and its (typically clean) diff — and then states a
|
|
337
|
+
* conclusion about the repo it never looked at. Reuses the same bounded segmentation the gate
|
|
338
|
+
* already trusts, and only ever feeds a read-only `git -C` probe. */
|
|
339
|
+
function commandWorkingDirectory(command, base) {
|
|
340
|
+
const [first] = boundedShellSegments(command);
|
|
341
|
+
const match = /^cd\s+(?!-)(?:"([^"]+)"|'([^']+)'|([^\s;&|]+))\s*$/.exec(first?.text ?? '');
|
|
342
|
+
const target = match?.[1] ?? match?.[2] ?? match?.[3];
|
|
343
|
+
if (!target || target.startsWith('~')) return null;
|
|
344
|
+
const resolved = isAbsolute(target) ? target : (typeof base === 'string' && base ? resolve(base, target) : null);
|
|
345
|
+
return resolved && existsSync(resolved) ? resolved : null;
|
|
346
|
+
}
|
|
347
|
+
|
|
296
348
|
function repositoryRoot(input) {
|
|
297
|
-
const candidates = [input?.cwd, process.cwd()]
|
|
349
|
+
const candidates = [commandWorkingDirectory(input?.tool_input?.command, input?.cwd), input?.cwd, process.cwd()]
|
|
298
350
|
.filter((cwd, index, values) => typeof cwd === 'string' && cwd && values.indexOf(cwd) === index);
|
|
299
351
|
for (const cwd of candidates) {
|
|
300
352
|
try {
|
|
@@ -372,8 +424,14 @@ function runTestCommandPolicy(input, { stdout = process.stdout } = {}) {
|
|
|
372
424
|
stdout.write(preToolUseDeny(reason) + '\n');
|
|
373
425
|
return { denied: true };
|
|
374
426
|
}
|
|
427
|
+
// #5264: name the repository this verdict was computed from. The refusal text is authoritative and
|
|
428
|
+
// gets pasted into PR bodies as verification, so a bare "the current diff" — with no statement of
|
|
429
|
+
// WHICH diff — reads as a claim about the repo the author is working in even when the gate resolved
|
|
430
|
+
// a different one. Naming the root makes a wrong resolution self-evident instead of quotable.
|
|
375
431
|
const reason = 'TEST-POLICY TEST COMMAND REFUSED [test-command-outside-mandatory-zone]: '
|
|
376
|
-
+ '
|
|
432
|
+
+ `no path in ${root}'s task diff matches a mandatory glob in its test-policy.json. `
|
|
433
|
+
+ 'Verify that is the repository you meant before quoting this: it is resolved from the command\'s own `cd`, then the host cwd. '
|
|
434
|
+
+ 'Do not run tests; use policy-approved non-test verification, '
|
|
377
435
|
+ 'or touch and run mandatory-zone coverage only when the diff actually requires it.';
|
|
378
436
|
appendHookActivity({ event: 'PreToolUse', script: TEST_COMMAND_GATE_NAME, outcome: 'deny', action: reason, reasonId: 'test-command-outside-mandatory-zone', tool: input?.tool_name });
|
|
379
437
|
stdout.write(preToolUseDeny(reason) + '\n');
|