Skip to content
workspaceguardrails
  • Home
  • Open Source[4]
  • System Policies[0]
  • Git Hooks[21]
  • Anti-Patterns[293]
  • Hook Configs[20]
  • Sandbox Configs[27]
  • Tools & Scripts[14]
  • AI Governance[20]
  • LLM Gateway
  • Static Analysis
  • Playground
  • Integration Guide
  1. Home
  2. Git Hooks
    The Digital and AI Workspace Guardrails Platform2026 ◆ Independent AI Labs
    Skip to content
    workspaceguardrails
    • Home
    • Open Source[4]
    • System Policies[0]
    • Git Hooks[21]
    • Anti-Patterns[293]
    • Hook Configs[20]
    • Sandbox Configs[27]
    • Tools & Scripts[14]
    • AI Governance[20]
    • LLM Gateway
    • Static Analysis
    • Playground
    • Integration Guide
    1. Home
    2. Git Hooks

      Hook Reference

      The Digital and AI Workspace Guardrails Platform2026 ◆ Independent AI Labs
      Skip to content
      workspaceguardrails
      • Home
      • Open Source[4]
      • System Policies[0]
      • Git Hooks[21]
      • Anti-Patterns[293]
      • Hook Configs[20]
      • Sandbox Configs[27]
      • Tools & Scripts[14]
      • AI Governance[20]
      • LLM Gateway
      • Static Analysis
      • Playground
      • Integration Guide
      1. Home
      2. Git Hooks

        Hook Reference

        Required git hooks organized by stage (pre-commit, commit-msg, pre-push) and kind (shell, python module, makefile target).

        21 of 21 hooks

        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.

        Pre-commitShellSafety 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
        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
        }

        Share feedback

        You voted thumbs down

        block-sensitive-files

        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-commitShellSafety 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
        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
        }

        Share feedback

        You voted thumbs down

        gitleaks

        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-commitShellSafety 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
        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
        }

        Share feedback

        You voted thumbs down

        ci-check-error-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.

        Pre-commitShellSafety 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
        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
        }

        Share feedback

        You voted thumbs down

        check-coverage-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.

        Pre-commitShellSafety 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
        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
        }

        Share feedback

        You voted thumbs down

        check-banned-words

        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-commitShellSafety 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
        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
        }

        Share feedback

        You voted thumbs down

        check-commit-message

        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-msgShell (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
        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
        }

        Share feedback

        You voted thumbs down

        block-coauthored

        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-msgShell (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
        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
        }

        Share feedback

        You voted thumbs down

        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.

        Pre-pushShellSafety 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
        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
        }

        Share feedback

        You voted thumbs down

        check-portable-shell

        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-commitShellSafety 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
        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
        }

        Share feedback

        You voted thumbs down

        check-file-length

        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-commitShellStrict 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
        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
        }

        Share feedback

        You voted thumbs down

        check-required-hooks-present

        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-commitPython moduleStrict 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
        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

        Share feedback

        You voted thumbs down

        check-boot-venv-layout

        Non-blocking audit of the platform-aware boot-layout contract.

        Pre-commitPython moduleSafety 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
        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

        Share feedback

        You voted thumbs down

        ci-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.

        Pre-commitMakefile targetStrict 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
        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

        Share feedback

        You voted thumbs down

        ci-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.

        Pre-commitMakefile targetStrict 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
        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

        Share feedback

        You voted thumbs down

        ci-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.

        Pre-pushMakefile targetStrict 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
        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

        Share feedback

        You voted thumbs down

        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.

        Pre-pushShellStrict 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
        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
        }

        Share feedback

        You voted thumbs down

        osv-scan

        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-pushShellStrict 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
        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
        }

        Share feedback

        You voted thumbs down

        check-dependency-versions

        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-commitPython moduleStrict 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
        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("\nRun 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

        Share feedback

        You voted thumbs down

        check-duplicate-dependencies

        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-commitPython moduleStrict 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
        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"\nDUPLICATE/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

        Share feedback

        You voted thumbs down

        check-markdown-docs

        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-commitPython 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
        def main() -> None:
            sys.exit(run())

        Share feedback

        You voted thumbs down

        The Digital and AI Workspace Guardrails Platform2026 ◆ Independent AI Labs