navori 0.2.19 → 0.2.21

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.
@@ -3,12 +3,12 @@
3
3
  # `{{branchBase}}`. Skips silently if jscpd or git are absent — the tool is
4
4
  # optional, not a hard dependency of the project.
5
5
  #
6
- # Triggered as a PreToolUse(Bash) hook, gated to git commit/push — so the
6
+ # Triggered as a PreToolUse(Bash) hook, gated to git commit — so the
7
7
  # duplication check runs right before code lands, not on every turn.
8
8
 
9
9
  set -euo pipefail
10
10
 
11
- # PreToolUse(Bash) passes the command — gate to commit/push. Extract without
11
+ # PreToolUse(Bash) passes the command — gate to commit. Extract without
12
12
  # hard-depending on jq (not preinstalled on macOS): try jq, then node (Claude
13
13
  # Code's own runtime), then a best-effort sed unwrap. No command extracted →
14
14
  # empty $cmd → runs unconditionally (defensive: never silently skip a commit).
@@ -23,11 +23,68 @@ extract_cmd() {
23
23
  printf '%s' "$payload" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\(.*\)".*/\1/p'
24
24
  }
25
25
  cmd=$(extract_cmd)
26
- if [ -n "$cmd" ]; then
27
- case "$cmd" in
28
- 'git commit'*|'git push'*) ;;
29
- *) exit 0 ;;
30
- esac
26
+
27
+ # --- shared gate detection (keep IN SYNC across sibling hooks) --------------
28
+ # Detect a `git commit` invocation anywhere in a (possibly
29
+ # compound) command. Splits $1 on the shell separators && || ; | and newlines,
30
+ # strips leading whitespace plus simple `VAR=value` env prefixes from each
31
+ # segment, and returns 0 if ANY segment STARTS with `git commit` on
32
+ # a word boundary. Replaces literal-prefix `case` matching, which silently
33
+ # skipped the gate for `cd x && git commit`, `echo y; git commit`, or a leading
34
+ # space. Because it matches a segment START, a quoted `echo "git commit"` does
35
+ # NOT trigger it. Known limitation: it cannot see through `sh -c`, `eval`, or
36
+ # obfuscation — a seatbelt, not a sandbox. Body duplicated across the navori quality hooks (they render standalone — no
37
+ # shared lib). The quality-gate/jscpd/cognitive copies gate `git commit` ONLY;
38
+ # the semgrep copy also gates `git push` and `gh pr create` (remote-push
39
+ # security backstop). Keep in sync.
40
+ is_git_commit() {
41
+ local input="$1" segment
42
+ # FIX B: join `\<newline>` continuations into a space FIRST, so a command
43
+ # split across lines with a trailing backslash stays ONE logical segment
44
+ # (otherwise the subcommand/flag lands in a segment not starting with git).
45
+ input="${input//\\$'\n'/ }"
46
+ input="${input//&&/$'\n'}"
47
+ input="${input//||/$'\n'}"
48
+ input="${input//;/$'\n'}"
49
+ input="${input//|/$'\n'}"
50
+ # `<<<` feeds the already-expanded value as data — no re-evaluation — so a
51
+ # command that contains backticks/$() is inspected, never executed.
52
+ while IFS= read -r segment; do
53
+ segment="${segment#"${segment%%[![:space:]]*}"}" # strip leading ws
54
+ # FIX C: peel wrappers so `(git …`, `\git`, `command git …` and
55
+ # `VAR=val git …` all reduce to a plain `git …` before matching.
56
+ while [[ "$segment" == \(* ]]; do # strip leading ( runs
57
+ segment="${segment#\(}"
58
+ segment="${segment#"${segment%%[![:space:]]*}"}"
59
+ done
60
+ segment="${segment#\\}" # strip a leading backslash (\git)
61
+ while [[ "$segment" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; do # strip VAR=val prefixes
62
+ case "$segment" in
63
+ *[[:space:]]*)
64
+ segment="${segment#*[[:space:]]}"
65
+ segment="${segment#"${segment%%[![:space:]]*}"}"
66
+ ;;
67
+ *) segment=""; break ;;
68
+ esac
69
+ done
70
+ if [[ "$segment" == command\ * ]]; then # strip a leading `command ` word
71
+ segment="${segment#command }"
72
+ segment="${segment#"${segment%%[![:space:]]*}"}"
73
+ fi
74
+ # FIX C: allow git global options between `git` and the subcommand
75
+ # (`git -c k=v commit`, `git -C /repo push`). Trailing boundary keeps
76
+ # `git commitgraph` / `git config …` from matching as commit.
77
+ if printf '%s' "$segment" | grep -qE '^git([[:space:]]+-[a-zA-Z-]+(=[^[:space:]]+)?([[:space:]]+[^-][^[:space:]]*)?)*[[:space:]]+commit([[:space:]]|$)'; then
78
+ return 0
79
+ fi
80
+ done <<< "$input"
81
+ return 1
82
+ }
83
+
84
+ # No command extracted (empty $cmd) → run unconditionally. A real command that
85
+ # is NOT a git commit → skip. Anything else → fall through and scan.
86
+ if [ -n "$cmd" ] && ! is_git_commit "$cmd"; then
87
+ exit 0
31
88
  fi
32
89
 
33
90
  if ! command -v jscpd >/dev/null 2>&1; then
@@ -3,12 +3,12 @@
3
3
  # vs `{{branchBase}}` with the `auto` ruleset. Skips silently if semgrep or
4
4
  # git are absent — the tool is optional.
5
5
  #
6
- # Triggered as a PreToolUse(Bash) hook (gated to git commit/push) and as a
6
+ # Triggered as a PreToolUse(Bash) hook (gated to git commit / push / gh pr create) and as a
7
7
  # Stop hook (runs unconditionally at session close).
8
8
 
9
9
  set -euo pipefail
10
10
 
11
- # PreToolUse(Bash) passes the command — gate to commit/push. The Stop hook
11
+ # PreToolUse(Bash) passes the command — gate to commit / push / gh pr create. The Stop hook
12
12
  # passes no command — run unconditionally at session close. Extract without
13
13
  # hard-depending on jq (not preinstalled on macOS): try jq, then node (Claude
14
14
  # Code's own runtime), then a best-effort sed unwrap. No command extracted →
@@ -24,11 +24,68 @@ extract_cmd() {
24
24
  printf '%s' "$payload" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\(.*\)".*/\1/p'
25
25
  }
26
26
  cmd=$(extract_cmd)
27
- if [ -n "$cmd" ]; then
28
- case "$cmd" in
29
- 'git commit'*|'git push'*) ;;
30
- *) exit 0 ;;
31
- esac
27
+
28
+ # --- shared gate detection (keep IN SYNC across sibling hooks) --------------
29
+ # Detect a `git commit` / `git push` / `gh pr create` invocation anywhere in a (possibly
30
+ # compound) command. Splits $1 on the shell separators && || ; | and newlines,
31
+ # strips leading whitespace plus simple `VAR=value` env prefixes from each
32
+ # segment, and returns 0 if ANY segment STARTS with `git commit`/`git push` on
33
+ # a word boundary. Replaces literal-prefix `case` matching, which silently
34
+ # skipped the gate for `cd x && git commit`, `echo y; git push`, or a leading
35
+ # space. Because it matches a segment START, a quoted `echo "git commit"` does
36
+ # NOT trigger it. Known limitation: it cannot see through `sh -c`, `eval`, or
37
+ # obfuscation — a seatbelt, not a sandbox. Body duplicated across the navori quality hooks (they render standalone — no
38
+ # shared lib). The quality-gate/jscpd/cognitive copies gate `git commit` ONLY;
39
+ # the semgrep copy also gates `git push` and `gh pr create` (remote-push
40
+ # security backstop). Keep in sync.
41
+ is_scan_trigger() {
42
+ local input="$1" segment
43
+ # FIX B: join `\<newline>` continuations into a space FIRST, so a command
44
+ # split across lines with a trailing backslash stays ONE logical segment
45
+ # (otherwise the subcommand/flag lands in a segment not starting with git).
46
+ input="${input//\\$'\n'/ }"
47
+ input="${input//&&/$'\n'}"
48
+ input="${input//||/$'\n'}"
49
+ input="${input//;/$'\n'}"
50
+ input="${input//|/$'\n'}"
51
+ # `<<<` feeds the already-expanded value as data — no re-evaluation — so a
52
+ # command that contains backticks/$() is inspected, never executed.
53
+ while IFS= read -r segment; do
54
+ segment="${segment#"${segment%%[![:space:]]*}"}" # strip leading ws
55
+ # FIX C: peel wrappers so `(git …`, `\git`, `command git …` and
56
+ # `VAR=val git …` all reduce to a plain `git …` before matching.
57
+ while [[ "$segment" == \(* ]]; do # strip leading ( runs
58
+ segment="${segment#\(}"
59
+ segment="${segment#"${segment%%[![:space:]]*}"}"
60
+ done
61
+ segment="${segment#\\}" # strip a leading backslash (\git)
62
+ while [[ "$segment" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; do # strip VAR=val prefixes
63
+ case "$segment" in
64
+ *[[:space:]]*)
65
+ segment="${segment#*[[:space:]]}"
66
+ segment="${segment#"${segment%%[![:space:]]*}"}"
67
+ ;;
68
+ *) segment=""; break ;;
69
+ esac
70
+ done
71
+ if [[ "$segment" == command\ * ]]; then # strip a leading `command ` word
72
+ segment="${segment#command }"
73
+ segment="${segment#"${segment%%[![:space:]]*}"}"
74
+ fi
75
+ # FIX C: allow git global options between `git` and the subcommand
76
+ # (`git -c k=v commit`, `git -C /repo push`). Trailing boundary keeps
77
+ # `git commitgraph` / `git config …` from matching as commit/push.
78
+ if printf '%s' "$segment" | grep -qE '(^git([[:space:]]+-[a-zA-Z-]+(=[^[:space:]]+)?([[:space:]]+[^-][^[:space:]]*)?)*[[:space:]]+(commit|push)([[:space:]]|$))|(^gh[[:space:]]+pr[[:space:]]+create([[:space:]]|$))'; then
79
+ return 0
80
+ fi
81
+ done <<< "$input"
82
+ return 1
83
+ }
84
+
85
+ # No command extracted (empty $cmd) → run unconditionally (Stop hook path). A
86
+ # real command that is NOT a scanned op → skip. Anything else → scan.
87
+ if [ -n "$cmd" ] && ! is_scan_trigger "$cmd"; then
88
+ exit 0
32
89
  fi
33
90
 
34
91
  if ! command -v semgrep >/dev/null 2>&1; then