@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "2.2.4",
3
+ "version": "2.2.6",
4
4
  "description": "Install/update an index-first AI workspace for Claude Code, OpenAI Codex, OpenCode, and omp (Oh My Pi).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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 "$COMMAND" | grep -qE "$pattern"; then
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 "$COMMAND" | grep -qE "(^|[;&|[:space:]])rm([[:space:]]|$)"; then
38
- if echo "$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
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
- if echo "$COMMAND" | grep -qE "$UNSAFE_TARGET_REGEX"; then
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
- fi
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\"",
@@ -11,7 +11,6 @@ const FAIL_CLOSED_SCRIPTS = new Set([
11
11
  'vision-gate.sh',
12
12
  'context-hardcap-gate.sh',
13
13
  'block-dangerous.sh',
14
- 'verification-guard.sh',
15
14
  ]);
16
15
 
17
16
  const TOTAL_BUDGET_MS = 10000;
@@ -18,7 +18,7 @@ import {
18
18
 
19
19
  export const HOOK_EVENT_MAP = {
20
20
  tool_call: {
21
- 'Read|Grep|Glob': ['skill-router.sh'],
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
  }