@massu/core 2.5.5 → 2.5.6
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 +33 -0
- package/dist/cli.js +69 -42
- package/dist/hooks/classify-failure.js +27 -654
- package/dist/hooks/fix-detector.js +4 -2
- package/dist/hooks/pre-delete-check.js +379 -69
- package/dist/hooks/pre-tool-use-gate.js +170 -221
- package/dist/hooks/security-gate.js +544 -179
- package/dist/hooks/session-end.js +470 -63
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,39 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
10
10
|
> redistributed under the **Apache License 2.0**. Full license + NOTICE:
|
|
11
11
|
> `packages/core/assets/embedder/MODEL-LICENSE`.
|
|
12
12
|
|
|
13
|
+
## [2.5.6] - 2026-11-09
|
|
14
|
+
|
|
15
|
+
### Security
|
|
16
|
+
|
|
17
|
+
- **The safety gate blocks again.** In 2.5.2 through 2.5.5 the PreToolUse gate that `massu init`
|
|
18
|
+
installs warned about everything it should have blocked and then let the call run: commands
|
|
19
|
+
that read `.env` and other secret files, piping a download into a shell, `rm -rf /`, disk
|
|
20
|
+
formatting and writes to `/etc/passwd`. Claude Code blocks a tool call only when the hook exits
|
|
21
|
+
with code 2; the gate printed "BLOCKED" and exited 0. It now exits 2 for every blocking
|
|
22
|
+
finding. **Upgrade, then run `massu init` again** so your settings pick up the change below.
|
|
23
|
+
- **A gate that fails now blocks.** `massu init` sets Claude Code's `"onFailure": "block"`
|
|
24
|
+
(Claude Code 2.1.295 or later) on the gate, so a gate that crashes, times out or prints invalid
|
|
25
|
+
output blocks the call instead of letting it through. On older Claude Code, the `massu-hook`
|
|
26
|
+
launcher and `hook-runner` turn a crashed gate into a block.
|
|
27
|
+
- **The older hook names decide too.** Settings written before 1.12.0 may still name the
|
|
28
|
+
`security-gate` and `pre-delete-check` hooks. They used to warn only; they now make the same
|
|
29
|
+
decision as the gate.
|
|
30
|
+
- **More secret files are recognised:** secrets data files under any name, such as
|
|
31
|
+
`config.secrets.yaml` or `app.secret.json`. Source files such as `secrets.ts` are still allowed.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **The gate no longer refuses good calls under load.** A slow-arriving tool call could be
|
|
36
|
+
denied as "Unexpected end of JSON input" and pass on retry. The gate now reads its whole
|
|
37
|
+
input before deciding.
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- **`massu_cost_feature` reports real data.** Each session's priced cost is split across the
|
|
42
|
+
registered features whose files it touched, in proportion to those touches; activity on files
|
|
43
|
+
that belong to no registered feature is shown as `(no registered feature)`, so the rows add up
|
|
44
|
+
to the sessions' total. Sessions whose cost is unknown are left out.
|
|
45
|
+
|
|
13
46
|
## [2.5.5] - 2026-11-09
|
|
14
47
|
|
|
15
48
|
### Fixed
|
package/dist/cli.js
CHANGED
|
@@ -5893,6 +5893,42 @@ var init_hook_timeouts = __esm({
|
|
|
5893
5893
|
}
|
|
5894
5894
|
});
|
|
5895
5895
|
|
|
5896
|
+
// src/lib/hook-registry.ts
|
|
5897
|
+
function getExpectedHookFiles() {
|
|
5898
|
+
return REGISTERED_HOOKS.map((name) => `${name}.js`);
|
|
5899
|
+
}
|
|
5900
|
+
var REGISTERED_HOOKS, DECISION_HOOKS;
|
|
5901
|
+
var init_hook_registry = __esm({
|
|
5902
|
+
"src/lib/hook-registry.ts"() {
|
|
5903
|
+
"use strict";
|
|
5904
|
+
REGISTERED_HOOKS = [
|
|
5905
|
+
"auto-learning-pipeline",
|
|
5906
|
+
"classify-failure",
|
|
5907
|
+
"cost-tracker",
|
|
5908
|
+
"fix-detector",
|
|
5909
|
+
"incident-pipeline",
|
|
5910
|
+
"intent-suggester",
|
|
5911
|
+
"memory-recall",
|
|
5912
|
+
"post-edit-context",
|
|
5913
|
+
"post-tool-use",
|
|
5914
|
+
"pre-compact",
|
|
5915
|
+
// P-E-019 (1.12.0): security-gate + pre-delete-check are kept registered
|
|
5916
|
+
// for back-compat (operator settings.json may still reference them
|
|
5917
|
+
// individually), but new installs emit only `pre-tool-use-gate` —
|
|
5918
|
+
// the consolidated single-spawn PreToolUse hook.
|
|
5919
|
+
"pre-delete-check",
|
|
5920
|
+
"pre-tool-use-gate",
|
|
5921
|
+
"quality-event",
|
|
5922
|
+
"rule-enforcement-pipeline",
|
|
5923
|
+
"security-gate",
|
|
5924
|
+
"session-end",
|
|
5925
|
+
"session-start",
|
|
5926
|
+
"user-prompt"
|
|
5927
|
+
];
|
|
5928
|
+
DECISION_HOOKS = ["pre-delete-check", "pre-tool-use-gate", "security-gate"];
|
|
5929
|
+
}
|
|
5930
|
+
});
|
|
5931
|
+
|
|
5896
5932
|
// src/detect/manifest-registry.ts
|
|
5897
5933
|
var manifest_registry_exports = {};
|
|
5898
5934
|
__export(manifest_registry_exports, {
|
|
@@ -17102,6 +17138,22 @@ fi
|
|
|
17102
17138
|
MASSU_VERSION="$1"
|
|
17103
17139
|
shift
|
|
17104
17140
|
MASSU_CLI="\${HOME}/.massu/runtime/\${MASSU_VERSION}/node_modules/@massu/core/dist/cli.js"
|
|
17141
|
+
# A DECIDING hook's exit code is its verdict: 0 allows, 2 denies. If the launch itself fails
|
|
17142
|
+
# (a runtime that cannot load exits 1), Claude Code would RUN the call, so for these hooks
|
|
17143
|
+
# any other status becomes a deny. Every other subcommand keeps the exec path below.
|
|
17144
|
+
if [ "\${1:-}" = hook-runner ]; then
|
|
17145
|
+
case "\${2:-}" in
|
|
17146
|
+
${DECISION_HOOKS.join("|")})
|
|
17147
|
+
if [ -f "$MASSU_CLI" ]; then node "$MASSU_CLI" "$@"; else npx -y "@massu/core@\${MASSU_VERSION}" "$@"; fi
|
|
17148
|
+
st=$?
|
|
17149
|
+
if [ "$st" -ne 0 ] && [ "$st" -ne 2 ]; then
|
|
17150
|
+
echo "MASSU SECURITY GATE \u2014 BLOCKED: the $2 hook ended without a verdict (exit $st)" >&2
|
|
17151
|
+
exit 2
|
|
17152
|
+
fi
|
|
17153
|
+
exit "$st"
|
|
17154
|
+
;;
|
|
17155
|
+
esac
|
|
17156
|
+
fi
|
|
17105
17157
|
if [ -f "$MASSU_CLI" ]; then
|
|
17106
17158
|
exec node "$MASSU_CLI" "$@"
|
|
17107
17159
|
fi
|
|
@@ -17149,6 +17201,7 @@ function getHookTimeout(name) {
|
|
|
17149
17201
|
function hookEntry(version, name, shim) {
|
|
17150
17202
|
const entry = { type: "command", command: hookCmd(version, name, shim), timeout: getHookTimeout(name) };
|
|
17151
17203
|
if (BACKGROUND_HOOKS.has(name)) entry.async = true;
|
|
17204
|
+
if (DECISION_HOOKS.includes(name)) entry.onFailure = "block";
|
|
17152
17205
|
return entry;
|
|
17153
17206
|
}
|
|
17154
17207
|
function buildHooksConfig(_hooksDir, shim = resolveMassuShim()) {
|
|
@@ -17705,6 +17758,7 @@ var init_init = __esm({
|
|
|
17705
17758
|
init_settings_local();
|
|
17706
17759
|
init_memory_path();
|
|
17707
17760
|
init_hook_timeouts();
|
|
17761
|
+
init_hook_registry();
|
|
17708
17762
|
init_detect();
|
|
17709
17763
|
init_drift();
|
|
17710
17764
|
__filename2 = fileURLToPath3(import.meta.url);
|
|
@@ -18291,41 +18345,6 @@ var init_license = __esm({
|
|
|
18291
18345
|
}
|
|
18292
18346
|
});
|
|
18293
18347
|
|
|
18294
|
-
// src/lib/hook-registry.ts
|
|
18295
|
-
function getExpectedHookFiles() {
|
|
18296
|
-
return REGISTERED_HOOKS.map((name) => `${name}.js`);
|
|
18297
|
-
}
|
|
18298
|
-
var REGISTERED_HOOKS;
|
|
18299
|
-
var init_hook_registry = __esm({
|
|
18300
|
-
"src/lib/hook-registry.ts"() {
|
|
18301
|
-
"use strict";
|
|
18302
|
-
REGISTERED_HOOKS = [
|
|
18303
|
-
"auto-learning-pipeline",
|
|
18304
|
-
"classify-failure",
|
|
18305
|
-
"cost-tracker",
|
|
18306
|
-
"fix-detector",
|
|
18307
|
-
"incident-pipeline",
|
|
18308
|
-
"intent-suggester",
|
|
18309
|
-
"memory-recall",
|
|
18310
|
-
"post-edit-context",
|
|
18311
|
-
"post-tool-use",
|
|
18312
|
-
"pre-compact",
|
|
18313
|
-
// P-E-019 (1.12.0): security-gate + pre-delete-check are kept registered
|
|
18314
|
-
// for back-compat (operator settings.json may still reference them
|
|
18315
|
-
// individually), but new installs emit only `pre-tool-use-gate` —
|
|
18316
|
-
// the consolidated single-spawn PreToolUse hook.
|
|
18317
|
-
"pre-delete-check",
|
|
18318
|
-
"pre-tool-use-gate",
|
|
18319
|
-
"quality-event",
|
|
18320
|
-
"rule-enforcement-pipeline",
|
|
18321
|
-
"security-gate",
|
|
18322
|
-
"session-end",
|
|
18323
|
-
"session-start",
|
|
18324
|
-
"user-prompt"
|
|
18325
|
-
];
|
|
18326
|
-
}
|
|
18327
|
-
});
|
|
18328
|
-
|
|
18329
18348
|
// src/preflight.ts
|
|
18330
18349
|
import { existsSync as existsSync22 } from "fs";
|
|
18331
18350
|
function assertCodegraphUsable(dbPath) {
|
|
@@ -18412,6 +18431,7 @@ var init_preflight = __esm({
|
|
|
18412
18431
|
var hook_runner_exports = {};
|
|
18413
18432
|
__export(hook_runner_exports, {
|
|
18414
18433
|
HOOK_NAME_TO_FILE: () => HOOK_NAME_TO_FILE,
|
|
18434
|
+
decisionExitCode: () => decisionExitCode,
|
|
18415
18435
|
resolveHookFile: () => resolveHookFile,
|
|
18416
18436
|
runHookRunner: () => runHookRunner
|
|
18417
18437
|
});
|
|
@@ -18443,6 +18463,16 @@ function resolveHookFile(hookName) {
|
|
|
18443
18463
|
`Hook file not found for "${hookName}". Searched: ${candidates.join(", ")}. This indicates a broken @massu/core install. Re-run \`npx -y @massu/core init\`.`
|
|
18444
18464
|
);
|
|
18445
18465
|
}
|
|
18466
|
+
function decisionExitCode(hookName, code) {
|
|
18467
|
+
if (!DECISION_HOOKS.includes(hookName) || code === 0 || code === 2) return code;
|
|
18468
|
+
process.stderr.write(
|
|
18469
|
+
`MASSU SECURITY GATE \u2014 BLOCKED
|
|
18470
|
+
|
|
18471
|
+
The ${hookName} hook ended without a verdict (exit ${code}). A gate that did not decide has not approved this call.
|
|
18472
|
+
`
|
|
18473
|
+
);
|
|
18474
|
+
return 2;
|
|
18475
|
+
}
|
|
18446
18476
|
async function runHookRunner(args2) {
|
|
18447
18477
|
const hookName = args2[0];
|
|
18448
18478
|
if (!hookName) {
|
|
@@ -18471,11 +18501,7 @@ Recognized: ${Object.keys(HOOK_NAME_TO_FILE).join(", ")}
|
|
|
18471
18501
|
env: { ...process.env, MASSU_HOOK_RUNTIME: "1" }
|
|
18472
18502
|
});
|
|
18473
18503
|
child.on("exit", (code, signal) => {
|
|
18474
|
-
|
|
18475
|
-
resolvePromise({ exitCode: 128 });
|
|
18476
|
-
return;
|
|
18477
|
-
}
|
|
18478
|
-
resolvePromise({ exitCode: code ?? 0 });
|
|
18504
|
+
resolvePromise({ exitCode: decisionExitCode(hookName, signal ? 128 : code ?? 0) });
|
|
18479
18505
|
});
|
|
18480
18506
|
child.on("error", (err) => {
|
|
18481
18507
|
process.stderr.write(`massu hook-runner: failed to spawn hook "${hookName}": ${err.message}
|
|
@@ -33605,12 +33631,13 @@ function handleCostFeature(args2, db) {
|
|
|
33605
33631
|
ORDER BY total_cost DESC
|
|
33606
33632
|
`).all(`-${days} days`);
|
|
33607
33633
|
if (rows.length === 0) {
|
|
33608
|
-
return text7(`No feature cost data found in the last ${days} days.
|
|
33634
|
+
return text7(`No feature cost data found in the last ${days} days. Each session's priced cost is allocated across registered features (massu_sentinel_register) by the files it touched; sessions with unknown cost are skipped. Try: ${p8("cost_trend")} { days: 30 } for session-level cost trends instead.`);
|
|
33609
33635
|
}
|
|
33610
33636
|
const totalCost = rows.reduce((sum, r3) => sum + r3.total_cost, 0);
|
|
33611
33637
|
const lines = [
|
|
33612
33638
|
`## Feature Cost Attribution (${days} days)`,
|
|
33613
|
-
`Total: $${totalCost.toFixed(2)} across ${rows.length}
|
|
33639
|
+
`Total: $${totalCost.toFixed(2)} across ${rows.length} feature row(s)`,
|
|
33640
|
+
"Each session's priced cost is ALLOCATED across the registered features whose files it touched, in proportion to those touches. It is an allocation by activity, not a measured per-feature cost. Sessions with unknown cost are not included.",
|
|
33614
33641
|
"",
|
|
33615
33642
|
"| Feature | Cost | Tokens | Sessions | % of Total |",
|
|
33616
33643
|
"|---------|------|--------|----------|------------|"
|