@ngockhoale/ukit 2.2.4 → 2.2.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
CHANGED
|
@@ -2,6 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
|
+
## 2.2.6 - 2026-08-27
|
|
6
|
+
|
|
7
|
+
Follow-up to a real-world report: a user on a Bun-hosted/self-compiled omp build kept getting
|
|
8
|
+
every Edit/Write blocked with `UKit OMP hook runner failed before producing a valid result
|
|
9
|
+
(killed=true, ..., runtime=/Users/.../bin/omp)`. The diagnostic added in 2.2.4 was already
|
|
10
|
+
honest about the failure, but it did not say *why* — `runtime=` pointed straight at omp's own
|
|
11
|
+
binary (proof that `process.execPath` inside that omp build resolves to omp itself, not a real
|
|
12
|
+
Node.js binary), yet the message never told the user the fix was to set `UKIT_NODE_PATH`.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **The omp bridge's transport-failure message named the wrong-runtime symptom but never the
|
|
17
|
+
fix.** [`ukit-bridge.js`](.omp/hooks/pre/ukit-bridge.js) already resolved
|
|
18
|
+
`process.env.UKIT_NODE_PATH || process.execPath` and reported the resolved `runtime` on failure
|
|
19
|
+
(2.2.4), but a user seeing `runtime=/Users/lenk/.local/bin/omp` had no way to know that string
|
|
20
|
+
meant "wrong binary" or what to do about it. When `UKIT_NODE_PATH` is unset, the failure
|
|
21
|
+
message now appends: the reported runtime may not be a real Node.js binary, and the exact fix —
|
|
22
|
+
`export UKIT_NODE_PATH="$(command -v node)"`. No behavior change when `UKIT_NODE_PATH` is
|
|
23
|
+
already set.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **2 regression tests** in `tests/hooks/ompHookBridge.test.js` (case 13) — the wrong-runtime
|
|
28
|
+
hint appears when `UKIT_NODE_PATH` is unset, and is omitted when it is already set.
|
|
29
|
+
|
|
30
|
+
## 2.2.5 - 2026-08-27
|
|
31
|
+
|
|
32
|
+
Follow-up to the user's report that UKit had become too heavy-handed: too many hook scripts firing per tool call, too much blocking, and a concrete false-positive in the destructive-command gate itself. This wave trims default hook wiring and fixes the false positive, while explicitly preserving `rm`/destructive-command protection as instructed.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- **`PreToolUse:Bash` now runs 4 hook scripts instead of 6.** `verification-guard.sh` was removed from the default Bash chain (both `.claude/settings.json` copies, the omp bridge's `HOOK_EVENT_MAP`/`FAIL_CLOSED_SCRIPTS`, and `hook-chain-runner.mjs`'s own classification). Its hard-block path is gated behind `UKIT_VERIFICATION_GUARD_ENFORCE=1`, unset by default, so it was pure per-call overhead (a full `node` spawn) for zero present safety value. The script and its standalone tests are untouched — still shippable, opt-in.
|
|
37
|
+
- **`PreToolUse:Read|Grep|Glob` no longer runs `skill-router.sh`.** Automatic skill activation still runs on `UserPromptSubmit` and `Edit|Write`, where a skill choice actually matters; it no longer re-evaluates on every read-only tool call. Traded away: skill-router no longer sees "which files were Read/Grep'd" as mid-task evidence — only the original prompt and Edit/Write targets.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **`block-dangerous.sh` misidentified dangerous *text* as a dangerous *command*.** Both its per-pattern denylist and its `rm` handling matched against the entire raw command string with no awareness of shell quoting, so `grep -rn "rm -rf" .` or `echo "rm -rf /tmp/foo"` — which merely mention the text — were blocked as if they executed it. Quoted spans are now scrubbed before matching, but only when the command's own invoked program is a pure text tool (`grep|egrep|fgrep|rg|ag|awk|sed|echo|printf|cat`) and it does not pipe into a shell/eval sink (`| bash|sh|zsh|eval|xargs`), since `echo "rm -rf /" | bash` really does execute the quoted text. Every other command shape is scanned exactly as strictly as before.
|
|
42
|
+
- **`rm -rf ./dist` (and other allowlisted safe-cleanup targets with a `./` prefix) was blocked as an "unsafe delete target."** The unsafe-target check ran before the safe-cleanup allowlist check and its pattern misfired on the literal `./` prefix. Reordered so the allowlist is checked first; genuinely risky targets like `rm -rf ../important` still block.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- **11 regression tests** in `tests/hooks/blockDangerous.test.js` covering the false-positive fix, the closed shell-pipe escape hatch, the `./dist` allowlist fix, and all pre-existing destructive-command blocks (`rm -rf /`, `rm -rf ~`, `rm -rf .`, `rm -rf ../important`, `git push --force`, `git reset --hard`).
|
|
47
|
+
|
|
5
48
|
## 2.2.4 - 2026-08-27
|
|
6
49
|
|
|
7
50
|
Follow-up to 2.2.3's hook-chain collapse: a real omp session on a CloudMounter/FUSE-mounted project reported every tool call — including harmless `echo`, `pwd`, `ls`, and `Edit` — failing with `invalid hook-chain output`, and in one case `protect-files.sh hook chain was killed before safety gates completed`, despite `protect-files.sh` itself passing trivially. The bridge was misattributing any aggregate-runner transport failure to the first fail-closed script in the chain, whether or not that script ever ran.
|
package/package.json
CHANGED
|
@@ -9,6 +9,22 @@ if [ -z "$COMMAND" ]; then
|
|
|
9
9
|
exit 0
|
|
10
10
|
fi
|
|
11
11
|
|
|
12
|
+
# A command like `grep "rm -rf" file.sh` or `echo "rm -rf /"` merely mentions the
|
|
13
|
+
# text of a dangerous pattern as data — it does not execute it. Naively substring-matching
|
|
14
|
+
# the raw command line false-positives on any search/print of that text. Scrub quoted
|
|
15
|
+
# spans before pattern-matching, but ONLY when the invoked command is a pure text tool
|
|
16
|
+
# AND its output cannot reach a shell (no piping into bash/sh/zsh/eval/xargs), since
|
|
17
|
+
# `echo "rm -rf /" | bash` really does execute the quoted text.
|
|
18
|
+
SAFE_TEXT_CMD_REGEX='^(grep|egrep|fgrep|rg|ag|awk|sed|echo|printf|cat)$'
|
|
19
|
+
PIPE_TO_SHELL_REGEX='\|[[:space:]]*(bash|sh|zsh|eval|xargs)([[:space:]]|$)'
|
|
20
|
+
|
|
21
|
+
CMD_HEAD=$(echo "$COMMAND" | sed -E 's/^[[:space:]]*//' | awk '{print $1}' | xargs -I{} basename {} 2>/dev/null)
|
|
22
|
+
|
|
23
|
+
SCAN_COMMAND="$COMMAND"
|
|
24
|
+
if echo "$CMD_HEAD" | grep -qE "$SAFE_TEXT_CMD_REGEX" && ! echo "$COMMAND" | grep -qE "$PIPE_TO_SHELL_REGEX"; then
|
|
25
|
+
SCAN_COMMAND=$(printf '%s' "$COMMAND" | sed -E "s/'[^']*'/'Q'/g; s/\"[^\"]*\"/\"Q\"/g")
|
|
26
|
+
fi
|
|
27
|
+
|
|
12
28
|
# Dangerous patterns to block
|
|
13
29
|
DANGEROUS_PATTERNS=(
|
|
14
30
|
"rm -rf /"
|
|
@@ -27,24 +43,26 @@ DANGEROUS_PATTERNS=(
|
|
|
27
43
|
)
|
|
28
44
|
|
|
29
45
|
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
|
|
30
|
-
if echo "$
|
|
46
|
+
if echo "$SCAN_COMMAND" | grep -qE "$pattern"; then
|
|
31
47
|
echo "BLOCKED: Dangerous command detected matching pattern '$pattern'. This command could cause irreversible damage. Ask the user to run it manually if truly needed." >&2
|
|
32
48
|
exit 2
|
|
33
49
|
fi
|
|
34
50
|
done
|
|
35
51
|
|
|
36
52
|
# Handle rm commands: allow safe cleanup targets, block risky recursive force-deletes
|
|
37
|
-
if echo "$
|
|
38
|
-
if echo "$
|
|
53
|
+
if echo "$SCAN_COMMAND" | grep -qE "(^|[;&|[:space:]])rm([[:space:]]|$)"; then
|
|
54
|
+
if echo "$SCAN_COMMAND" | grep -qE "rm\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r|--recursive\s+--force|--force\s+--recursive|--recursive\s+-f|-f\s+--recursive)"; then
|
|
39
55
|
SAFE_DELETE_REGEX='(^|[[:space:]])(\./)?(dist|build|coverage|\.next|\.nuxt|\.turbo|tmp|temp|\.cache|node_modules)(/|[[:space:]]|$)'
|
|
40
56
|
UNSAFE_TARGET_REGEX='(^|[[:space:]])(/|~|\.\.?($|/)|\.($|/))'
|
|
41
57
|
|
|
42
|
-
|
|
58
|
+
# Check the allowlist first: `./dist` legitimately contains a leading "./" that would
|
|
59
|
+
# otherwise be mistaken for a bare current-directory target by UNSAFE_TARGET_REGEX below.
|
|
60
|
+
if echo "$SCAN_COMMAND" | grep -qE "$SAFE_DELETE_REGEX"; then
|
|
61
|
+
:
|
|
62
|
+
elif echo "$SCAN_COMMAND" | grep -qE "$UNSAFE_TARGET_REGEX"; then
|
|
43
63
|
echo "BLOCKED: Unsafe delete target detected. Refusing recursive force-delete." >&2
|
|
44
64
|
exit 2
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if ! echo "$COMMAND" | grep -qE "$SAFE_DELETE_REGEX"; then
|
|
65
|
+
else
|
|
48
66
|
echo "BLOCKED: 'rm -rf' is only auto-allowed for safe cleanup targets (dist/build/coverage/.next/.nuxt/.turbo/tmp/.cache/node_modules)." >&2
|
|
49
67
|
exit 2
|
|
50
68
|
fi
|
|
@@ -54,13 +54,7 @@
|
|
|
54
54
|
"PreToolUse": [
|
|
55
55
|
{
|
|
56
56
|
"matcher": "Read|Grep|Glob",
|
|
57
|
-
"hooks": [
|
|
58
|
-
{
|
|
59
|
-
"type": "command",
|
|
60
|
-
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/skill-router.sh\"",
|
|
61
|
-
"timeout": 8
|
|
62
|
-
}
|
|
63
|
-
]
|
|
57
|
+
"hooks": []
|
|
64
58
|
},
|
|
65
59
|
{
|
|
66
60
|
"matcher": "Edit|Write",
|
|
@@ -105,21 +99,11 @@
|
|
|
105
99
|
{
|
|
106
100
|
"matcher": "Bash",
|
|
107
101
|
"hooks": [
|
|
108
|
-
{
|
|
109
|
-
"type": "command",
|
|
110
|
-
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/verification-guard.sh\"",
|
|
111
|
-
"timeout": 8
|
|
112
|
-
},
|
|
113
102
|
{
|
|
114
103
|
"type": "command",
|
|
115
104
|
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/auto-allow-bash.sh\"",
|
|
116
105
|
"timeout": 8
|
|
117
106
|
},
|
|
118
|
-
{
|
|
119
|
-
"type": "command",
|
|
120
|
-
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/skill-router.sh\"",
|
|
121
|
-
"timeout": 8
|
|
122
|
-
},
|
|
123
107
|
{
|
|
124
108
|
"type": "command",
|
|
125
109
|
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/block-dangerous.sh\"",
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
|
|
19
19
|
export const HOOK_EVENT_MAP = {
|
|
20
20
|
tool_call: {
|
|
21
|
-
'Read|Grep|Glob': [
|
|
21
|
+
'Read|Grep|Glob': [],
|
|
22
22
|
'Edit|Write': [
|
|
23
23
|
'protect-files.sh',
|
|
24
24
|
'stale-spec-guard.sh',
|
|
@@ -29,9 +29,7 @@ export const HOOK_EVENT_MAP = {
|
|
|
29
29
|
'context-hardcap-gate.sh',
|
|
30
30
|
],
|
|
31
31
|
Bash: [
|
|
32
|
-
'verification-guard.sh',
|
|
33
32
|
'auto-allow-bash.sh',
|
|
34
|
-
'skill-router.sh',
|
|
35
33
|
'block-dangerous.sh',
|
|
36
34
|
'handoff-model-guard.sh',
|
|
37
35
|
'context-hardcap-gate.sh',
|
|
@@ -86,7 +84,6 @@ export const FAIL_CLOSED_SCRIPTS = new Set([
|
|
|
86
84
|
'vision-gate.sh',
|
|
87
85
|
'context-hardcap-gate.sh',
|
|
88
86
|
'block-dangerous.sh',
|
|
89
|
-
'verification-guard.sh',
|
|
90
87
|
]);
|
|
91
88
|
|
|
92
89
|
export const ADVISORY_SCRIPTS = new Set([
|
|
@@ -244,10 +241,15 @@ export async function runScriptChain(
|
|
|
244
241
|
wrapperError: chainResult?.wrapperError || null,
|
|
245
242
|
};
|
|
246
243
|
recordHookErrorDiagnostic(projectRoot, payload.session_id, diagnostic);
|
|
244
|
+
const nodePathHint = process.env.UKIT_NODE_PATH
|
|
245
|
+
? ''
|
|
246
|
+
: ` "${diagnostic.nodeExecutable}" may not be a real Node.js binary (e.g. a Bun-hosted or `
|
|
247
|
+
+ `self-compiled omp reports its own path as process.execPath). If so, set UKIT_NODE_PATH `
|
|
248
|
+
+ `to one, e.g.: export UKIT_NODE_PATH="$(command -v node)".`;
|
|
247
249
|
const reason = `UKit OMP hook runner failed before producing a valid result `
|
|
248
250
|
+ `(killed=${diagnostic.killed}, code=${diagnostic.code}, elapsedMs=${diagnostic.elapsedMs}, `
|
|
249
251
|
+ `runtime=${diagnostic.nodeExecutable}). No safety-gate verdict was available for [${scripts.join(', ')}]. `
|
|
250
|
-
+ `See .ukit/storage/cache/hook-errors
|
|
252
|
+
+ `See .ukit/storage/cache/hook-errors/.${nodePathHint}`;
|
|
251
253
|
if (failClosedOnTransportError) {
|
|
252
254
|
return { block: true, reason, context, invoked };
|
|
253
255
|
}
|