crewforth 3.0.1 → 3.0.2
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 +23 -0
- package/VERSION +1 -1
- package/kit/eval/smoke-test.sh +161 -0
- package/kit/hooks/commit-msg +50 -1
- package/kit/hooks/guard-bash.sh +39 -1
- package/kit/hooks/guard-commit-scan.sh +19 -0
- package/kit/hooks/guard-powershell.sh +19 -0
- package/kit/hooks/guard-write.sh +19 -0
- package/kit/hooks/pre-commit +89 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ Crewforth was named Claude Starter Kit until 3.0.0.
|
|
|
5
5
|
Notable changes to this project are recorded here. Format follows [Keep a Changelog](https://keepachangelog.com/en/),
|
|
6
6
|
versioning follows [SemVer](https://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [3.0.2] — 2026-10-03
|
|
9
|
+
|
|
10
|
+
### Security
|
|
11
|
+
|
|
12
|
+
- **Under a Turkish system locale the gates missed what they are written to catch.** There `i` and `I` are not each
|
|
13
|
+
other's other case, and every case-insensitive match in the gates is written in ASCII. Measured under `tr_TR.UTF-8`
|
|
14
|
+
with GNU grep and bash 5 on Linux: `grep -i` did not find `INIT` with `init`, bash's `nocasematch` did not match
|
|
15
|
+
`GIT` against `git`, and a range like `[A-Za-z]` did not hold `i` or `I`, so a recursive `Remove-Item`, `git config
|
|
16
|
+
--remove-section core`, a read of a nested `.env`, a staged key and a vendor name in capitals all passed;
|
|
17
|
+
Crewforth's own suite, run whole under that locale, went from 2 failures to 23. In Git Bash on Windows `grep -iF`
|
|
18
|
+
aborted (exit 134) and the pre-commit scan for private strings read that as "no match". macOS folds the two letters
|
|
19
|
+
the ASCII way and was not affected. Every gate (`pre-commit`, `commit-msg`, `guard-commit-scan.sh`, `guard-bash.sh`,
|
|
20
|
+
`guard-write.sh`, `guard-powershell.sh`) now sets the C locale before it matches anything. A private string or a
|
|
21
|
+
pattern that holds a letter outside ASCII is still looked for under the session's own locale as well.
|
|
22
|
+
- **A scan that could not run is no longer a clean scan.** In `pre-commit` and `commit-msg`, grep failing for any
|
|
23
|
+
reason but a pattern that does not compile stops the commit and says so; the private-string scan used to read every
|
|
24
|
+
failure as "no match", `commit-msg` read a broken pattern as one too, silently, and a staged diff that could not be
|
|
25
|
+
collected was scanned as an empty one. A pattern that does not compile still only warns, now in `commit-msg` as well.
|
|
26
|
+
- **The approval prompt could carry invalid UTF-8.** The command it shows is cut to 300 a line with `cut -c`, and GNU
|
|
27
|
+
cut counts bytes in every locale: a two-byte letter across byte 300 left the prompt's JSON invalid (measured on
|
|
28
|
+
Linux under `en_US.UTF-8`, and on macOS under the C locale; Git Bash ships GNU cut too, not measured there). The cut
|
|
29
|
+
steps back over an unfinished letter now.
|
|
30
|
+
|
|
8
31
|
## [3.0.1] — 2026-10-01
|
|
9
32
|
|
|
10
33
|
### Security
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.0.
|
|
1
|
+
3.0.2
|
package/kit/eval/smoke-test.sh
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
# Kit smoke-test: structural validation (without running Claude Code).
|
|
3
3
|
# Usage: bash .claude/eval/smoke-test.sh (from the repo root or from inside .claude/eval)
|
|
4
4
|
set -uo pipefail
|
|
5
|
+
# The suite measures in the C locale, whatever the session's is. Its own patterns are ASCII, and under a Turkish
|
|
6
|
+
# locale a range like [a-z] does not hold `i`: run whole under tr_TR.UTF-8 on Linux, assertions failed for that
|
|
7
|
+
# reason alone (`skill-trust.sh` read as `ll-trust.sh`), and one more because start.sh answers in Turkish there,
|
|
8
|
+
# which is its design. A measurement that depends on who runs it is not one. The gates are handed a Turkish locale
|
|
9
|
+
# on purpose in section 12e.
|
|
10
|
+
export LC_ALL=C
|
|
5
11
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
6
12
|
ROOT="$(cd "$HERE/.." && pwd)" # .claude/
|
|
7
13
|
AGENTS="$ROOT/agents"; SKILLS="$ROOT/skills"; HOOKS="$ROOT/hooks"
|
|
@@ -1347,6 +1353,8 @@ _blk_gate(){ # $1 = marker name, $2 = what the block is, in words
|
|
|
1347
1353
|
}
|
|
1348
1354
|
_blk_gate CREW-TRANSCRIPT-DIR "the duplicated transcript-dir resolver"
|
|
1349
1355
|
_blk_gate CREW-JSON-PARSE "the duplicated JSON parser"
|
|
1356
|
+
_blk_gate CREW-LOCALE "the locale block every gate matches under"
|
|
1357
|
+
_blk_gate CREW-SCAN "the scan that tells 'could not look' from 'nothing there'"
|
|
1350
1358
|
# End to end: called by hand from this repo, the hook must produce a reading rather than "transcript not found".
|
|
1351
1359
|
cu_hand="$(cd "$ROOT/.." && bash "$HOOKS/context-usage.sh" 2>&1)"
|
|
1352
1360
|
# The three arms used to be pass / note / note, and `note` touches no counter — so on any machine without a
|
|
@@ -6406,6 +6414,159 @@ for _c in "rm /Users/u/.claude/plugins/cache/acme/tool/1.0/hooks/x.sh" "diff $PG
|
|
|
6406
6414
|
done
|
|
6407
6415
|
[ -z "$PGF$_pgok" ] && pass "plugin gate files: $PGN writes, deletes, renames and links refused (this version, another cached version, ~/\$HOME/glob/..//. spellings, Windows spellings, a root outside the cache, the sourced crew-env.sh, Write tool); each negative twin — reading, running board.sh, a source tree, another plugin — stays free" \
|
|
6408
6416
|
|| fail "plugin gate files:$PGF${_pgok:+ | wrongly refused:$_pgok}"
|
|
6417
|
+
if [ "$UNITS" = 1 ]; then
|
|
6418
|
+
sec "== 12e) the gates answer the same under a Turkish locale: there i and I are not each other's other case =="
|
|
6419
|
+
# Under tr_TR.UTF-8 the partners are i/İ and ı/I. Every case-insensitive match in the gates is written in ASCII, so
|
|
6420
|
+
# with the locale left as the session had it: GNU grep -i did not find INIT with `init`, bash's nocasematch did not
|
|
6421
|
+
# match GIT against git, [A-Za-z] did not hold I, and in Git Bash `grep -iF` aborted (exit 134) and the
|
|
6422
|
+
# private-string scan read that as "no match". macOS folds i and I the ASCII way under tr_TR, so there the locale
|
|
6423
|
+
# rows below cannot fail whatever the gates do — the line says which kind of platform it ran on.
|
|
6424
|
+
# Every gate now sets LC_ALL=C before it matches anything (the CREW-LOCALE block).
|
|
6425
|
+
_thq="'"; _thbad=""; _thn=0
|
|
6426
|
+
for _h in pre-commit commit-msg guard-commit-scan.sh guard-bash.sh guard-write.sh guard-powershell.sh; do
|
|
6427
|
+
_thn=$((_thn+1))
|
|
6428
|
+
# the first line of code after `set -…` is the block: nothing matches before the locale is set
|
|
6429
|
+
_n="$(LC_ALL=C awk '/^[[:space:]]*(#|$)/{next} /^set -/{next} {print; exit}' "$HOOKS/$_h")"
|
|
6430
|
+
[ "$_n" = '_CREW_LOCALE="${LC_ALL:-${LC_CTYPE:-${LANG:-}}}"' ] && grep -q '^export LC_ALL=C$' "$HOOKS/$_h" || _thbad="$_thbad $_h"
|
|
6431
|
+
done
|
|
6432
|
+
[ -z "$_thbad" ] && pass "all $_thn gates set the C locale before their first line of matching (pre-commit, commit-msg, guard-commit-scan, guard-bash, guard-write, guard-powershell)" \
|
|
6433
|
+
|| fail "a gate matches before it sets the C locale, or does not set it:$_thbad"
|
|
6434
|
+
|
|
6435
|
+
# A scan that could not run is not a clean scan. grep is shadowed by one that aborts for -iF (what Git Bash's grep does
|
|
6436
|
+
# under a Turkish locale), for -iE, and for the grep that collects the added lines. Runs on every platform.
|
|
6437
|
+
_TH="$(mktemp -d)"; _TH="$(cd -P "$_TH" && pwd)"
|
|
6438
|
+
_th_repo(){ # $1 = directory -> a repository with the two git hooks beside their lists, nothing staged
|
|
6439
|
+
rm -rf "$1"; mkdir -p "$1"
|
|
6440
|
+
( cd "$1" && git init -q && git config user.email t@example.com && git config user.name t \
|
|
6441
|
+
&& cp "$HOOKS/pre-commit" "$HOOKS/commit-msg" "$HOOKS/trace-blocklist.txt" "$HOOKS/secret-blocklist.txt" "$HOOKS/floor-blocklist.txt" . ) >/dev/null 2>&1; }
|
|
6442
|
+
# $1 = repository, $2 = hook, $3 = locale or empty, $4 = a directory to put first on PATH or empty, $5 = the hook's argument -> _thr, output in $_TH/out
|
|
6443
|
+
_th_hook(){ ( cd "$1" && env ${3:+LC_ALL=$3} ${4:+PATH="$4:$PATH"} bash "./$2" ${5:+"$5"} ) > "$_TH/out" 2>&1; _thr=$?; }
|
|
6444
|
+
_thg="$(command -v grep)"
|
|
6445
|
+
mkdir -p "$_TH/bin" "$_TH/bin2" "$_TH/bin3"
|
|
6446
|
+
printf '#!/bin/sh\ncase " $* " in *" -qiF "*) kill -ABRT $$ ;; esac\nexec "%s" "$@"\n' "$_thg" > "$_TH/bin/grep"
|
|
6447
|
+
printf '#!/bin/sh\ncase " $* " in *" -qiE "*) kill -ABRT $$ ;; esac\nexec "%s" "$@"\n' "$_thg" > "$_TH/bin2/grep"
|
|
6448
|
+
printf '#!/bin/sh\n[ "$1" = -E ] && [ "$2" = "^\\+" ] && kill -ABRT $$\nexec "%s" "$@"\n' "$_thg" > "$_TH/bin3/grep"
|
|
6449
|
+
chmod +x "$_TH/bin/grep" "$_TH/bin2/grep" "$_TH/bin3/grep"
|
|
6450
|
+
_th_repo "$_TH/r"
|
|
6451
|
+
printf 'acme-internal-host\n' > "$_TH/r/.private-terms.txt"
|
|
6452
|
+
( cd "$_TH/r" && printf 'see ACME-INTERNAL-HOST for the dump\n' > notes.md && git add notes.md ) >/dev/null 2>&1
|
|
6453
|
+
_thbad=""
|
|
6454
|
+
_th_hook "$_TH/r" pre-commit ""; { [ "$_thr" = 1 ] && grep -q 'PRIVATE-PATH SCANNER: a staged line contains' "$_TH/out"; } \
|
|
6455
|
+
|| _thbad="$_thbad [FIXTURE: with a working grep the private string is not reported: rc=$_thr]"
|
|
6456
|
+
_th_hook "$_TH/r" pre-commit "" "$_TH/bin"; { [ "$_thr" = 1 ] && grep -q 'could not run' "$_TH/out"; } \
|
|
6457
|
+
|| _thbad="$_thbad [grep aborting for -iF: rc=$_thr, want 1 and 'could not run' — $(sed -n 1p "$_TH/out" | cut -c1-120)]"
|
|
6458
|
+
_th_repo "$_TH/e"; ( cd "$_TH/e" && printf 'an ordinary line\n' > a.txt && git add a.txt && printf 'docs: an ordinary message\n' > msg.txt ) >/dev/null 2>&1
|
|
6459
|
+
_th_hook "$_TH/e" pre-commit "" "$_TH/bin2"; { [ "$_thr" = 1 ] && grep -q 'TRACE-SCANNER: the look for .* could not run' "$_TH/out"; } \
|
|
6460
|
+
|| _thbad="$_thbad [pre-commit, grep aborting for -iE: rc=$_thr, want 1 and 'could not run' — $(sed -n 1p "$_TH/out" | cut -c1-120)]"
|
|
6461
|
+
_th_hook "$_TH/e" commit-msg "" "$_TH/bin2" msg.txt; { [ "$_thr" = 1 ] && grep -q 'could not run' "$_TH/out"; } \
|
|
6462
|
+
|| _thbad="$_thbad [commit-msg, grep aborting for -iE: rc=$_thr, want 1 and 'could not run' — $(sed -n 1p "$_TH/out" | cut -c1-120)]"
|
|
6463
|
+
_th_hook "$_TH/r" pre-commit "" "$_TH/bin3"; { [ "$_thr" = 1 ] && grep -q 'the staged diff could not be read' "$_TH/out"; } \
|
|
6464
|
+
|| _thbad="$_thbad [pre-commit, the grep that collects the added lines aborting: rc=$_thr, want 1 and 'could not be read' — $(sed -n 1p "$_TH/out" | cut -c1-120)]"
|
|
6465
|
+
# a pattern that does not compile is a typo, not a failed look: it warns, in both git hooks, and the commit goes on
|
|
6466
|
+
_th_repo "$_TH/b"; printf 'unclosed[\n' >> "$_TH/b/trace-blocklist.txt"
|
|
6467
|
+
( cd "$_TH/b" && printf 'an ordinary line\n' > a.txt && git add a.txt && printf 'docs: an ordinary message\n' > msg.txt ) >/dev/null 2>&1
|
|
6468
|
+
_th_hook "$_TH/b" pre-commit ""; { [ "$_thr" = 0 ] && grep -q 'is not a valid regular expression' "$_TH/out"; } \
|
|
6469
|
+
|| _thbad="$_thbad [pre-commit with a pattern that does not compile: rc=$_thr, want 0 and the warning]"
|
|
6470
|
+
_th_hook "$_TH/b" commit-msg "" "" msg.txt; { [ "$_thr" = 0 ] && grep -q 'is not a valid regular expression' "$_TH/out"; } \
|
|
6471
|
+
|| _thbad="$_thbad [commit-msg with a pattern that does not compile: rc=$_thr, want 0 and the warning]"
|
|
6472
|
+
[ -z "$_thbad" ] && pass "a scan that could not run stops the commit: with grep aborting for -iF (what Git Bash's grep does under a Turkish locale) or for -iE, or while the added lines are collected, pre-commit and commit-msg exit 1 and say so; a pattern that does not compile still only warns, in both" \
|
|
6473
|
+
|| fail "a scan that could not run:$_thbad"
|
|
6474
|
+
|
|
6475
|
+
# guard-bash under a locale: $1 = locale or empty, $2 = tool, $3 = command as JSON text, $4 = mode -> _thr, stdout in $_TH/gb.out
|
|
6476
|
+
_th_gb(){ printf '{"tool_name":"%s","permission_mode":"%s","cwd":"%s","tool_input":{"command":"%s"}}' "$2" "${4:-auto}" "$_TH/w" "$3" > "$_TH/pl.json"
|
|
6477
|
+
( cd "$_TH/w" && CREW_GATE_LOG=/dev/null env -u CLAUDE_GIT_OK ${1:+LC_ALL=$1} bash "$HOOKS/guard-bash.sh" < "$_TH/pl.json" > "$_TH/gb.out" 2>"$_TH/gb.err" ); _thr=$?; }
|
|
6478
|
+
# a repository with one change staged and a review record that matches it, so a plain commit reaches the prompt
|
|
6479
|
+
rm -rf "$_TH/w"; mkdir -p "$_TH/w"
|
|
6480
|
+
( cd "$_TH/w" && git init -q && git config user.email t@example.com && git config user.name t && mkdir -p .claude \
|
|
6481
|
+
&& echo one > a.txt && git add a.txt && git -c core.hooksPath=/dev/null commit -qm init && echo two >> a.txt && git add a.txt \
|
|
6482
|
+
&& printf '{"diff_oid":"%s","head":"%s"}\n' "$(git diff --cached | git hash-object --stdin)" "$(git rev-parse HEAD)" > .claude/review-pass.json ) >/dev/null 2>&1
|
|
6483
|
+
|
|
6484
|
+
# The command shown in the approval prompt is cut to 300 bytes a line and goes into JSON. `cut -c` did it: GNU cut
|
|
6485
|
+
# counts bytes in every locale, so a two-byte letter across byte 300 left the JSON invalid UTF-8 (measured on Linux
|
|
6486
|
+
# under en_US.UTF-8). The cut steps back over an unfinished letter now, in any locale.
|
|
6487
|
+
_thutf=""; for _l in en_US.UTF-8 C.UTF-8 C.utf8; do
|
|
6488
|
+
[ "$(LC_ALL=$_l bash -c 'x=ş; printf %s "${#x}"' 2>/dev/null)" = 1 ] && { _thutf="$_l"; break; }
|
|
6489
|
+
done
|
|
6490
|
+
if ! command -v iconv >/dev/null 2>&1; then skip tool "the approval prompt's text is never cut inside a letter (no iconv to check UTF-8 with)"
|
|
6491
|
+
else
|
|
6492
|
+
_thbad=""
|
|
6493
|
+
for _l in "" ${_thutf:+"$_thutf"}; do
|
|
6494
|
+
# `git commit -m '` is 15 bytes; 284 a's bring it to 299, so byte 300 is the first half of the first ş
|
|
6495
|
+
_tha="$(printf '%*s' 284 '' | tr ' ' a)"; _th_gb "$_l" Bash "git commit -m ${_thq}${_tha}şşşşşşşşşş${_thq}" default
|
|
6496
|
+
{ [ "$(gdec "$(cat "$_TH/gb.out")")" = ask ] && iconv -f UTF-8 -t UTF-8 < "$_TH/gb.out" >/dev/null 2>&1 && grep -q 'a…' "$_TH/gb.out"; } \
|
|
6497
|
+
|| _thbad="$_thbad [locale '${_l:-C}', a cut that falls inside a letter: rc=$_thr, valid UTF-8: $(iconv -f UTF-8 -t UTF-8 < "$_TH/gb.out" >/dev/null 2>&1 && echo yes || echo NO)]"
|
|
6498
|
+
# a three-byte letter with two of its bytes before the cut: 283 a's, then € across bytes 299-301
|
|
6499
|
+
_tha="$(printf '%*s' 283 '' | tr ' ' a)"; _th_gb "$_l" Bash "git commit -m ${_thq}${_tha}€€€€${_thq}" default
|
|
6500
|
+
{ [ "$(gdec "$(cat "$_TH/gb.out")")" = ask ] && iconv -f UTF-8 -t UTF-8 < "$_TH/gb.out" >/dev/null 2>&1 && grep -q 'a…' "$_TH/gb.out"; } \
|
|
6501
|
+
|| _thbad="$_thbad [locale '${_l:-C}', a cut two bytes into a three-byte letter: rc=$_thr, valid UTF-8: $(iconv -f UTF-8 -t UTF-8 < "$_TH/gb.out" >/dev/null 2>&1 && echo yes || echo NO)]"
|
|
6502
|
+
# exactly 300 bytes: shown whole, no ellipsis
|
|
6503
|
+
_tha="$(printf '%*s' 264 '' | tr ' ' a)"; _th_gb "$_l" Bash "git commit -m ${_thq}şşşşşşşşşş${_tha}${_thq}" default
|
|
6504
|
+
{ [ "$(gdec "$(cat "$_TH/gb.out")")" = ask ] && ! grep -q "a${_thq}…" "$_TH/gb.out" && grep -q "şa" "$_TH/gb.out"; } \
|
|
6505
|
+
|| _thbad="$_thbad [locale '${_l:-C}', a command of exactly 300 bytes: rc=$_thr, want it whole and no ellipsis]"
|
|
6506
|
+
done
|
|
6507
|
+
[ -z "$_thbad" ] && pass "the command in the approval prompt is cut at 300 bytes a line and never inside a letter, under C${_thutf:+ and $_thutf}: the JSON stays UTF-8, and 300 bytes exactly are shown whole" \
|
|
6508
|
+
|| fail "the approval prompt's text:$_thbad"
|
|
6509
|
+
fi
|
|
6510
|
+
|
|
6511
|
+
# The locale itself, where this machine has one.
|
|
6512
|
+
_th=""; for _l in tr_TR.UTF-8 tr_TR.utf8 az_AZ.UTF-8 az_AZ.utf8; do
|
|
6513
|
+
[ "$(LC_ALL=$_l bash -c 'x=ş; printf %s "${#x}"' 2>/dev/null)" = 1 ] && { _th="$_l"; break; }
|
|
6514
|
+
done
|
|
6515
|
+
if [ -z "$_th" ]; then
|
|
6516
|
+
skip platform "the gates under a Turkish locale (this machine has no tr_TR.UTF-8 or az_AZ.UTF-8)" 2
|
|
6517
|
+
else
|
|
6518
|
+
# What plain matching does under it here: this is what says whether the rows below can fail on this platform.
|
|
6519
|
+
_thc="$(LC_ALL=$_th bash -c 'shopt -s nocasematch; [[ GIT == git ]] && printf folds || printf "does not fold"' 2>/dev/null)"
|
|
6520
|
+
# in a subshell: where grep aborts, the shell that waits for it reports the job ("Aborted"), and that line is
|
|
6521
|
+
# not grep's stderr, so a redirection on the pipeline does not silence it
|
|
6522
|
+
_thx=0; ( printf 'INIT\n' | LC_ALL=$_th grep -qiF init ) >/dev/null 2>&1 || _thx=$?
|
|
6523
|
+
_thkind="here plain bash $_thc I to i and grep -iF exits $_thx for INIT/init"
|
|
6524
|
+
case "$_thc/$_thx" in folds/0) _thkind="$_thkind, so these rows cannot fail on this platform" ;; esac
|
|
6525
|
+
# (1) guard-bash: each of these gave another verdict under the locale before the block
|
|
6526
|
+
THT='2 @@ PowerShell @@ Remove-Item -Recurse -Force C:\\\\proj\\\\*
|
|
6527
|
+
2 @@ Bash @@ git config --remove-section core
|
|
6528
|
+
2 @@ Bash @@ git config --rename-section core x
|
|
6529
|
+
2 @@ Bash @@ cat config/.env.production
|
|
6530
|
+
2 @@ Bash @@ cat ~/.ssh/ID_RSA
|
|
6531
|
+
0 @@ Bash @@ cat ~/.ssh/id_rsa.pub
|
|
6532
|
+
0 @@ Bash @@ cat ~/.ssh/id_rsa.pub | ssh-keygen -lf -
|
|
6533
|
+
0 @@ Bash @@ ls -la'
|
|
6534
|
+
_thbad=""; _thn=0
|
|
6535
|
+
while IFS= read -r _cl; do [ -z "$_cl" ] && continue
|
|
6536
|
+
_cw="${_cl%% @@ *}"; _cc="${_cl#* @@ }"; _ct="${_cc%% @@ *}"; _cc="${_cc#* @@ }"; _thn=$((_thn+1))
|
|
6537
|
+
_th_gb "$_th" "$_ct" "$_cc"; _thl="$_thr"; _th_gb "" "$_ct" "$_cc"
|
|
6538
|
+
{ [ "$_thl" = "$_cw" ] && [ "$_thr" = "$_cw" ]; } || _thbad="$_thbad [$_ct: $_cc → $_thl under $_th, $_thr under C, want $_cw]"
|
|
6539
|
+
done <<< "$THT"
|
|
6540
|
+
if [ "$_thn" != 8 ]; then fail "FIXTURE: the gate table under $_th ran $_thn rows, not 8"
|
|
6541
|
+
elif [ -z "$_thbad" ]; then pass "under $_th guard-bash gives the verdicts it gives under C: 8 rows (a recursive Remove-Item, core.hooksPath removed or renamed away, a nested .env, ID_RSA by case refused; id_rsa.pub, alone and piped, and ls allowed) — $_thkind"
|
|
6542
|
+
else fail "verdicts that change under $_th:$_thbad"; fi
|
|
6543
|
+
# (2) the two git hooks
|
|
6544
|
+
_thbad=""
|
|
6545
|
+
_th_repo "$_TH/t"
|
|
6546
|
+
( cd "$_TH/t" && printf 'notes generated by %s\n' 'COPI''LOT' > t.md && git add t.md ) >/dev/null 2>&1
|
|
6547
|
+
_th_hook "$_TH/t" pre-commit "$_th"; { [ "$_thr" = 1 ] && grep -q 'TRACE-SCANNER' "$_TH/out"; } || _thbad="$_thbad [pre-commit, a vendor name in capitals in an added line: rc=$_thr, want 1]"
|
|
6548
|
+
_th_repo "$_TH/s"
|
|
6549
|
+
( cd "$_TH/s" && printf 'aws_key = AKIA%s\n' 'IOSFODNN7EXAMPLE' > leak.txt && git add leak.txt ) >/dev/null 2>&1
|
|
6550
|
+
_th_hook "$_TH/s" pre-commit "$_th"; { [ "$_thr" = 1 ] && grep -q 'SECRET-SCANNER' "$_TH/out"; } || _thbad="$_thbad [pre-commit, a staged key that holds an I: rc=$_thr, want 1]"
|
|
6551
|
+
_th_hook "$_TH/r" pre-commit "$_th"; { [ "$_thr" = 1 ] && grep -q 'PRIVATE-PATH SCANNER: a staged line contains' "$_TH/out"; } || _thbad="$_thbad [pre-commit, a private string in another case: rc=$_thr, want 1 — $(sed -n 1p "$_TH/out" | cut -c1-100)]"
|
|
6552
|
+
_th_hook "$_TH/e" pre-commit "$_th"; [ "$_thr" = 0 ] || _thbad="$_thbad [pre-commit, an ordinary line: rc=$_thr, want 0]"
|
|
6553
|
+
( cd "$_TH/t" && printf 'docs: notes\n\nWritten with %s\n' 'COPI''LOT' > msg.txt ) >/dev/null 2>&1
|
|
6554
|
+
_th_hook "$_TH/t" commit-msg "$_th" "" msg.txt; [ "$_thr" = 1 ] || _thbad="$_thbad [commit-msg, a vendor name in capitals in the message: rc=$_thr, want 1]"
|
|
6555
|
+
_th_hook "$_TH/e" commit-msg "$_th" "" msg.txt; [ "$_thr" = 0 ] || _thbad="$_thbad [commit-msg, an ordinary message: rc=$_thr, want 0]"
|
|
6556
|
+
# a letter outside ASCII keeps its other case: the second look, under the session's locale
|
|
6557
|
+
_th_repo "$_TH/n"; printf 'şirket-gizli\n' > "$_TH/n/.private-terms.txt"
|
|
6558
|
+
( cd "$_TH/n" && printf 'see ŞİRKET-GİZLİ\n' > n.md && git add n.md ) >/dev/null 2>&1
|
|
6559
|
+
_th_hook "$_TH/n" pre-commit "$_th"; _thna="$_thr"
|
|
6560
|
+
# required where this platform's grep folds such a letter under the locale at all (GNU grep does, BSD grep does not)
|
|
6561
|
+
if ( printf 'ŞİRKET\n' | LC_ALL=$_th grep -qiF 'şirket' ) >/dev/null 2>&1; then
|
|
6562
|
+
[ "$_thna" = 1 ] || _thbad="$_thbad [pre-commit, a private string of non-ASCII letters in another case: rc=$_thna, want 1 (this grep folds them under $_th)]"
|
|
6563
|
+
fi
|
|
6564
|
+
[ -z "$_thbad" ] && pass "under $_th the git hooks stop what they stop under C: a vendor name in another case in an added line and in the message, a staged key that holds an I, a private string in another case; an ordinary line and message pass (a private string of non-ASCII letters in another case: exit $_thna, 1 = caught by the second look)" \
|
|
6565
|
+
|| fail "the git hooks under $_th:$_thbad"
|
|
6566
|
+
fi
|
|
6567
|
+
rm -rf "$_TH"
|
|
6568
|
+
fi # UNITS
|
|
6569
|
+
|
|
6409
6570
|
sec "== 13) pre-commit cost — the gate people route around is the one that is slow =="
|
|
6410
6571
|
# Measured on a 373-file merge: the old file loop spawned ~7 processes per file (three `printf | grep` pairs and
|
|
6411
6572
|
# a `git cat-file`), 2,643 in total. At the 62-135 ms a Git Bash process was measured to cost on a Windows 11
|
package/kit/hooks/commit-msg
CHANGED
|
@@ -4,6 +4,49 @@
|
|
|
4
4
|
# (2) team board claim gate — a commit that names an item must be backed by a live claim held by YOU.
|
|
5
5
|
# Skipping either requires an explicit request per §4.5 (git commit --no-verify).
|
|
6
6
|
set -euo pipefail
|
|
7
|
+
# ---- CREW-LOCALE -----------------------------------------------------------------------------------------
|
|
8
|
+
# Everything this gate matches with runs in the C locale, whatever the session's own is. Under a Turkish locale
|
|
9
|
+
# the letters i and I are not each other's other case (their partners are İ and ı), and every case-insensitive
|
|
10
|
+
# match here is written in ASCII. Measured under tr_TR.UTF-8 with the locale left as it came:
|
|
11
|
+
# GNU grep 3.11 / bash 5.2 on Linux: `grep -i init` does not find INIT (-F, -E and plain alike);
|
|
12
|
+
# bash's nocasematch does not match GIT against git; `[A-Za-z]` in a regex does not hold I; awk's tolower
|
|
13
|
+
# turns GIT into gıt. The suite, run whole under that locale, went from 2 failures to 23: a recursive
|
|
14
|
+
# Remove-Item, a write to a gate file, `git config --remove-section core`, a read of a nested .env and a
|
|
15
|
+
# staged key all passed.
|
|
16
|
+
# GNU grep 3.0 in Git Bash on Windows: `grep -iF` with an ASCII pattern aborts (exit 134), and the pre-commit
|
|
17
|
+
# scan for private strings read that as "no match".
|
|
18
|
+
# macOS (BSD grep, bash 3.2): none of it; tr_TR.UTF-8 folds i and I the ASCII way there.
|
|
19
|
+
# The cost: a letter outside ASCII has no other case in the C locale. The two git hooks that scan a user's own
|
|
20
|
+
# words look a second time under the session's locale for a pattern that holds such a letter (_CREW_LOCALE).
|
|
21
|
+
# Byte-identical in every gate; the suite pins it.
|
|
22
|
+
_CREW_LOCALE="${LC_ALL:-${LC_CTYPE:-${LANG:-}}}"
|
|
23
|
+
case "$_CREW_LOCALE" in C|POSIX) _CREW_LOCALE="" ;; esac
|
|
24
|
+
export LC_ALL=C
|
|
25
|
+
# ---- /CREW-LOCALE
|
|
26
|
+
# ---- CREW-SCAN -------------------------------------------------------------------------------------------
|
|
27
|
+
# One scan of one file for one pattern, with "could not look" kept apart from "nothing there". grep exits 1 for
|
|
28
|
+
# no match and 2 or more when it could not answer: a pattern that does not compile, a file it could not read, or
|
|
29
|
+
# grep itself dying (exit 134 for `grep -iF` under a Turkish locale in Git Bash, measured). Read as "no match",
|
|
30
|
+
# that is a scan that passes whatever it was shown.
|
|
31
|
+
# 0 a match 1 no match
|
|
32
|
+
# 2 the pattern does not compile (grep says exactly 2, and says it again for empty input) - the caller warns:
|
|
33
|
+
# a typo in a hand-edited list must not lock every commit, and the other patterns still ran
|
|
34
|
+
# 3 could not look - the caller stops the commit
|
|
35
|
+
# A pattern that holds a letter outside ASCII is looked for a second time under the session's own locale when
|
|
36
|
+
# the first look finds nothing and the match ignores case: in the C locale such a letter has no other case.
|
|
37
|
+
_crew_scan(){ # $1 = grep's flags after -q (iE, E, iF) · $2 = pattern · $3 = file
|
|
38
|
+
local r=0
|
|
39
|
+
grep -q"$1" -- "$2" "$3" 2>/dev/null || r=$?
|
|
40
|
+
if [ "$r" = 1 ] && [ -n "$_CREW_LOCALE" ]; then
|
|
41
|
+
case "$1" in *i*) case "$2" in *[!$'\t'\ -~]*) r=0; LC_ALL="$_CREW_LOCALE" grep -q"$1" -- "$2" "$3" 2>/dev/null || r=$? ;; esac ;; esac
|
|
42
|
+
fi
|
|
43
|
+
[ "$r" -ge 2 ] || return "$r"
|
|
44
|
+
case "$1" in *F*) return 3 ;; esac
|
|
45
|
+
r=0; grep -q"$1" -- "$2" /dev/null 2>/dev/null || r=$?
|
|
46
|
+
[ "$r" != 2 ] || return 2
|
|
47
|
+
return 3
|
|
48
|
+
}
|
|
49
|
+
# ---- /CREW-SCAN
|
|
7
50
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
8
51
|
BL="$HERE/trace-blocklist.txt"
|
|
9
52
|
MSG_FILE="${1:-}"
|
|
@@ -36,9 +79,15 @@ while IFS= read -r pat || [ -n "$pat" ]; do
|
|
|
36
79
|
# project, Git for Windows defaults core.autocrlf=true,
|
|
37
80
|
# and this file is meant to be edited by hand.
|
|
38
81
|
if [ -n "$AL" ] && grep -qxF -- "$pat" "$AL"; then continue; fi
|
|
39
|
-
|
|
82
|
+
_gr=0; _crew_scan iE "$pat" "$MSG_FILE" || _gr=$?
|
|
83
|
+
if [ "$_gr" = 0 ]; then
|
|
40
84
|
echo "TRACE-SCANNER (message): forbidden expression -> '$pat' (§4.1)"
|
|
41
85
|
HIT=1
|
|
86
|
+
elif [ "$_gr" = 2 ]; then
|
|
87
|
+
echo "TRACE-SCANNER (message): /$pat/ in $BL is not a valid regular expression, so it matches NOTHING. Every other pattern still ran; fix or remove this line."
|
|
88
|
+
elif [ "$_gr" -ge 3 ]; then
|
|
89
|
+
echo "TRACE-SCANNER (message): the look for '$pat' could not run, so the message is not known to be clean and the commit is stopped (§4.1)."
|
|
90
|
+
HIT=1
|
|
42
91
|
fi
|
|
43
92
|
done < "$BL"
|
|
44
93
|
|
package/kit/hooks/guard-bash.sh
CHANGED
|
@@ -39,6 +39,25 @@
|
|
|
39
39
|
# CLAUDE_GIT_OK=1, exported by the user before the session starts, pre-authorises the session. It exists for
|
|
40
40
|
# headless/CI runs where no one is at the keyboard. It does NOT replace approval: present the message first.
|
|
41
41
|
set -uo pipefail
|
|
42
|
+
# ---- CREW-LOCALE -----------------------------------------------------------------------------------------
|
|
43
|
+
# Everything this gate matches with runs in the C locale, whatever the session's own is. Under a Turkish locale
|
|
44
|
+
# the letters i and I are not each other's other case (their partners are İ and ı), and every case-insensitive
|
|
45
|
+
# match here is written in ASCII. Measured under tr_TR.UTF-8 with the locale left as it came:
|
|
46
|
+
# GNU grep 3.11 / bash 5.2 on Linux: `grep -i init` does not find INIT (-F, -E and plain alike);
|
|
47
|
+
# bash's nocasematch does not match GIT against git; `[A-Za-z]` in a regex does not hold I; awk's tolower
|
|
48
|
+
# turns GIT into gıt. The suite, run whole under that locale, went from 2 failures to 23: a recursive
|
|
49
|
+
# Remove-Item, a write to a gate file, `git config --remove-section core`, a read of a nested .env and a
|
|
50
|
+
# staged key all passed.
|
|
51
|
+
# GNU grep 3.0 in Git Bash on Windows: `grep -iF` with an ASCII pattern aborts (exit 134), and the pre-commit
|
|
52
|
+
# scan for private strings read that as "no match".
|
|
53
|
+
# macOS (BSD grep, bash 3.2): none of it; tr_TR.UTF-8 folds i and I the ASCII way there.
|
|
54
|
+
# The cost: a letter outside ASCII has no other case in the C locale. The two git hooks that scan a user's own
|
|
55
|
+
# words look a second time under the session's locale for a pattern that holds such a letter (_CREW_LOCALE).
|
|
56
|
+
# Byte-identical in every gate; the suite pins it.
|
|
57
|
+
_CREW_LOCALE="${LC_ALL:-${LC_CTYPE:-${LANG:-}}}"
|
|
58
|
+
case "$_CREW_LOCALE" in C|POSIX) _CREW_LOCALE="" ;; esac
|
|
59
|
+
export LC_ALL=C
|
|
60
|
+
# ---- /CREW-LOCALE
|
|
42
61
|
# The 2.x names of the variables a user can set still work (one helper: eval/lib/crew-env.sh).
|
|
43
62
|
_crew_d="${BASH_SOURCE%/*}"; [ "$_crew_d" = "${BASH_SOURCE}" ] && _crew_d=.
|
|
44
63
|
[ -f "$_crew_d/../eval/lib/crew-env.sh" ] && . "$_crew_d/../eval/lib/crew-env.sh"; unset _crew_d
|
|
@@ -1221,6 +1240,25 @@ case "$CMD" in *[Rr][Mm]*) : ;; *) false ;; esac && echo "$CMD" | grep -qE '(rm|
|
|
|
1221
1240
|
# - fold a surviving tab to a space (display-only text; the command Claude runs is untouched);
|
|
1222
1241
|
# - escape backslash and double quote;
|
|
1223
1242
|
# - fold newlines to the two-character \n escape.
|
|
1243
|
+
# Each line of a text cut to at most N bytes, never inside a letter. This is the command as the approval prompt
|
|
1244
|
+
# shows it, and it goes into JSON. `cut -c1-300` did this job: GNU cut counts BYTES whatever the locale (measured on
|
|
1245
|
+
# Linux under en_US.UTF-8: a two-byte letter across byte 300 left the prompt's JSON invalid UTF-8), and BSD cut
|
|
1246
|
+
# counts characters only while the session's locale says so. Here the cut is by bytes and then steps back over an
|
|
1247
|
+
# unfinished letter: a UTF-8 letter is one lead byte (0xC0 and up) followed by continuation bytes (0x80-0xBF).
|
|
1248
|
+
_utf8_cut(){ # $1 = text, $2 = bytes per line -> _UC
|
|
1249
|
+
local LC_ALL=C
|
|
1250
|
+
local l out="" nl=""
|
|
1251
|
+
while IFS= read -r l || [ -n "$l" ]; do
|
|
1252
|
+
if [ "${#l}" -gt "$2" ]; then
|
|
1253
|
+
l="${l:0:$2}"
|
|
1254
|
+
while :; do case "$l" in *[$'\x80'-$'\xbf']) l="${l%?}" ;; *) break ;; esac; done
|
|
1255
|
+
case "$l" in *[$'\xc0'-$'\xff']) l="${l%?}" ;; esac
|
|
1256
|
+
fi
|
|
1257
|
+
out="$out$nl$l"; nl=$'\n'
|
|
1258
|
+
done <<< "$1"
|
|
1259
|
+
while :; do case "$out" in *$'\n') out="${out%?}" ;; *) break ;; esac; done
|
|
1260
|
+
_UC="$out"
|
|
1261
|
+
}
|
|
1224
1262
|
json_escape(){
|
|
1225
1263
|
printf '%s' "$1" \
|
|
1226
1264
|
| tr -d '\000-\010\013-\037\177' \
|
|
@@ -1560,7 +1598,7 @@ if git_has "$CMD" 'commit|push'; then
|
|
|
1560
1598
|
default|acceptEdits)
|
|
1561
1599
|
# A prompt provably reaches the user in these modes: ask, and let them approve in one keypress.
|
|
1562
1600
|
SHORT="$CMD"
|
|
1563
|
-
[ "${#SHORT}" -gt 300 ] &&
|
|
1601
|
+
[ "${#SHORT}" -gt 300 ] && { _utf8_cut "$SHORT" 300; SHORT="$_UC…"; }
|
|
1564
1602
|
# §4.4 branch guard: committing straight onto main/master is not blocked (a fresh project legitimately
|
|
1565
1603
|
# lives on main), but it is surfaced in the approval prompt so the user can send it to a branch instead.
|
|
1566
1604
|
BRANCH_WARN=""
|
|
@@ -19,6 +19,25 @@
|
|
|
19
19
|
# Deliberately NOT covered: `--no-verify`. It is §4.5 and `guard-bash.sh` blocks it outright, so it never
|
|
20
20
|
# reaches this hook.
|
|
21
21
|
set -uo pipefail
|
|
22
|
+
# ---- CREW-LOCALE -----------------------------------------------------------------------------------------
|
|
23
|
+
# Everything this gate matches with runs in the C locale, whatever the session's own is. Under a Turkish locale
|
|
24
|
+
# the letters i and I are not each other's other case (their partners are İ and ı), and every case-insensitive
|
|
25
|
+
# match here is written in ASCII. Measured under tr_TR.UTF-8 with the locale left as it came:
|
|
26
|
+
# GNU grep 3.11 / bash 5.2 on Linux: `grep -i init` does not find INIT (-F, -E and plain alike);
|
|
27
|
+
# bash's nocasematch does not match GIT against git; `[A-Za-z]` in a regex does not hold I; awk's tolower
|
|
28
|
+
# turns GIT into gıt. The suite, run whole under that locale, went from 2 failures to 23: a recursive
|
|
29
|
+
# Remove-Item, a write to a gate file, `git config --remove-section core`, a read of a nested .env and a
|
|
30
|
+
# staged key all passed.
|
|
31
|
+
# GNU grep 3.0 in Git Bash on Windows: `grep -iF` with an ASCII pattern aborts (exit 134), and the pre-commit
|
|
32
|
+
# scan for private strings read that as "no match".
|
|
33
|
+
# macOS (BSD grep, bash 3.2): none of it; tr_TR.UTF-8 folds i and I the ASCII way there.
|
|
34
|
+
# The cost: a letter outside ASCII has no other case in the C locale. The two git hooks that scan a user's own
|
|
35
|
+
# words look a second time under the session's locale for a pattern that holds such a letter (_CREW_LOCALE).
|
|
36
|
+
# Byte-identical in every gate; the suite pins it.
|
|
37
|
+
_CREW_LOCALE="${LC_ALL:-${LC_CTYPE:-${LANG:-}}}"
|
|
38
|
+
case "$_CREW_LOCALE" in C|POSIX) _CREW_LOCALE="" ;; esac
|
|
39
|
+
export LC_ALL=C
|
|
40
|
+
# ---- /CREW-LOCALE
|
|
22
41
|
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
23
42
|
|
|
24
43
|
# ---- CREW-JSON-PARSE ------------------------------------------------------------------------------------
|
|
@@ -18,6 +18,25 @@
|
|
|
18
18
|
# Wired ONLY on the PowerShell matcher (both editions; smoke pins it). On `Bash` it would block the very commands it
|
|
19
19
|
# points to.
|
|
20
20
|
set -uo pipefail
|
|
21
|
+
# ---- CREW-LOCALE -----------------------------------------------------------------------------------------
|
|
22
|
+
# Everything this gate matches with runs in the C locale, whatever the session's own is. Under a Turkish locale
|
|
23
|
+
# the letters i and I are not each other's other case (their partners are İ and ı), and every case-insensitive
|
|
24
|
+
# match here is written in ASCII. Measured under tr_TR.UTF-8 with the locale left as it came:
|
|
25
|
+
# GNU grep 3.11 / bash 5.2 on Linux: `grep -i init` does not find INIT (-F, -E and plain alike);
|
|
26
|
+
# bash's nocasematch does not match GIT against git; `[A-Za-z]` in a regex does not hold I; awk's tolower
|
|
27
|
+
# turns GIT into gıt. The suite, run whole under that locale, went from 2 failures to 23: a recursive
|
|
28
|
+
# Remove-Item, a write to a gate file, `git config --remove-section core`, a read of a nested .env and a
|
|
29
|
+
# staged key all passed.
|
|
30
|
+
# GNU grep 3.0 in Git Bash on Windows: `grep -iF` with an ASCII pattern aborts (exit 134), and the pre-commit
|
|
31
|
+
# scan for private strings read that as "no match".
|
|
32
|
+
# macOS (BSD grep, bash 3.2): none of it; tr_TR.UTF-8 folds i and I the ASCII way there.
|
|
33
|
+
# The cost: a letter outside ASCII has no other case in the C locale. The two git hooks that scan a user's own
|
|
34
|
+
# words look a second time under the session's locale for a pattern that holds such a letter (_CREW_LOCALE).
|
|
35
|
+
# Byte-identical in every gate; the suite pins it.
|
|
36
|
+
_CREW_LOCALE="${LC_ALL:-${LC_CTYPE:-${LANG:-}}}"
|
|
37
|
+
case "$_CREW_LOCALE" in C|POSIX) _CREW_LOCALE="" ;; esac
|
|
38
|
+
export LC_ALL=C
|
|
39
|
+
# ---- /CREW-LOCALE
|
|
21
40
|
# Read with the builtin: `INPUT="$(cat)"` is a subshell plus a process on EVERY PowerShell call, and on Git Bash a
|
|
22
41
|
# process costs 62-135 ms. With this, a command that cannot match (no `bash` in the payload) opens no process at all
|
|
23
42
|
# (measured with the xtrace counter: `cat` 1 -> 0). `read` returns 1 at end of input; the text is read regardless.
|
package/kit/hooks/guard-write.sh
CHANGED
|
@@ -30,6 +30,25 @@
|
|
|
30
30
|
# Normalise first, match second — and normalise with parameter expansion only, because this hook runs before
|
|
31
31
|
# EVERY Write/Edit and a fork per call is a freeze on Windows (Git Bash charges 62-135 ms per process, measured).
|
|
32
32
|
set -uo pipefail
|
|
33
|
+
# ---- CREW-LOCALE -----------------------------------------------------------------------------------------
|
|
34
|
+
# Everything this gate matches with runs in the C locale, whatever the session's own is. Under a Turkish locale
|
|
35
|
+
# the letters i and I are not each other's other case (their partners are İ and ı), and every case-insensitive
|
|
36
|
+
# match here is written in ASCII. Measured under tr_TR.UTF-8 with the locale left as it came:
|
|
37
|
+
# GNU grep 3.11 / bash 5.2 on Linux: `grep -i init` does not find INIT (-F, -E and plain alike);
|
|
38
|
+
# bash's nocasematch does not match GIT against git; `[A-Za-z]` in a regex does not hold I; awk's tolower
|
|
39
|
+
# turns GIT into gıt. The suite, run whole under that locale, went from 2 failures to 23: a recursive
|
|
40
|
+
# Remove-Item, a write to a gate file, `git config --remove-section core`, a read of a nested .env and a
|
|
41
|
+
# staged key all passed.
|
|
42
|
+
# GNU grep 3.0 in Git Bash on Windows: `grep -iF` with an ASCII pattern aborts (exit 134), and the pre-commit
|
|
43
|
+
# scan for private strings read that as "no match".
|
|
44
|
+
# macOS (BSD grep, bash 3.2): none of it; tr_TR.UTF-8 folds i and I the ASCII way there.
|
|
45
|
+
# The cost: a letter outside ASCII has no other case in the C locale. The two git hooks that scan a user's own
|
|
46
|
+
# words look a second time under the session's locale for a pattern that holds such a letter (_CREW_LOCALE).
|
|
47
|
+
# Byte-identical in every gate; the suite pins it.
|
|
48
|
+
_CREW_LOCALE="${LC_ALL:-${LC_CTYPE:-${LANG:-}}}"
|
|
49
|
+
case "$_CREW_LOCALE" in C|POSIX) _CREW_LOCALE="" ;; esac
|
|
50
|
+
export LC_ALL=C
|
|
51
|
+
# ---- /CREW-LOCALE
|
|
33
52
|
# The 2.x names of the variables a user can set still work (one helper: eval/lib/crew-env.sh).
|
|
34
53
|
_crew_d="${BASH_SOURCE%/*}"; [ "$_crew_d" = "${BASH_SOURCE}" ] && _crew_d=.
|
|
35
54
|
[ -f "$_crew_d/../eval/lib/crew-env.sh" ] && . "$_crew_d/../eval/lib/crew-env.sh"; unset _crew_d
|
package/kit/hooks/pre-commit
CHANGED
|
@@ -5,6 +5,59 @@
|
|
|
5
5
|
# (3) private-path scanner — this machine's $HOME + .private-terms.txt (§4.3)
|
|
6
6
|
# Skipping requires an explicit request per §4.5 (git commit --no-verify).
|
|
7
7
|
set -euo pipefail
|
|
8
|
+
# ---- CREW-LOCALE -----------------------------------------------------------------------------------------
|
|
9
|
+
# Everything this gate matches with runs in the C locale, whatever the session's own is. Under a Turkish locale
|
|
10
|
+
# the letters i and I are not each other's other case (their partners are İ and ı), and every case-insensitive
|
|
11
|
+
# match here is written in ASCII. Measured under tr_TR.UTF-8 with the locale left as it came:
|
|
12
|
+
# GNU grep 3.11 / bash 5.2 on Linux: `grep -i init` does not find INIT (-F, -E and plain alike);
|
|
13
|
+
# bash's nocasematch does not match GIT against git; `[A-Za-z]` in a regex does not hold I; awk's tolower
|
|
14
|
+
# turns GIT into gıt. The suite, run whole under that locale, went from 2 failures to 23: a recursive
|
|
15
|
+
# Remove-Item, a write to a gate file, `git config --remove-section core`, a read of a nested .env and a
|
|
16
|
+
# staged key all passed.
|
|
17
|
+
# GNU grep 3.0 in Git Bash on Windows: `grep -iF` with an ASCII pattern aborts (exit 134), and the pre-commit
|
|
18
|
+
# scan for private strings read that as "no match".
|
|
19
|
+
# macOS (BSD grep, bash 3.2): none of it; tr_TR.UTF-8 folds i and I the ASCII way there.
|
|
20
|
+
# The cost: a letter outside ASCII has no other case in the C locale. The two git hooks that scan a user's own
|
|
21
|
+
# words look a second time under the session's locale for a pattern that holds such a letter (_CREW_LOCALE).
|
|
22
|
+
# Byte-identical in every gate; the suite pins it.
|
|
23
|
+
_CREW_LOCALE="${LC_ALL:-${LC_CTYPE:-${LANG:-}}}"
|
|
24
|
+
case "$_CREW_LOCALE" in C|POSIX) _CREW_LOCALE="" ;; esac
|
|
25
|
+
export LC_ALL=C
|
|
26
|
+
# ---- /CREW-LOCALE
|
|
27
|
+
# ---- CREW-SCAN -------------------------------------------------------------------------------------------
|
|
28
|
+
# One scan of one file for one pattern, with "could not look" kept apart from "nothing there". grep exits 1 for
|
|
29
|
+
# no match and 2 or more when it could not answer: a pattern that does not compile, a file it could not read, or
|
|
30
|
+
# grep itself dying (exit 134 for `grep -iF` under a Turkish locale in Git Bash, measured). Read as "no match",
|
|
31
|
+
# that is a scan that passes whatever it was shown.
|
|
32
|
+
# 0 a match 1 no match
|
|
33
|
+
# 2 the pattern does not compile (grep says exactly 2, and says it again for empty input) - the caller warns:
|
|
34
|
+
# a typo in a hand-edited list must not lock every commit, and the other patterns still ran
|
|
35
|
+
# 3 could not look - the caller stops the commit
|
|
36
|
+
# A pattern that holds a letter outside ASCII is looked for a second time under the session's own locale when
|
|
37
|
+
# the first look finds nothing and the match ignores case: in the C locale such a letter has no other case.
|
|
38
|
+
_crew_scan(){ # $1 = grep's flags after -q (iE, E, iF) · $2 = pattern · $3 = file
|
|
39
|
+
local r=0
|
|
40
|
+
grep -q"$1" -- "$2" "$3" 2>/dev/null || r=$?
|
|
41
|
+
if [ "$r" = 1 ] && [ -n "$_CREW_LOCALE" ]; then
|
|
42
|
+
case "$1" in *i*) case "$2" in *[!$'\t'\ -~]*) r=0; LC_ALL="$_CREW_LOCALE" grep -q"$1" -- "$2" "$3" 2>/dev/null || r=$? ;; esac ;; esac
|
|
43
|
+
fi
|
|
44
|
+
[ "$r" -ge 2 ] || return "$r"
|
|
45
|
+
case "$1" in *F*) return 3 ;; esac
|
|
46
|
+
r=0; grep -q"$1" -- "$2" /dev/null 2>/dev/null || r=$?
|
|
47
|
+
[ "$r" != 2 ] || return 2
|
|
48
|
+
return 3
|
|
49
|
+
}
|
|
50
|
+
# ---- /CREW-SCAN
|
|
51
|
+
_crew_looked(){ # $1 = what a pipeline collects for the scans · the rest = the exit status of each of its commands
|
|
52
|
+
# Every status, not the pipeline's own: with pipefail that is the LAST command that failed, and the last grep
|
|
53
|
+
# exits 1 whenever nothing reaches it - which is exactly what an abort further up leaves behind.
|
|
54
|
+
local what="$1" r; shift
|
|
55
|
+
for r in "$@"; do
|
|
56
|
+
[ "$r" -lt 2 ] && continue
|
|
57
|
+
echo "PRE-COMMIT: $what could not be read (exit $r), so nothing was scanned and the commit is stopped."
|
|
58
|
+
exit 1
|
|
59
|
+
done
|
|
60
|
+
}
|
|
8
61
|
# The 2.x names of the variables a user can set still work (one helper: eval/lib/crew-env.sh).
|
|
9
62
|
_crew_d="${BASH_SOURCE%/*}"; [ "$_crew_d" = "${BASH_SOURCE}" ] && _crew_d=.
|
|
10
63
|
[ -f "$_crew_d/../eval/lib/crew-env.sh" ] && . "$_crew_d/../eval/lib/crew-env.sh"; unset _crew_d
|
|
@@ -47,7 +100,7 @@ trap '_crew_cleanup; exit 143' INT TERM
|
|
|
47
100
|
git diff --cached --unified=0 -- . \
|
|
48
101
|
':(top,exclude,glob)**/trace-blocklist.txt' ':(top,exclude,glob)**/secret-blocklist.txt' \
|
|
49
102
|
':(top,exclude)trace-blocklist.txt' ':(top,exclude)secret-blocklist.txt' \
|
|
50
|
-
| grep -E '^\+' | grep -Ev '^\+\+\+' > "$SECRET_F" ||
|
|
103
|
+
| grep -E '^\+' | grep -Ev '^\+\+\+' > "$SECRET_F" || _crew_looked "the staged diff" "${PIPESTATUS[@]}"
|
|
51
104
|
|
|
52
105
|
# Traces (§4.1/§4.2) are scanned in the PROJECT's own files only. `.claude/` is Crewforth's tree: it configures the
|
|
53
106
|
# assistant, it legitimately names the tool it configures, and an update overwrites it — it is not an artifact the
|
|
@@ -56,7 +109,7 @@ git diff --cached --unified=0 -- . \
|
|
|
56
109
|
# falls under .claude/, but in Crewforth's own repo it sits at kit/hooks/ where that exclusion misses it.
|
|
57
110
|
git diff --cached --unified=0 -- . ':(top,exclude).claude' \
|
|
58
111
|
':(top,exclude,glob)**/trace-blocklist.txt' ':(top,exclude)trace-blocklist.txt' \
|
|
59
|
-
| grep -E '^\+' | grep -Ev '^\+\+\+' > "$TRACE_F" ||
|
|
112
|
+
| grep -E '^\+' | grep -Ev '^\+\+\+' > "$TRACE_F" || _crew_looked "the staged diff" "${PIPESTATUS[@]}"
|
|
60
113
|
|
|
61
114
|
# `git commit -a` stages tracked changes as part of the commit. As a git hook that is invisible — git stages
|
|
62
115
|
# first, so --cached already contains them by the time this runs. Called AHEAD of the commit (the plugin
|
|
@@ -67,10 +120,10 @@ if [ "${CREW_SCAN_UNSTAGED:-0}" = 1 ]; then
|
|
|
67
120
|
git diff --unified=0 -- . \
|
|
68
121
|
':(top,exclude,glob)**/trace-blocklist.txt' ':(top,exclude,glob)**/secret-blocklist.txt' \
|
|
69
122
|
':(top,exclude)trace-blocklist.txt' ':(top,exclude)secret-blocklist.txt' \
|
|
70
|
-
| grep -E '^\+' | grep -Ev '^\+\+\+' >> "$SECRET_F" ||
|
|
123
|
+
| grep -E '^\+' | grep -Ev '^\+\+\+' >> "$SECRET_F" || _crew_looked "the unstaged diff" "${PIPESTATUS[@]}"
|
|
71
124
|
git diff --unified=0 -- . ':(top,exclude).claude' \
|
|
72
125
|
':(top,exclude,glob)**/trace-blocklist.txt' ':(top,exclude)trace-blocklist.txt' \
|
|
73
|
-
| grep -E '^\+' | grep -Ev '^\+\+\+' >> "$TRACE_F" ||
|
|
126
|
+
| grep -E '^\+' | grep -Ev '^\+\+\+' >> "$TRACE_F" || _crew_looked "the unstaged diff" "${PIPESTATUS[@]}"
|
|
74
127
|
fi
|
|
75
128
|
|
|
76
129
|
HIT=0
|
|
@@ -300,16 +353,21 @@ done < <({ _floor_diff --cached; [ "${CREW_SCAN_UNSTAGED:-0}" = 1 ] && _floor_di
|
|
|
300
353
|
# secrets newly passed. Also checked structurally: no pattern in either shipped blocklist carries a bare
|
|
301
354
|
# top-level `|` (every one is inside a group) and none uses a back-reference, so joining cannot reshape them.
|
|
302
355
|
_crew_any(){ # $1 = pattern list $2 = corpus $3 = grep flags $4 = 1 for newline-joined fixed strings
|
|
303
|
-
local pat alt="" sep="|"
|
|
356
|
+
local pat alt="" sep="|" na=0 r=0
|
|
304
357
|
[ "$4" = 1 ] && sep="
|
|
305
358
|
"
|
|
306
359
|
while IFS= read -r pat || [ -n "$pat" ]; do
|
|
307
360
|
pat="${pat%$'\r'}"
|
|
308
361
|
case "$pat" in ''|\#*) continue;; esac
|
|
309
362
|
alt="${alt:+$alt$sep}$pat"
|
|
363
|
+
case "$pat" in *[!$'\t'\ -~]*) na=1 ;; esac
|
|
310
364
|
done < "$1"
|
|
311
365
|
[ -n "$alt" ] || return 1
|
|
312
|
-
grep -q$3 -- "$alt" "$2" 2>/dev/null
|
|
366
|
+
grep -q$3 -- "$alt" "$2" 2>/dev/null || r=$?
|
|
367
|
+
# Nothing found, but a pattern holds a letter outside ASCII and the match ignores case: the per-pattern loop looks
|
|
368
|
+
# again under the session's own locale (_crew_scan), so send the caller there.
|
|
369
|
+
if [ "$r" = 1 ] && [ "$na" = 1 ] && [ -n "$_CREW_LOCALE" ]; then case "$3" in *i*) return 4 ;; esac; fi
|
|
370
|
+
return "$r"
|
|
313
371
|
}
|
|
314
372
|
# (1) trace scan — case-INsensitive (vendor / AI-authorship strings), project files only
|
|
315
373
|
# `!= 1` AND NOT `= 0`, AND THE DIFFERENCE IS THE WHOLE GATE. _crew_any joins every pattern into ONE
|
|
@@ -345,12 +403,15 @@ if [ -f "$BL" ] && [ -s "$TRACE_F" ] && [ "$_dec" != 1 ]; then
|
|
|
345
403
|
# `|| _gr=$?` and not a bare call: `set -e` is in force at the top of this file, and a grep that simply
|
|
346
404
|
# does not match exits 1, which would kill the hook on the first clean pattern. Measured the wrong way once
|
|
347
405
|
# already -- the bare form made every commit exit 1 with no output at all.
|
|
348
|
-
_gr=0;
|
|
406
|
+
_gr=0; _crew_scan iE "$pat" "$TRACE_F" || _gr=$?
|
|
349
407
|
if [ "$_gr" = 0 ]; then
|
|
350
408
|
echo "TRACE-SCANNER: forbidden expression in added code -> '$pat' (§4.1/§4.2)"
|
|
351
409
|
HIT=1
|
|
352
|
-
elif [ "$_gr"
|
|
410
|
+
elif [ "$_gr" = 2 ]; then
|
|
353
411
|
echo "TRACE-SCANNER: /$pat/ in $BL is not a valid regular expression, so it matches NOTHING. Every other pattern still ran; fix or remove this line."
|
|
412
|
+
elif [ "$_gr" -ge 3 ]; then
|
|
413
|
+
echo "TRACE-SCANNER: the look for '$pat' could not run, so the added lines are not known to be clean and the commit is stopped (§4.1/§4.2)."
|
|
414
|
+
HIT=1
|
|
354
415
|
fi
|
|
355
416
|
done < "$BL"
|
|
356
417
|
fi
|
|
@@ -361,11 +422,14 @@ if [ -f "$SL" ] && [ -s "$SECRET_F" ] && [ "$_dec" != 1 ]; then
|
|
|
361
422
|
pat="${pat%$'\r'}" # tolerate a CRLF blocklist (Windows/autocrlf checkout)
|
|
362
423
|
case "$pat" in ''|\#*) continue;; esac
|
|
363
424
|
if [ -n "$SAL" ] && grep -qxF -- "$pat" "$SAL"; then continue; fi
|
|
364
|
-
_gr=0;
|
|
425
|
+
_gr=0; _crew_scan E "$pat" "$SECRET_F" || _gr=$?
|
|
365
426
|
if [ "$_gr" = 0 ]; then
|
|
366
427
|
echo "SECRET-SCANNER: a staged line matches a secret pattern -> /$pat/"
|
|
367
428
|
HIT=1
|
|
368
|
-
elif [ "$_gr" -ge
|
|
429
|
+
elif [ "$_gr" -ge 3 ]; then
|
|
430
|
+
echo "SECRET-SCANNER: the look for /$pat/ could not run, so the staged lines are not known to be clean and the commit is stopped."
|
|
431
|
+
HIT=1
|
|
432
|
+
elif [ "$_gr" = 2 ]; then
|
|
369
433
|
# Same reasoning as the trace loop above; the comment there is the long form.
|
|
370
434
|
echo "SECRET-SCANNER: /$pat/ in $SL is not a valid regular expression, so it matches NOTHING. Every other pattern still ran; fix or remove this line."
|
|
371
435
|
fi
|
|
@@ -411,11 +475,17 @@ if [ -s "$PTERM_F" ] && [ -s "$SECRET_F" ] && [ "$_dec" != 1 ]; then
|
|
|
411
475
|
term="${term%$'\r'}"
|
|
412
476
|
[ -n "$term" ] || continue
|
|
413
477
|
if [ -n "$PAL" ] && grep -qxF -- "$term" "$PAL"; then continue; fi
|
|
414
|
-
|
|
478
|
+
_gr=0; _crew_scan iF "$term" "$SECRET_F" || _gr=$?
|
|
479
|
+
if [ "$_gr" = 0 ]; then
|
|
415
480
|
# The term is echoed back deliberately: unlike a secret, the author has to see WHICH private string to
|
|
416
481
|
# replace, and it is already sitting in their own working tree.
|
|
417
482
|
echo "PRIVATE-PATH SCANNER: a staged line contains a machine-private string -> '$term' (§4.3)"
|
|
418
483
|
HIT=1
|
|
484
|
+
elif [ "$_gr" -ge 2 ]; then
|
|
485
|
+
# This loop used to read every exit status but 0 as "no match": under a Turkish locale in Git Bash grep -iF
|
|
486
|
+
# aborted (134) for each term and the commit went through with both private strings in it (measured).
|
|
487
|
+
echo "PRIVATE-PATH SCANNER: the look for a machine-private string could not run, so the staged lines are not known to be clean and the commit is stopped (§4.3)."
|
|
488
|
+
HIT=1
|
|
419
489
|
fi
|
|
420
490
|
done < "$PTERM_F"
|
|
421
491
|
fi
|
|
@@ -535,7 +605,14 @@ EOF_FLOOR_CAND2
|
|
|
535
605
|
esac
|
|
536
606
|
_gr=0; hits="$(grep -nE -- "$pat" "$FLOOR_F" 2>/dev/null)" || _gr=$?
|
|
537
607
|
if [ "$_gr" -ge 2 ]; then
|
|
538
|
-
|
|
608
|
+
# Does the pattern compile? Tried against empty input: 1 = it does, so it was the look that failed.
|
|
609
|
+
_gv=0; grep -qE -- "$pat" /dev/null 2>/dev/null || _gv=$?
|
|
610
|
+
if [ "$_gv" = 2 ]; then
|
|
611
|
+
echo "FLOOR-GUARD: /$pat/ in $FL is not a valid regular expression, so it matches NOTHING. Every other pattern still ran; fix or remove this line."
|
|
612
|
+
else
|
|
613
|
+
echo "FLOOR-GUARD: the look for /$pat/ could not run, so the staged lines are not known to be clean and the commit is stopped."
|
|
614
|
+
HIT=1
|
|
615
|
+
fi
|
|
539
616
|
continue
|
|
540
617
|
fi
|
|
541
618
|
[ -n "$hits" ] || continue
|