Hook Reference Required git hooks organized by stage (pre-commit, commit-msg, pre-push) and kind (shell, python module, makefile target).
Pre-commit15 Commit-msg2 Pre-push4
Strict21 POC (safety)12
21 of 21 hooks
Fails the commit if there are unstaged or untracked files, auto-staging all changes so the developer can re-commit without losing work. Prevents partial commits that leave the working tree in an inconsistent state. The developer simply re-runs git commit after the auto-stage completes.
Pre-commit Shell Safety tier
Applicable to any ci_check_unstaged() lib/checks_core.sh --- ci_check_unstaged --- Fails the commit if there are unstaged or untracked files, auto-staging all changes so the developer can re-commit without losing work. Prevents partial commits that leave the working tree in an inconsistent state. The developer simply re-runs git commit after the auto-stage completes.
Stage pre-commit
Kind shell
Mandatory Yes
Safety tier Yes (POC)
Pass filenames No
Always run Yes
Applicable to any Copy code ci_check_unstaged () {
local untracked
untracked = $( git ls-files --others --exclude-standard )
if [[ -n "$( git diff --stat )" ]] || [ -n " $untracked " ]; then
echo ""
ci_fail "Unstaged or untracked files detected, auto-staging now."
git diff
local add_rc = 0
git add -A || add_rc = $?
if [[ $add_rc -ne 0 ]]; then
ci_fail "Auto-stage FAILED: git add -A exited $add_rc . Nothing was staged; stage manually and re-run."
return 1
fi
if [ -n " $untracked " ]; then
echo ""
ci_info "Untracked files being staged:"
echo " $untracked " | sed 's/^/ /'
fi
echo ""
ci_info "All changes staged. Re-run: git commit"
return 1
fi
return 0
} Blocks commit if any staged file matches sensitive extensions or keywords from config/sensitive_files.yaml. Enforces an allowlist of safe file types and supports per-repo exception lists via sensitive_files_exceptions.yaml. Prevents accidental leakage of credentials, keys, and environment files into version control.
Pre-commit Shell Safety tier
Applicable to any ci_block_sensitive_files() lib/checks_files.sh
Stage pre-commit
Kind shell
Mandatory Yes
Safety tier Yes (POC)
Pass filenames Yes
Always run No
Applicable to any Copy code ci_block_sensitive_files () {
_load_sensitive_config || { ci_fail "Config not found: $( ci_config_path sensitive_files)" ; return 1 ; }
local errors = 0
local sensitive_files = ()
local f _files = ()
ci_capture_lines _files -- ci_file_list " $@ "
for f in "${ _files [ @ ]}" ; do
[[ -z " $f " ]] && continue
if _is_sensitive " $f " ; then
sensitive_files += ( " $f " )
errors = $(( errors + 1 ))
fi
done
if [[ $errors -gt 0 ]]; then
echo ""
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
ci_fail "Sensitive files detected!"
echo "The following files match patterns forbidden for commitment:"
for sf in "${ sensitive_files [ @ ]}" ; do
echo " - $sf "
done
echo ""
echo "REASON: Files with sensitive extensions matching '.' or keywords"
echo "are blocked to prevent accidental secret leakage."
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
return 1
fi
return 0
} Scans all non-gitignored files (tracked + untracked) for leaked secrets using gitleaks in parallel across the full file set. No .gitleaks.toml is needed because each repo's .gitignore serves as the sole filter. Redacts secret values in output to prevent re-exposure in CI logs.
Pre-commit Shell Safety tier
Applicable to any ci_scan_secrets() lib/checks_secrets.sh !/usr/bin/env bash CI Secret Scanning: dynamic gitleaks wrapper. Sourced by checks.sh. Requires ci.sh to be loaded first. Scans ALL non-gitignored files (tracked + untracked) using gitleaks. No .gitleaks.toml needed: each repo's .gitignore is the sole filter. git ls-files gives tracked files; --others --exclude-standard gives untracked non-ignored files. Together = all files on disk minus gitignored. --- ci_scan_secrets --- Scans all non-gitignored files (tracked + untracked) for leaked secrets using gitleaks in parallel across the full file set. No .gitleaks.toml is needed because each repo's .gitignore serves as the sole filter. Redacts secret values in output to prevent re-exposure in CI logs.
Stage pre-commit
Kind shell
Mandatory Yes
Safety tier Yes (POC)
Pass filenames No
Always run Yes
Applicable to any Copy code ci_scan_secrets () {
local _ss_gitleaks_bin
_ss_gitleaks_bin = "$( command -v gitleaks)" || {
ci_fail "gitleaks not found on PATH"
return 1
}
# Dynamic .gitignore respect: git ls-files gives tracked files,
# --others --exclude-standard gives untracked non-ignored files.
local _ss_stderr_tmp _ss_files_tmp
_ss_stderr_tmp = "$( mktemp )"
_ss_files_tmp = "$( mktemp )"
local _ss_git_rc = 0
{ git ls-files -z ; git ls-files -z --others --exclude-standard ; } \
2 > " $_ss_stderr_tmp " | sort -zu > " $_ss_files_tmp " || _ss_git_rc = $?
if [[ $_ss_git_rc -ne 0 ]]; then
ci_fail "git ls-files failed"
[[ -s " $_ss_stderr_tmp " ]] && cat " $_ss_stderr_tmp " >&2
rm -f " $_ss_stderr_tmp " " $_ss_files_tmp "
return 1
fi
rm -f " $_ss_stderr_tmp "
if [[ ! -s " $_ss_files_tmp " ]]; then
ci_pass "gitleaks: no files to scan"
rm -f " $_ss_files_tmp "
return 0
fi
local _ss_total
_ss_total = $( tr -cd '\0' < " $_ss_files_tmp " | wc -c )
local _ss_rc = 0
# xargs -P4 parallelizes gitleaks across files for speed. Batching
# multiple paths per invocation was measured 150x SLOWER (gitleaks
# scans multi-path argv concurrently and thrashes), so one path per
# process stays. --log-level=error suppresses gitleaks INF/WRN status
# on stderr (redundant with exit code); real errors still surface.
# --verbose sends Finding blocks to stdout for the developer to see.
xargs -0 -P4 -I {} " $_ss_gitleaks_bin " dir {} \
--no-banner --redact --verbose --log-level=error \
< " $_ss_files_tmp " || _ss_rc = $?
rm -f " $_ss_files_tmp "
if [[ $_ss_rc -ne 0 ]]; then
if [[ $_ss_rc -eq 124 || $_ss_rc -eq 137 ]]; then
ci_fail "gitleaks: TIMED OUT (exit $_ss_rc , scanned $_ss_total files)"
elif [[ $_ss_rc -eq 139 || $_ss_rc -eq 245 ]]; then
ci_fail "gitleaks: CRASHED (SIGSEGV, exit $_ss_rc , scanned $_ss_total files)"
else
ci_fail "gitleaks: secrets found or error (exit $_ss_rc , scanned $_ss_total files)"
fi
return 1
fi
ci_pass "gitleaks: no leaks found ( $_ss_total files)"
return 0
} Scans all tracked files for silent error-swallowing patterns across Python, JavaScript/TypeScript, shell, and cron. Detects bare except/catch blocks, stderr-to-null redirects, and cron entries without log redirection using AST-based analysis. Blocks the commit so developers must re-raise, log, or handle errors explicitly. FAIL-CLOSED INVARIANT: ci_run_python_checker guarantees that ANY non-zero Python exit code (violations, crash, timeout, missing config) increments the error counter. There is no code path where a non-zero exit is swallowed as a clean pass. The prior bug (line 130 AND-gate on stdout) is eliminated by structural design: the wrapper returns 1 on every non-zero child exit, and this function increments errors on every wrapper non-zero return.
Pre-commit Shell Safety tier
Applicable to any ci_check_silent_swallow() lib/checks_silent.sh !/usr/bin/env bash CI Silent-Error-Swallow check. Sourced by checks.sh. Requires ci.sh to be loaded first. Motivation: silent except/catch/pipe-true patterns (and cron lines without a log redirect) are a common source of production failures. This check scans ALL tracked files. Every swallowed error is a bug. --- ci_check_silent_swallow --- Scans all tracked files for silent error-swallowing patterns across Python, JavaScript/TypeScript, shell, and cron. Detects bare except/catch blocks, stderr-to-null redirects, and cron entries without log redirection using AST-based analysis. Blocks the commit so developers must re-raise, log, or handle errors explicitly. FAIL-CLOSED INVARIANT: ci_run_python_checker guarantees that ANY non-zero Python exit code (violations, crash, timeout, missing config) increments the error counter. There is no code path where a non-zero exit is swallowed as a clean pass. The prior bug (line 130 AND-gate on stdout) is eliminated by structural design: the wrapper returns 1 on every non-zero child exit, and this function increments errors on every wrapper non-zero return.
Stage pre-commit
Kind shell
Mandatory Yes
Safety tier Yes (POC)
Pass filenames No
Always run Yes
Applicable to any Copy code ci_check_silent_swallow () {
local files_tmp combined_tmp diff_tmp stderr_tmp
files_tmp = "$( mktemp )"
combined_tmp = "$( mktemp )"
diff_tmp = "$( mktemp )"
stderr_tmp = "$( mktemp )"
# Git-repo check: capture stderr to an explicit temp file. On rc!=0 we print
# the captured stderr (real signal, no swallow) and skip the check.
local _gd_rc = 0 _git_dir = ""
_git_dir = "$( git rev-parse --git-dir 2> " $stderr_tmp ")" || _gd_rc = $?
if [[ $_gd_rc -ne 0 ]]; then
ci_pass "Silent-swallow: not a git repo, skipping."
[[ -s " $stderr_tmp " ]] && cat " $stderr_tmp " >&2
rm -f " $files_tmp " " $combined_tmp " \
" $diff_tmp " " $stderr_tmp "
return 0
fi
rm -f " $stderr_tmp "
local script_path = "${ CI_LIB_DIR :- $( cd "$( dirname "${ BASH_SOURCE [0]}")" && pwd )}/check_silent_swallow.py"
if [[ ! -f " $script_path " ]]; then
ci_fail "Silent-swallow: helper script not found at $script_path "
rm -f " $files_tmp " " $combined_tmp " " $diff_tmp "
return 1
fi
# Config file validation: fail fast if the pattern config is missing.
# This prevents entering the per-file loop where every file would crash
# with FileNotFoundError. The Python checker (ci_paths.find_config_dir)
# also resolves this via __file__ walk-up, but checking here gives a
# clear, immediate error message.
local _config_file
_config_file = "$( ci_config_path silent_swallow_patterns)" || {
ci_fail "Silent-swallow: failed to resolve config path"
rm -f " $files_tmp " " $combined_tmp " " $diff_tmp "
return 1
}
if [[ ! -f " $_config_file " ]]; then
ci_fail "Silent-swallow: config not found at $_config_file "
rm -f " $files_tmp " " $combined_tmp " " $diff_tmp "
return 1
fi
# Build file list: ALL tracked files (not just staged). Every swallowed
# error is a bug, regardless of which file triggered the commit.
# Pathspec filtering is delegated to the Python classifier
# (lib/check_silent_swallow_system.py::is_shell_file) so extensionless
# scripts (scripts/bootstrap-uv, scripts/audit-workspace, etc.) are
# scanned per the §3.7 "hooks scan ALL files: no exceptions" rule.
git ls-files --cached --others --exclude-standard > " $files_tmp "
if [[ ! -s " $files_tmp " ]]; then
ci_pass "Silent-swallow: no scannable files found."
rm -f " $files_tmp " " $combined_tmp " " $diff_tmp "
return 0
fi
# Read per-project exceptions from config/silent_swallow_exceptions.yaml.
# Format mirrors banned_words_exceptions.yaml:
# exceptions:
# - paths: ['public/vendor/', 'other/path']
# Provenance is validated fail-closed (root-owned regular file).
local -a _exc_paths = ()
local _exc_cfg = "config/silent_swallow_exceptions.yaml"
if ! ci_validate_exemption_file " $_exc_cfg " "silent_swallow_exceptions.yaml" ; then
rm -f " $files_tmp " " $combined_tmp " " $diff_tmp "
return 1
fi
if [[ -f " $_exc_cfg " ]]; then
local _exc_tmp
_exc_tmp = "$( mktemp )"
sed -n "s/^\s*-\s*[' \" ]\(.*[^' \" ]\)[' \" ]\s*$/\1/p" " $_exc_cfg " > " $_exc_tmp "
while IFS = read -r _pat ; do
[[ -n " $_pat " ]] && _exc_paths += ( " $_pat " )
done < " $_exc_tmp "
rm -f " $_exc_tmp "
fi
# Stage 1: pure-bash candidate filter (no subprocess per file).
local cand_tmp text_tmp
cand_tmp = "$( mktemp )"
text_tmp = "$( mktemp )"
local file _excluded _pat
while IFS = read -r file ; do
[[ -z " $file " || ! -f " $file " ]] && continue
# Default exemptions: tests may contain deliberate error patterns;
# compose.yml has pre-existing patterns fixed incrementally.
case " $file " in
tests/ * ) continue ;;
res/ansible/compose.yml ) continue ;;
esac
# Per-project exemptions from config/silent_swallow_exceptions.yaml
_excluded = 0
for _pat in "${ _exc_paths [ @ ]}" ; do
if [[ " $file " == " $_pat " * ]]; then
_excluded = 1
break
fi
done
[[ $_excluded -eq 1 ]] && continue
printf '%s\n' " $file " >> " $cand_tmp "
done < " $files_tmp "
rm -f " $files_tmp "
# Stage 2: batched binary filter. One `file` process for the whole
# candidate list (was: one per file) -- skip non-UTF-8 files to prevent
# UnicodeDecodeError in the Python checker.
if [[ -s " $cand_tmp " ]]; then
if ! file --mime-encoding -b -f " $cand_tmp " > " $text_tmp .enc" ; then
echo "[silent-swallow] file --mime-encoding batch failed" >&2
fi
paste " $cand_tmp " " $text_tmp .enc" | \
awk -F '\t' '$2=="utf-8"||$2=="us-ascii"{print $1}' > " $text_tmp "
rm -f " $text_tmp .enc"
fi
rm -f " $cand_tmp "
# Stage 3: ONE combined multi-file diff, ONE Python checker process
# (was: one Python spawn per file). parse_diff tracks the current file
# via the +++ b/<path> headers, so violation attribution is preserved.
# Chunked via xargs to stay under argv limits on large trees.
local errors = 0
if [[ -s " $text_tmp " ]]; then
: > " $diff_tmp "
xargs -a " $text_tmp " -n 512 awk '
FNR==1 {
print "--- a/" FILENAME
print "+++ b/" FILENAME
print "@@ -0,0 +1 @@"
}
{ print "+" $0 }
' >> " $diff_tmp "
rm -f " $text_tmp "
# FAIL-CLOSED: ci_run_python_checker returns 0 IFF the Python
# checker exits 0. Any non-zero exit (violations, crash, timeout,
# missing config) returns 1. There is no code path where a
# non-zero child exit is swallowed as a clean pass.
ci_run_python_checker " $script_path " < " $diff_tmp "
local _chk_rc = $?
if [[ $_chk_rc -ne 0 ]]; then
if [[ -s " $CI_CHECKER_STDOUT " ]]; then
cat " $CI_CHECKER_STDOUT " >> " $combined_tmp "
else
echo "[CHECKER CRASHED exit= $_chk_rc ] -- see stderr above" >> " $combined_tmp "
fi
errors = 1
fi
rm -f " $CI_CHECKER_STDOUT "
else
rm -f " $text_tmp "
fi
if [[ $errors -eq 0 ]]; then
ci_pass "Silent-swallow: no silent-error patterns found."
rm -f " $combined_tmp " " $diff_tmp "
return 0
fi
echo ""
ci_fail "Silent-swallow: violations found:"
echo ""
cat " $combined_tmp "
echo ""
echo "Silent-error swallowing breaks production debuggability."
echo "Re-raise, log, or handle the error explicitly."
echo ""
rm -f " $combined_tmp " " $diff_tmp "
return 1
} Compares the staged coverage_thresholds.yaml against HEAD and fails the commit if any suite's min_coverage value is being lowered. Thresholds are earned and may only stay the same or increase over time. An exception applies when a suite's path changes, since the new path tests a different set of code.
Pre-commit Shell Safety tier
Applicable to any ci_check_coverage_thresholds_no_devolution() lib/checks_coverage.sh --- ci_check_coverage_thresholds_no_devolution --- Compares the staged coverage_thresholds.yaml against HEAD and fails the commit if any suite's min_coverage value is being lowered. Thresholds are earned and may only stay the same or increase over time. An exception applies when a suite's path changes, since the new path tests a different set of code.
Stage pre-commit
Kind shell
Mandatory Yes
Safety tier Yes (POC)
Pass filenames No
Always run Yes
Applicable to any Copy code ci_check_coverage_thresholds_no_devolution () {
local config = "config/coverage_thresholds.yaml"
if [[ ! -f " $config " ]]; then
ci_pass "No $config : nothing to guard."
return 0
fi
if ! _git_dir = "$( git rev-parse --git-dir 2>&1 )" ; then
ci_pass "Not a git repo, skipping."
return 0
fi
if [[ -z "$( git diff --cached --stat -- " $config ")" ]]; then
ci_pass "Coverage thresholds: unchanged, OK."
return 0
fi
# Parse HEAD version: suite keys are at column 0 (no indent)
# Track both min_coverage and path per suite.
declare -A old_thresholds old_paths
local _head_output _suite = "" _val = ""
_head_output = "$( git show HEAD:" $config ")"
if [[ $? -eq 0 ]]; then
while IFS = read -r line ; do
if echo " $line " | grep -qE '^[a-z][a-z_]*:' ; then
_suite = "$( echo " $line " | cut -d: -f1 )"
elif echo " $line " | grep -q 'min_coverage:' ; then
_val = "$( echo " $line " | sed 's/.*min_coverage:[[:space:]]*//')"
if [[ -n " $_suite " && -n " $_val " ]]; then
old_thresholds[ " $_suite " ] = " $_val "
fi
elif echo " $line " | grep -q '^\s*path:' ; then
_val = "$( echo " $line " | sed 's/.*path:[[:space:]]*//')"
if [[ -n " $_suite " && -n " $_val " ]]; then
old_paths[ " $_suite " ] = " $_val "
fi
fi
done <<< " $_head_output "
fi
# Parse staged version
declare -A new_thresholds new_paths
local _staged_output
_suite = "" _val = ""
_staged_output = "$( git show :" $config ")"
if [[ $? -eq 0 ]]; then
while IFS = read -r line ; do
if echo " $line " | grep -qE '^[a-z][a-z_]*:' ; then
_suite = "$( echo " $line " | cut -d: -f1 )"
elif echo " $line " | grep -q 'min_coverage:' ; then
_val = "$( echo " $line " | sed 's/.*min_coverage:[[:space:]]*//')"
if [[ -n " $_suite " && -n " $_val " ]]; then
new_thresholds[ " $_suite " ] = " $_val "
fi
elif echo " $line " | grep -q '^\s*path:' ; then
_val = "$( echo " $line " | sed 's/.*path:[[:space:]]*//')"
if [[ -n " $_suite " && -n " $_val " ]]; then
new_paths[ " $_suite " ] = " $_val "
fi
fi
done <<< " $_staged_output "
fi
local violations = 0
for suite in "${ ! old_thresholds [ @ ]}" ; do
local _old = "${ old_thresholds [ $suite ]}"
local _new = "${ new_thresholds [ $suite ] :- }"
if [[ -z " $_new " ]]; then
ci_fail "Coverage devolution: $suite removed (was ${ _old }%)"
violations = $(( violations + 1 ))
continue
fi
local _old_path = "${ old_paths [ $suite ] :- }"
local _new_path = "${ new_paths [ $suite ] :- }"
if [[ " $_old_path " != " $_new_path " ]]; then
ci_info "Coverage suite $suite restructured"
ci_info " path: ${ _old_path } -> ${ _new_path }"
ci_info " threshold ${ _old }% -> ${ _new }% (path changed, threshold"
ci_info " comparison skipped: different test set)"
continue
fi
if [[ " $_new " -lt " $_old " ]]; then
ci_fail "Coverage devolution: $suite lowered from ${ _old }% to ${ _new }%"
violations = $(( violations + 1 ))
fi
done
if [[ $violations -ne 0 ]]; then
echo ""
ci_fail "COVERAGE DE-EVOLUTION BLOCKED: Thresholds must only increase."
echo " Lowering coverage thresholds is forbidden."
echo " Raise the actual coverage to the target level, not the threshold."
return 1
fi
ci_pass "Coverage thresholds: no de-evolution detected."
return 0
} Delegates to lib/check_banned_words.py to scan all tracked files for banned patterns defined in config/banned_words.yaml. Replaces a prior bash+AWK implementation that spawned ~33,000 subprocesses under PRoot and took 5+ minutes; the Python version does zero subprocess spawns and completes in <1s. Per-project exemptions are supported via config/banned_words_exceptions.yaml with granular path and pattern scoping.
Pre-commit Shell Safety tier
Applicable to any ci_check_banned_words() lib/checks_core.sh --- ci_check_banned_words [files...] --- Delegates to lib/check_banned_words.py to scan all tracked files for banned patterns defined in config/banned_words.yaml. Replaces a prior bash+AWK implementation that spawned ~33,000 subprocesses under PRoot and took 5+ minutes; the Python version does zero subprocess spawns and completes in <1s. Per-project exemptions are supported via config/banned_words_exceptions.yaml with granular path and pattern scoping.
Stage pre-commit
Kind shell
Mandatory Yes
Safety tier Yes (POC)
Pass filenames No
Always run No
Applicable to any Copy code ci_check_banned_words () {
local config
config = "$( ci_config_path banned_words)" || return 1
if [[ ! -f " $config " ]]; then
ci_fail "Config not found: $config "
return 1
fi
local script_path = "${ CI_LIB_DIR :- $( cd "$( dirname "${ BASH_SOURCE [0]}")" && pwd )}/check_banned_words.py"
if [[ ! -f " $script_path " ]]; then
ci_fail "Banned-words: helper not found at $script_path "
return 1
fi
# ci_uv_run preserves the caller cwd; still pin the consuming repo
# explicitly for git ls-files and per-project exceptions via CI_SCAN_ROOT.
local _scan_root = "${ CI_SCAN_ROOT :- $PWD }"
CI_SCAN_ROOT = " $_scan_root " \
CI_CONFIG_DIR=" $CI_CONFIG_DIR " \
ci_uv_run " $script_path " " $@ " < /dev/null
} Commit-msg hook that enforces conventional-commit format on the first line, a blank second line, and a mandatory body explaining what changed and why. Validates the type prefix against feat, fix, refactor, docs, test, chore, ci, perf, style, build, and revert. Auto-generated git messages such as merge and squash are allowed through without validation.
Commit-msg Shell (with arg) Safety tier
Applicable to any ci_check_commit_message() lib/checks_commit.sh --- ci_check_commit_message <commit-msg-file> --- Commit-msg hook that enforces conventional-commit format on the first line, a blank second line, and a mandatory body explaining what changed and why. Validates the type prefix against feat, fix, refactor, docs, test, chore, ci, perf, style, build, and revert. Auto-generated git messages such as merge and squash are allowed through without validation.
Stage commit-msg
Kind shell_with_arg
Mandatory Yes
Safety tier Yes (POC)
Pass filenames No
Always run Yes
Applicable to any Copy code ci_check_commit_message () {
local msg_file = " ${1 :- } "
if [[ -z " $msg_file " || ! -f " $msg_file " ]]; then
ci_fail "No commit message file provided or file not found."
return 1
fi
# Strip comment lines (git -v adds diff as comments).
local first_line = "" second_line = ""
local -a lines = ()
while IFS = read -r line ; do
[[ " $line " == \# * ]] && continue
lines += ( " $line " )
done < " $msg_file "
first_line = "${ lines [0] :- }"
second_line = "${ lines [1] :- }"
if [[ -z " $first_line " ]]; then
ci_fail "Commit message is empty."
return 1
fi
# Allow auto-generated git messages (merge, revert, fixup, squash)
if [[ " $first_line " =~ ^(Merge \ | Revert \ \" | fixup ! \ | squash ! \ ) ]]; then
return 0
fi
# First line must match "type: description"
local valid_types = "^(feat|fix|refactor|docs|test|chore|ci|perf|style|build|revert): .+"
if ! [[ " $first_line " =~ $valid_types ]]; then
echo ""
ci_fail "Bad commit message format."
echo " Got: $first_line "
echo " Expected: type: description"
echo " Types: feat fix refactor docs test chore ci perf style build revert"
echo ""
echo " Example: feat: add websocket reconnection logic"
return 1
fi
# Second line MUST be blank
if [[ -n " $second_line " ]]; then
echo ""
ci_fail "Second line of commit message must be blank."
echo " Got: ' $second_line '"
echo ""
echo " Format:"
echo " type: description"
echo " (blank line)"
echo " body with details..."
return 1
fi
# Body is MANDATORY
local body = ""
for i in "${ ! lines [ @ ]}" ; do
(( i < 2 )) && continue
if [[ -n "${ lines [ $i ]}" && ! "${ lines [ $i ]}" =~ ^[[:space:]] * $ ]]; then
body = "${ lines [ $i ]}"
break
fi
done
if [[ -z " $body " ]]; then
echo ""
ci_fail "Commit message body is required."
echo " Title-only commits are not allowed."
echo " Explain WHAT changed and WHY after the blank line."
echo ""
echo " Format:"
echo " type: description"
echo " (blank line)"
echo " body with details..."
return 1
fi
return 0
} Commit-msg hook that validates the commit message against blocked patterns from config/blocked_commit_patterns.yaml. Catches AI-attribution markers and other forbidden text that must never enter commit history. Prints the configured reason for each match so the developer knows exactly what to remove.
Commit-msg Shell (with arg) Safety tier
Applicable to any ci_block_coauthored() lib/checks_commit.sh --- ci_block_coauthored <commit-msg-file> --- Commit-msg hook that validates the commit message against blocked patterns from config/blocked_commit_patterns.yaml. Catches AI-attribution markers and other forbidden text that must never enter commit history. Prints the configured reason for each match so the developer knows exactly what to remove.
Stage commit-msg
Kind shell_with_arg
Mandatory Yes
Safety tier Yes (POC)
Pass filenames No
Always run Yes
Applicable to any Copy code ci_block_coauthored () {
local msg_file = " ${1 :- } "
if [[ -z " $msg_file " || ! -f " $msg_file " ]]; then
ci_fail "No commit message file provided or file not found."
return 1
fi
_load_blocked_commit_patterns || return 1
local _msg
_msg = "$( cat " $msg_file ")"
if ! _check_message_blocked " $_msg " ; then
echo ""
echo "Remove blocked patterns from your commit message."
return 1
fi
return 0
} Pre-push hook that scans every commit in the actual push range for blocked patterns from config/blocked_commit_patterns.yaml. Reads push refs from stdin per the git pre-push protocol and computes the exact set of new commits being pushed. Blocks the push if any commit contains a violation, instructing the developer to rewrite history via interactive rebase.
Pre-push Shell Safety tier
Applicable to any ci_block_coauthored_history() lib/checks_commit.sh --- ci_block_coauthored_history --- Pre-push hook that scans every commit in the actual push range for blocked patterns from config/blocked_commit_patterns.yaml. Reads push refs from stdin per the git pre-push protocol and computes the exact set of new commits being pushed. Blocks the push if any commit contains a violation, instructing the developer to rewrite history via interactive rebase.
Stage pre-push
Kind shell
Mandatory Yes
Safety tier Yes (POC)
Pass filenames No
Always run Yes
Applicable to any Copy code ci_block_coauthored_history () {
_load_blocked_commit_patterns || return 1
local _zero = "0000000000000000000000000000000000000000"
local _all_commits = ""
# Read ALL push refs from stdin before entering the loop. If we
# read line-by-line inside the while-read, the $(git log ...)
# command substitutions inherit the pipe's stdin and hold the read
# end open. After the loop exits, the pipe is NOT at EOF because
# git's subprocess still holds it. The second while-read loop's
# $(git log -1 ...) calls then inherit the still-open pipe and
# the function hangs indefinitely. Reading stdin into a variable
# first and feeding the loop via here-string breaks the chain.
local _push_refs
_push_refs = "$( cat )"
while read -r _local_ref _local_sha _remote_ref _remote_sha ; do
[[ -z " $_local_sha " ]] && continue
# Deleted branch, nothing to check
[[ " $_local_sha " == " $_zero " ]] && continue
if [[ " $_remote_sha " == " $_zero " ]]; then
_all_commits += "$( git log --format=%H " $_local_sha " --not --remotes )"$' \n '
elif git merge-base --is-ancestor " $_remote_sha " " $_local_sha " ; then
_all_commits += "$( git log --format=%H " $_remote_sha .. $_local_sha ")"$' \n '
else
_all_commits += "$( git log --format=%H " $_local_sha " --not --remotes )"$' \n '
fi
done <<< " $_push_refs "
local _commits
_commits = "$( echo " $_all_commits " | sort -u | grep -v '^$')"
if [[ -z " $_commits " ]]; then
ci_pass "No new commits to check."
return 0
fi
local _violations = 0
while IFS = read -r _sha ; do
[[ -z " $_sha " ]] && continue
local _msg _short _subject
_msg = "$( git log -1 --format=%B " $_sha ")"
_short = "$( git log -1 --format=%h " $_sha ")"
_subject = "$( git log -1 --format=%s " $_sha ")"
for _i in "${ ! _BLOCKED_PATTERNS [ @ ]}" ; do
if echo " $_msg " | grep -qiE "${ _BLOCKED_PATTERNS [ $_i ]}" ; then
ci_fail " $_short $_subject -- ${ _BLOCKED_REASONS [ $_i ]}"
_violations = $(( _violations + 1 ))
break
fi
done
done <<< " $_commits "
if [[ $_violations -gt 0 ]]; then
echo ""
ci_fail " $_violations commit(s) with blocked patterns found in push range."
echo "Rewrite history to remove them before pushing:"
echo " git rebase -i <base>"
return 1
fi
ci_pass "No blocked patterns in push range."
return 0
} Enforces that no process substitution (< <(...), > >(...), <(...), >(...)) appears in shell scripts under lib/ and scripts/. Process substitution opens /dev/fd/NN by path, which breaks under PRoot, bwrap/firejail sandboxes, and chroots without /proc. Scans comment lines and string literals to avoid false positives where the pattern appears as prose.
Pre-commit Shell Safety tier
Applicable to any ci_check_portable_shell() lib/checks_files.sh --- ci_check_portable_shell [files...] --- Enforces that no process substitution (< <(...), > >(...), <(...), >(...)) appears in shell scripts under lib/ and scripts/. Process substitution opens /dev/fd/NN by path, which breaks under PRoot, bwrap/firejail sandboxes, and chroots without /proc. Scans comment lines and string literals to avoid false positives where the pattern appears as prose.
Stage pre-commit
Kind shell
Mandatory Yes
Safety tier Yes (POC)
Pass filenames No
Always run Yes
Applicable to any Copy code ci_check_portable_shell () {
local has_errors = 0
local _ci_root
_ci_root = "$( cd "$( dirname "${ BASH_SOURCE [0]}")/.." && pwd )"
# Collect shell files: lib/*.sh + scripts/* (extensionless scripts included)
local _shell_files = ()
local _f
for _f in " $_ci_root "/lib/*.sh ; do
[[ -f " $_f " ]] && _shell_files += ( " $_f " )
done
for _f in " $_ci_root "/scripts/* ; do
[[ -f " $_f " ]] || continue
# Skip non-shell files (yaml, json, md, .txt, directories)
case "$( basename " $_f ")" in
* .yaml |* .yml |* .json |* .md |* .txt |* .toml ) continue ;;
esac
_shell_files += ( " $_f " )
done
local violations = ()
local _file _line _lineno
# Regex patterns stored in variables to avoid bash interpreting < > as
# operators inside [[ =~ ]]. [<] and [>] match literal angle brackets in ERE.
local _re_dup_in = '[<][[:space:]]*[<][(]'
local _re_dup_out = '[>][[:space:]]*[>][(]'
local _re_pipe_in = '[|][[:space:]]*[<][(]'
local _re_pipe_out = '[|][[:space:]]*[>][(]'
local _re_standalone_in = '[[:space:]][<][(][^)]*[)][[:space:]]'
local _re_standalone_out = '[[:space:]][>][(][^)]*[)][[:space:]]?$'
for _file in "${ _shell_files [ @ ]}" ; do
_lineno = 0
while IFS = read -r _line ; do
_lineno = $(( _lineno + 1 ))
# Skip comment lines (the ci.sh doc comment mentions < <(cmd) as prose)
[[ " $_line " =~ ^[[:space:]] * # ]] && continue
# Detect process substitution: < <(...), > >(...), | <(...), | >(...),
# and standalone <(...) / >(...) as command arguments.
local _pat
for _pat in " $_re_dup_in " " $_re_dup_out " " $_re_pipe_in " \
" $_re_pipe_out " " $_re_standalone_in " \
" $_re_standalone_out " ; do
if [[ " $_line " =~ $_pat ]]; then
violations += ( " $_file : $_lineno : $_line " )
has_errors = 1
break
fi
done
done < " $_file "
done
if [[ $has_errors -eq 1 ]]; then
ci_fail "Process substitution found (not portable across virtualization layers):"
echo " Use ci_capture_lines / ci_capture_pipe from lib/ci.sh instead." >&2
echo "" >&2
for v in "${ violations [ @ ]}" ; do
echo " $v " >&2
done
return 1
fi
ci_pass "No process substitution in shell scripts."
return 0
} Fails the commit if any source file exceeds the line limit defined in config/file_length_limits.yaml. The default limit is 512 lines with support for per-file overrides in the config. Scans .py, .sh, .js, .ts, .tsx, .rs, .css, and .lua files, skipping ignored directories.
Pre-commit Shell Strict only
Applicable to any ci_check_file_length() lib/checks_files.sh --- ci_check_file_length [files...] --- Fails the commit if any source file exceeds the line limit defined in config/file_length_limits.yaml. The default limit is 512 lines with support for per-file overrides in the config. Scans .py, .sh, .js, .ts, .tsx, .rs, .css, and .lua files, skipping ignored directories.
Stage pre-commit
Kind shell
Mandatory Yes
Safety tier No (strict only)
Pass filenames No
Always run Yes
Applicable to any Copy code ci_check_file_length () {
local config
config = "$( ci_config_path file_length_limits "./config/file_length_limits.yaml")" || return 1
if [[ -f " $config " ]]; then
ci_validate_exemption_file " $config " "file_length_limits.yaml" || return 1
fi
local max_lines = 512
local exts = ( .py .sh .js .ts .tsx .rs .css .lua )
local errors = 0
# Read config if available
if [[ -f " $config " ]]; then
local cfg_max
cfg_max = "$( ci_read_yaml " $config " "max_lines")"
[[ -n " $cfg_max " ]] && max_lines = " $cfg_max "
local cfg_exts = ()
ci_capture_lines cfg_exts -- ci_read_yaml_list " $config " "extensions"
[[ ${ # cfg_exts[ @ ]} -gt 0 ]] && exts = ( "${ cfg_exts [ @ ]}" )
# Read per-file overrides: path -> max_lines
declare -A _fl_ovr = ()
local _fl_ovr_data
_fl_ovr_data = "$( awk '
/^overrides:/ { in_ov=1; path=""; next }
!in_ov { next }
/^[^ ]/ { exit }
/path:/ { path=$0; sub(/^[[:space:]]*[^:]*:[[:space:]]*/,"",path); gsub(/^["'"'"']|["'"'"']$/,"",path); gsub(/[[:space:]]+$/,"",path); next }
/max_lines:/ { max=$0; sub(/^[[:space:]]*[^:]*:[[:space:]]*/,"",max); gsub(/[^0-9]/,"",max); if(path!=""&&max!="") { print path,max; path="" } }
' " $config ")"
while IFS = ' ' read -r _fl_opath _fl_omax ; do
[[ -n " $_fl_opath " && -n " $_fl_omax " ]] && _fl_ovr[ " $_fl_opath " ] = " $_fl_omax "
done <<< " $_fl_ovr_data "
fi
ci_info "Checking file lengths (max $max_lines lines)..."
local violations = ()
local f lines fl_limit _fl_files = ()
ci_capture_pipe _fl_files 'ci_file_list "$@" | ci_filter_ext "${exts[@]}"' " $@ "
for f in "${ _fl_files [ @ ]}" ; do
[[ -z " $f " || ! -f " $f " ]] && continue
_in_ignored_dir " $f " && continue
lines = "$( wc -l < " $f ")"
# Check per-file override
fl_limit = " $max_lines "
if [[ ${ # _fl_ovr[ @ ]} -gt 0 ]]; then
for fl_path in "${ ! _fl_ovr [ @ ]}" ; do
[[ " $f " == " $fl_path " || " $f " == * "/ $fl_path " ]] && { fl_limit = "${ _fl_ovr [ $fl_path ]}" ; break ; }
done
fi
if [[ $lines -gt $fl_limit ]]; then
local excess = $(( lines - fl_limit ))
violations += ( " $f : $lines : $excess " )
errors = $(( errors + 1 ))
fi
done
if [[ $errors -gt 0 ]]; then
echo ""
ci_fail " $errors file(s) exceed their line limits:"
echo ""
# Sort by line count descending
printf '%s\n' "${ violations [ @ ]}" | sort -t: -k2 -rn | while IFS = : read -r vf vlines vexcess ; do
echo -e " ${ _BOLD }${ vf }${ _NC }: ${ vlines } lines (+${ vexcess } over limit)"
done
echo ""
return 1
fi
ci_pass "All files within $max_lines line limit."
return 0
} Self-check that validates the CI hook infrastructure of the invoking project across three invariants. Ensures every check_*.py module is registered in required_hooks.yaml, quality_exceptions.yaml is schema-valid, and all applicable mandatory hooks are rendered in .git/hooks. Tier-aware so POC tier checks only the safety subset while vendored tier passes trivially.
Pre-commit Python module Strict only
Applicable to any ci.check_required_hooks_present ci/check_required_hooks_present.py Self-check that validates the CI hook infrastructure of the invoking project across three invariants. Ensures every check_*.py module is registered in required_hooks.yaml, quality_exceptions.yaml is schema-valid, and all applicable mandatory hooks are rendered in .git/hooks. Tier-aware so POC tier checks only the safety subset while vendored tier passes trivially.
Stage pre-commit
Kind python_module
Mandatory Yes
Safety tier No (strict only)
Pass filenames No
Always run Yes
Applicable to any Copy code def main (argv: list[ str ] | None = None ) -> int :
args = _build_arg_parser().parse_args(argv)
project_dir = args.project.resolve()
workspace_root = _find_workspace_root(project_dir)
if workspace_root is None :
print (
f " {RED} error: {RESET} cannot find workspace root from { project_dir } " ,
)
return EXIT_INFRA_ERROR
manifest = _load_manifest(workspace_root)
if manifest is None :
print ( f " {RED} error: {RESET} required_hooks.yaml not found in workspace" )
return EXIT_INFRA_ERROR
rel = _project_path_relative(workspace_root, project_dir) or "."
tier = "strict" if rel == "." else _resolve_tier(workspace_root, rel)
project_name = project_dir.name
print ( f "Self-check: { project_name } (tier= { tier } )" )
if tier == "vendored" :
if not args.quiet:
_emit( "OK" , "vendored tier: wrapper passthrough, no contract" )
return EXIT_OK
issues: list[ str ] = []
issues.extend(
_run_invariant_1_manifest(
project_name,
workspace_root,
manifest,
quiet = args.quiet,
),
)
issues.extend(
_run_invariant_2_exceptions(
project_dir,
tier,
manifest,
quiet = args.quiet,
),
)
hook_issues = _check_hooks_rendered(project_dir, manifest, tier)
if hook_issues:
issues.extend(hook_issues)
elif not args.quiet:
_emit( "OK" , f "all applicable mandatory hooks rendered ( { tier } tier)" )
if issues:
print ()
for issue in issues:
_emit( "FAIL" , issue)
print ()
print ( f " {RED}{len (issues) } violation(s) {RESET} : see fix instructions above." )
return EXIT_VIOLATION
if not args.quiet:
print ( f " {GREEN} OK {RESET} { project_name } : contract satisfied." )
return EXIT_OK Non-blocking audit of the platform-aware boot-layout contract.
Pre-commit Python module Safety tier
Applicable to any ci.check_boot_venv_layout ci/check_boot_venv_layout.py Non-blocking audit of the platform-aware boot-layout contract.
Stage pre-commit
Kind python_module
Mandatory No (exemptable)
Safety tier Yes (POC)
Pass filenames No
Always run Yes
Applicable to any Copy code def main () -> int :
parser = argparse.ArgumentParser(
prog = "check-boot-venv-layout" ,
description = (
"Non-blocking audit of the boot-layout contract (SPEC-BOOT-LAYOUT)."
),
)
parser.add_argument(
"project_dir" ,
nargs = "?" ,
default = "." ,
help = "Project dir to audit (default: current dir)." ,
)
args = parser.parse_args()
project_dir = Path(args.project_dir).resolve( strict = False )
findings: list[tuple[ str , str ]] = []
# Checks 1+2: moon.yml existence + parse
moon_path, moon, found = _check_moon_exists(project_dir)
findings.extend(found)
if moon_path is None or moon is None :
_emit_and_exit(findings)
return EXIT_OK
# Checks 3+4: inherited_boot_dirs resolution + world-writable
findings.extend(_check_inherited_boot_dirs(moon, project_dir))
# Check 5: dependsOn alignment
findings.extend(_check_dependson_alignment(moon))
# Check 6: .pre-commit-config.yaml --project refs
findings.extend(_check_precommit_venv_python_refs(project_dir))
findings.extend(_check_precommit_project_refs(project_dir))
# Check 7: summary
_emit_and_exit(findings)
return EXIT_OK Runs ruff format and ruff lint with auto-fix on all ci/ modules. Catches style violations, import sorting issues, and unused variables before they reach the remote. Acts as the first stage of the pre-commit quality gate.
Pre-commit Makefile target Strict only
Applicable to any lint Makefile Runs ruff format and ruff lint with auto-fix on all ci/ modules. Catches style violations, import sorting issues, and unused variables before they reach the remote. Acts as the first stage of the pre-commit quality gate.
Stage pre-commit
Kind makefile_target
Mandatory Yes
Safety tier No (strict only)
Pass filenames No
Always run Yes
Applicable to any Copy code lint : ## Runs ruff format and ruff lint with auto-fix on all ci/ modules. Catches style violations, import sorting issues, and unused variables before they reach the remote. Acts as the first stage of the pre-commit quality gate.
$( MAKE ) _lint-impl Runs mypy strict type checking on all ci/ Python modules. Catches type errors, missing annotations, and incompatible signatures before they can break downstream consumers. Acts as the second stage of the pre-commit quality gate after lint passes.
Pre-commit Makefile target Strict only
Applicable to any type-check Makefile Runs mypy strict type checking on all ci/ Python modules. Catches type errors, missing annotations, and incompatible signatures before they can break downstream consumers. Acts as the second stage of the pre-commit quality gate after lint passes.
Stage pre-commit
Kind makefile_target
Mandatory Yes
Safety tier No (strict only)
Pass filenames No
Always run Yes
Applicable to any Copy code type-check : ## Runs mypy strict type checking on all ci/ Python modules. Catches type errors, missing annotations, and incompatible signatures before they can break downstream consumers. Acts as the second stage of the pre-commit quality gate after lint passes.
$( MAKE ) _type-check-impl Single-pass pre-push gate running ruff lint, mypy, shell unit tests, pytest with per-suite coverage, and web/ JS quality (eslint + tsc + vitest) in one invocation. Eliminates the previous redundancy where the same tests ran two to three times across separate targets. Fails the push if any lint, type, test, or coverage threshold check does not pass.
Pre-push Makefile target Strict only
Applicable to any check-push Makefile Single-pass pre-push gate running ruff lint, mypy, shell unit tests, pytest with per-suite coverage, and web/ JS quality (eslint + tsc + vitest) in one invocation. Eliminates the previous redundancy where the same tests ran two to three times across separate targets. Fails the push if any lint, type, test, or coverage threshold check does not pass.
Stage pre-push
Kind makefile_target
Mandatory Yes
Safety tier No (strict only)
Pass filenames No
Always run Yes
Applicable to any Copy code check-push : ## Single-pass pre-push gate running ruff lint, mypy, shell unit tests, pytest with per-suite coverage, and web/ JS quality (eslint + tsc + vitest) in one invocation. Eliminates the previous redundancy where the same tests ran two to three times across separate targets. Fails the push if any lint, type, test, or coverage threshold check does not pass.
$( MAKE ) _lint-impl && $( MAKE ) _type-check-impl && $( MAKE ) _test-push-impl Resolves the dangle binary (PATH, then $HOME/.cargo/bin/dangle), runs it over the whole git-tracked repository, parses the file:line:col: kind name is not referenced output, and post-filters the candidates against config/dead_code.yaml: scan_paths (allowlist) keep only items whose file is under one ignore_paths (denylist) drop items whose file is under one reference_only_paths (denylist for reporting, still cited as references) ignored_names (exact-name allowlist) ignored_name_patterns (regex-name allowlist) Reports survivors as warnings and returns 1 if any are found; returns 0 when the repo is clean or dangle/config is unavailable (non-blocking). Dangle's own test-file heuristic (test_ prefix or /tests/ segment) already suppresses reporting there; reference_only_paths covers any extras.
Pre-push Shell Strict only
Applicable to any ci_check_dead_code() lib/checks_dead_code.sh --- ci_check_dead_code --- Resolves the `dangle` binary (PATH, then $HOME/.cargo/bin/dangle), runs it over the whole git-tracked repository, parses the `file:line:col: kind name is not referenced` output, and post-filters the candidates against config/dead_code.yaml: scan_paths (allowlist) keep only items whose file is under one ignore_paths (denylist) drop items whose file is under one reference_only_paths (denylist for reporting, still cited as references) ignored_names (exact-name allowlist) ignored_name_patterns (regex-name allowlist) Reports survivors as warnings and returns 1 if any are found; returns 0 when the repo is clean or dangle/config is unavailable (non-blocking). Dangle's own test-file heuristic (`test_` prefix or `/tests/` segment) already suppresses reporting there; reference_only_paths covers any extras.
Stage pre-push
Kind shell
Mandatory No (exemptable)
Safety tier No (strict only)
Pass filenames No
Always run Yes
Applicable to any Copy code ci_check_dead_code () {
local _dc_cfg
_dc_cfg = "$( ci_config_path dead_code "./config/dead_code.yaml")" || return 0
if [[ ! -f " $_dc_cfg " ]]; then
ci_warn "dead_code.yaml not found; skipping dead-code check"
return 0
fi
ci_validate_exemption_file " $_dc_cfg " "dead_code.yaml" || return 1
local _dc_bin = ""
if _dc_bin = "$( command -v dangle 2>&1 )" ; then
:
elif [[ -x "${ HOME }/.cargo/bin/dangle" ]]; then
_dc_bin = "${ HOME }/.cargo/bin/dangle"
else
ci_warn "dangle not installed; skipping dead-code check (cargo install dangle)"
return 0
fi
ci_info "Dead Code Analysis (dangle, non-blocking)..."
ci_info " Config: $_dc_cfg "
ci_info " Binary: $_dc_bin "
local _dc_scan_paths = () _dc_ignore_paths = () _dc_ref_only_paths = ()
local _dc_ignored_names = () _dc_ignored_name_patterns = ()
_dc_load_config_lists _dc_scan_paths scan_paths " $_dc_cfg "
_dc_load_config_lists _dc_ignore_paths ignore_paths " $_dc_cfg "
_dc_load_config_lists _dc_ref_only_paths reference_only_paths " $_dc_cfg "
_dc_load_config_lists _dc_ignored_names ignored_names " $_dc_cfg "
_dc_load_config_lists _dc_ignored_name_patterns ignored_name_patterns " $_dc_cfg "
# Default scan_paths to "ci" when the key is empty, mirroring the YAML.
if [[ ${ # _dc_scan_paths[ @ ]} -eq 0 ]]; then
_dc_scan_paths = ( "ci" )
fi
local _dc_out_tmp _dc_err_tmp
_dc_out_tmp = "$( mktemp )"
_dc_err_tmp = "$( mktemp )"
local _dc_rc = 0
" $_dc_bin " > " $_dc_out_tmp " 2> " $_dc_err_tmp " || _dc_rc = $?
# dangle returns 0 even when dead candidates are found; treat anything
# greater than 1 as a tool crash (config error, parse failure, etc.).
if [[ $_dc_rc -gt 1 ]]; then
ci_warn "dangle crashed (exit $_dc_rc ):"
sed 's/^/ /' " $_dc_err_tmp " >&2
rm -f " $_dc_out_tmp " " $_dc_err_tmp "
return 0
fi
local _dc_violations = ()
local _dc_line _dc_file _dc_kind _dc_name _dc_lineno _dc_col
while IFS = read -r _dc_line ; do
[[ -z " $_dc_line " ]] && continue
# Parse "file:line:col: kind name is not referenced".
if [[ " $_dc_line " =~ ^([^ : ] + ):([ 0 - 9 ] + ):([ 0 - 9 ] + ): \ ([a - zA - Z] + ) \ ([^[ : space : ]] + ) \ is \ not \ referenced$ ]]; then
_dc_file = "${ BASH_REMATCH [1]}"
_dc_lineno = "${ BASH_REMATCH [2]}"
_dc_col = "${ BASH_REMATCH [3]}"
_dc_kind = "${ BASH_REMATCH [4]}"
_dc_name = "${ BASH_REMATCH [5]}"
else
continue
fi
_dc_in_paths " $_dc_file " _dc_scan_paths || continue
if _dc_in_paths " $_dc_file " _dc_ignore_paths ; then
continue
fi
if _dc_in_paths " $_dc_file " _dc_ref_only_paths ; then
continue
fi
if _dc_name_ignored " $_dc_name " ; then
continue
fi
_dc_violations += ( " $_dc_file : $_dc_lineno : $_dc_kind : $_dc_name " )
done < " $_dc_out_tmp "
rm -f " $_dc_out_tmp " " $_dc_err_tmp "
if [[ ${ # _dc_violations[ @ ]} -eq 0 ]]; then
ci_pass "No dead code detected (dangle)."
return 0
fi
echo ""
ci_warn "Dead code candidates (${ # _dc_violations [ @ ]}):"
local _dc_v
for _dc_v in "${ _dc_violations [ @ ]}" ; do
local _dc_f _dc_l _dc_k _dc_n
IFS = : read -r _dc_f _dc_l _dc_k _dc_n <<< " $_dc_v "
printf ' %s:%s %s %s\n' " $_dc_f " " $_dc_l " " $_dc_k " " $_dc_n "
done
echo ""
ci_info "Note: check-dead-code is advisory; dead code does not block the push."
return 1
} Scans the repo for supported lockfiles (uv.lock, package-lock.json, pnpm-lock.yaml, yarn.lock, bun.lock, Cargo.lock, go.mod) and checks them against the live OSV.dev vulnerability database. Exit contract: 0: scan completed with no findings, OR scan unavailable (offline, binary missing) -- unavailability is always a loud WARN. 1: scan completed and found vulnerabilities. Environment: OSV_SCANNER_FORMAT : "json" emits the scanner JSON report (default: human-readable vertical output). OSV_SCANNER_OUTPUT : file path for JSON report (default: stdout).
Pre-push Shell Strict only
Applicable to any ci_scan_vulnerabilities() lib/checks_security.sh !/usr/bin/env bash CI Dependency Vulnerability Scanning: osv-scanner wrapper. Sourced by checks.sh. Requires ci.sh to be loaded first. Implements REQ-CVE-SCAN FR-2: recursive lockfile scan against the live OSV.dev database. Fail-closed on findings, fail-open (WARN, exit 0) when the scanner or network is unavailable so offline development is never blocked. --- ci_scan_vulnerabilities --- Scans the repo for supported lockfiles (uv.lock, package-lock.json, pnpm-lock.yaml, yarn.lock, bun.lock, Cargo.lock, go.mod) and checks them against the live OSV.dev vulnerability database. Exit contract: 0: scan completed with no findings, OR scan unavailable (offline, binary missing) -- unavailability is always a loud WARN. 1: scan completed and found vulnerabilities. Environment: OSV_SCANNER_FORMAT : "json" emits the scanner JSON report (default: human-readable vertical output). OSV_SCANNER_OUTPUT : file path for JSON report (default: stdout).
Stage pre-push
Kind shell
Mandatory No (exemptable)
Safety tier No (strict only)
Pass filenames No
Always run Yes
Applicable to any Copy code ci_scan_vulnerabilities () {
local _vs_bin
_vs_bin = "$( command -v osv-scanner)" || {
ci_warn "osv-scanner not found on PATH; vulnerability scan SKIPPED"
echo " Fix: run scripts/bootstrap-osv-scanner (or make install)" >&2
return 0
}
local _vs_root
_vs_root = "$( git rev-parse --show-toplevel )"
# Discover supported lockfiles, excluding binary/vendored trees.
local _vs_lockfiles
_vs_lockfiles = "$( find " $_vs_root " \
\( -name .git -o -name " $CI_BOOT_NAME " -o -name .venv \
-o -name node_modules -o -name dist -o -name build \
-o -name .next \) -prune \
-o -type f \( -name uv.lock -o -name package-lock.json \
-o -name pnpm-lock.yaml -o -name yarn.lock -o -name bun.lock \
-o -name Cargo.lock -o -name go.mod \) -print )"
if [[ -z " $_vs_lockfiles " ]]; then
ci_pass "vulnerability scan: no supported lockfiles; skipped"
return 0
fi
local _vs_count
_vs_count = "$( printf '%s\n' " $_vs_lockfiles " | grep -c .)"
local -a _vs_args = ( scan source )
local _vs_lf
while IFS = read -r _vs_lf ; do
_vs_args += ( --lockfile = " $_vs_lf " )
done <<< " $_vs_lockfiles "
if [[ "${ OSV_SCANNER_FORMAT :- }" == "json" ]]; then
_vs_args += ( --format = json )
if [[ -n "${ OSV_SCANNER_OUTPUT :- }" ]]; then
_vs_args += ( --output-file = " $OSV_SCANNER_OUTPUT " )
fi
else
_vs_args += ( --format = vertical )
fi
if [[ -f " $_vs_root /osv-scanner.toml" ]]; then
_vs_args += ( --config = " $_vs_root /osv-scanner.toml" )
fi
local _vs_err _vs_rc = 0
_vs_err = "$( mktemp )"
" $_vs_bin " "${ _vs_args [ @ ]}" 2> " $_vs_err " || _vs_rc = $?
if [[ " $_vs_rc " -eq 0 ]]; then
ci_pass "vulnerability scan: no known vulnerabilities in $_vs_count lockfile(s)"
rm -f " $_vs_err "
return 0
fi
# Distinguish findings (fail-closed) from infrastructure/network
# failure (fail-open). osv-scanner exits 1 for vulnerabilities;
# higher codes indicate operational errors. Network signatures are
# also matched on stderr for exit-1 runs that failed mid-scan.
if [[ " $_vs_rc " -gt 1 ]] || grep -qiE 'dial tcp|no such host|connection refused|timeout|temporary failure|network is unreachable|EOF' " $_vs_err " ; then
ci_warn "vulnerability scan UNAVAILABLE (offline or scanner error); SKIPPED"
cat " $_vs_err " >&2
rm -f " $_vs_err "
return 0
fi
ci_fail "vulnerability scan: advisories found in dependencies (scan output above)"
echo " Fix: upgrade affected packages, or suppress via osv-scanner.toml with a reason." >&2
rm -f " $_vs_err "
return 1
} Dependency version checker (PyPI, npm, Docker). Enforces strict pinning (==X.Y.Z or >=X.Y,<A.B) and verifies every pin is the latest from its registry. Supports --upgrade mode to auto-fix outdated dependencies.
Pre-commit Python module Strict only
Applicable to any ci.check_dependency_versions ci/check_dependency_versions.py Dependency version checker (PyPI, npm, Docker). Enforces strict pinning (==X.Y.Z or >=X.Y,<A.B) and verifies every pin is the latest from its registry. Supports --upgrade mode to auto-fix outdated dependencies.
Stage pre-commit
Kind python_module
Mandatory Yes
Safety tier No (strict only)
Pass filenames No
Always run Yes
Applicable to any Copy code def main () -> int :
parser = argparse.ArgumentParser(
description = "Check and optionally upgrade dependency versions"
)
parser.add_argument(
"--upgrade" ,
action = "store_true" ,
help = "Automatically upgrade outdated/loose deps to latest versions" ,
)
parser.add_argument(
"--exclude" ,
type = str ,
default = "" ,
help = "Comma-separated list of packages to exclude from checking" ,
)
parser.add_argument(
"paths" ,
nargs = "*" ,
default = [ "pyproject.toml" , "web/package.json" ],
help = "Paths to check (pyproject.toml, package.json, "
"docker-compose.yml, Dockerfile)" ,
)
args = parser.parse_args()
cli_excludes = {x.strip().lower() for x in args.exclude.split( "," ) if x.strip()}
pypi_excludes = cli_excludes | load_config_excludes()
npm_excludes = cli_excludes | load_config_excludes( "npm_excludes" )
docker_excludes = cli_excludes | load_config_excludes( "docker_excludes" )
dep_errors = False
upgrade_count = 0
found_count = 0
for path_str in args.paths:
path = Path(path_str)
if not path.exists():
print ( f "Warning: { path } not found, skipping" )
continue
found_count += 1
if path.name == "package.json" :
excludes = npm_excludes
elif docker.is_compose_file(path) or docker.is_dockerfile(path):
excludes = docker_excludes
else :
excludes = pypi_excludes
errors, upgraded = _check_path(path, excludes, upgrade = args.upgrade)
dep_errors = dep_errors or errors
upgrade_count += upgraded
if args.upgrade and upgrade_count > 0 :
print ( f "Updated { upgrade_count } dependencies." )
return 0
if dep_errors:
print ( " \n Run with --upgrade to auto-fix." )
return 1
if found_count == 0 :
print ( "ERROR: No dependency files found to check." )
return 1
f = _QUERY_RESULTS [ "failures" ]
s = _QUERY_RESULTS [ "successes" ]
if f > 0 and s == 0 :
print ( "ERROR: All registry queries failed: network may be offline." )
print ( "Fix by running with a working internet connection." )
return 1
if f > s:
msg = f "WARNING: { f } query failures vs { s } successes"
print (msg + ": check may be incomplete." )
print ( "All dependencies are strictly pinned and up to date." )
return 0 Duplicate and redundant dependency checker for uv workspaces that detects two classes of issues in pyproject.toml. Flags self-duplicates where the same package appears in multiple dependency sections with version mismatches as errors. Warns on redundant dependencies where the workspace root declares a package already provided by a workspace member.
Pre-commit Python module Strict only
Applicable to python ci.check_duplicate_dependencies ci/check_duplicate_dependencies.py Duplicate and redundant dependency checker for uv workspaces that detects two classes of issues in pyproject.toml. Flags self-duplicates where the same package appears in multiple dependency sections with version mismatches as errors. Warns on redundant dependencies where the workspace root declares a package already provided by a workspace member.
Stage pre-commit
Kind python_module
Mandatory No (exemptable)
Safety tier No (strict only)
Pass filenames No
Always run Yes
Applicable to python Copy code def main () -> int :
parser = argparse.ArgumentParser(
description = "Check for duplicate and redundant dependencies"
)
parser.add_argument( "--json" , action = "store_true" , help = "Output results as JSON" )
parser.add_argument(
"--workspace-root" ,
type = Path,
default = None ,
help = "Path to workspace root (default: auto-detect)" ,
)
args = parser.parse_args()
root = args.workspace_root or find_workspace_root()
excludes = _load_excludes()
all_issues: list[DuplicateIssue] = []
root_toml = root / "pyproject.toml"
if root_toml.exists():
all_issues.extend(check_self_duplicates(root_toml))
members = _get_workspace_member_paths(root)
for member_path in members.values():
member_toml = member_path / "pyproject.toml"
if member_toml.exists():
all_issues.extend(check_self_duplicates(member_toml))
if members:
all_issues.extend(check_redundant(root, members, excludes))
if args.json:
output = [{ "kind" : i.kind, "message" : i.message} for i in all_issues]
print (json.dumps(output, indent = 2 ))
return 1 if all_issues else 0
else :
if all_issues:
errors = sum ( 1 for i in all_issues if i.kind == "SELF_DUPLICATE_MISMATCH" )
warnings = len (all_issues) - errors
print (
f " \n DUPLICATE/REDUNDANT DEPENDENCIES FOUND "
f "( { errors } errors, { warnings } warnings) \n "
)
for issue in all_issues:
prefix = "ERROR" if issue.kind == "SELF_DUPLICATE_MISMATCH" else "WARN"
print ( f " [ { prefix } ] { issue.message } " )
print ()
if errors > 0 :
print (
"Version-mismatch duplicates are BLOCKING. "
"Fix conflicting version constraints. \n "
)
return 1
print (
"Same-version duplicates and redundancies are "
"WARNINGS (non-blocking). Review and clean up if "
"unnecessary. \n "
)
return 0
print ( "No duplicate or redundant dependencies found." )
return 0 Markdown documentation validator that walks .md files and checks every link and image target against the local filesystem and remote endpoints. Detects broken file links, missing section anchors, dead URLs, and unresolvable image references. Supports --check-remote for live HTTP verification and --all-md for scanning every tracked markdown file in the repo.
Pre-commit Python module (files) Safety tier
Applicable to any ci.check_markdown_docs --check-remote ci/check_markdown_docs.py Markdown documentation validator that walks .md files and checks every link and image target against the local filesystem and remote endpoints. Detects broken file links, missing section anchors, dead URLs, and unresolvable image references. Supports --check-remote for live HTTP verification and --all-md for scanning every tracked markdown file in the repo.
Stage pre-commit
Kind python_module_files
Mandatory Yes
Safety tier Yes (POC)
Pass filenames Yes
Always run No
Applicable to any
File types markdown Copy code def main () -> None :
sys.exit(run())