Skip to content
workspaceguardrails
  • Home
  • Open Source[4]
  • System Policies[0]
  • Git Hooks[25]
  • Anti-Patterns[300]
  • Hook Configs[20]
  • Sandbox Configs[27]
  • Tools & Scripts[12]
  • 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[25]
    • Anti-Patterns[300]
    • Hook Configs[20]
    • Sandbox Configs[27]
    • Tools & Scripts[12]
    • 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[25]
      • Anti-Patterns[300]
      • Hook Configs[20]
      • Sandbox Configs[27]
      • Tools & Scripts[12]
      • 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).

        25 of 25 hooks

        check-unstaged

        Auto-stages all unstaged or untracked changes before the remaining hooks run. Prevents partial commits that leave the working tree in an inconsistent state. Single-command flow (AGENTS.md Code §8): git decides "anything to commit?" from its pre-hook index snapshot, so a hook-time add cannot make git count content the user never staged (stock git 2.43 semantics, proven 2026-08-24). When nothing was pre-staged, this check stages everything, then fails with instructions naming the cause; the same commit command re-run succeeds because the index now carries the content. When something IS pre-staged, hook-time staging rides the commit via git's post-hook index re-read.

        Pre-commitShellStrict only
        Applicable to
        any

        ci_check_unstaged()

        lib/checks_core.sh

        --- ci_check_unstaged --- Auto-stages all unstaged or untracked changes before the remaining hooks run. Prevents partial commits that leave the working tree in an inconsistent state. Single-command flow (AGENTS.md Code §8): git decides "anything to commit?" from its pre-hook index snapshot, so a hook-time add cannot make git count content the user never staged (stock git 2.43 semantics, proven 2026-08-24). When nothing was pre-staged, this check stages everything, then fails with instructions naming the cause; the same commit command re-run succeeds because the index now carries the content. When something IS pre-staged, hook-time staging rides the commit via git's post-hook index re-read.

        Stage
        pre-commit
        Kind
        shell
        Mandatory
        Yes
        Safety tier
        No (strict only)
        Pass filenames
        No
        Always run
        Yes
        Applicable to
        any
        ci_check_unstaged() {
            local untracked pre_staged
            untracked=$(git ls-files --others --exclude-standard)
            if [[ -z "$(git diff --stat)" ]] && [[ -z "$untracked" ]]; then
                return 0
            fi
            # Was anything staged before this commit attempt began? git's
            # committable check counts only pre-hook staging; hook staging here
            # cannot save an otherwise-empty commit.
            pre_staged=1
            if git diff --cached --quiet; then
                pre_staged=0
            fi
            echo ""
            ci_info "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
                # Self-diagnosing failure: capture the capability and PATH
                # context of this exact hook process so a permission fault
                # identifies itself instead of needing post-hoc replay.
                echo "" >&2
                ci_info "Auto-stage diagnostics (hook exec context):" >&2
                grep -E 'Cap(Eff|Bnd|Amb)|NoNewPrivs' /proc/self/status | sed 's/^/  /' >&2
                ci_info "  git resolves to: $(command -v git)" >&2
                getcap "$(command -v git)" 2>&1 | sed 's/^/  caps: /' >&2
                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
            if [[ $pre_staged -eq 0 ]]; then
                echo ""
                ci_fail "Nothing was pre-staged: git cannot count hook-time staging (pre-hook index snapshot)."
                echo "  All changes are now staged. Re-run the SAME git commit command; it succeeds."
                return 1
            fi
            echo ""
            ci_info "All changes staged; continuing commit."
            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-commitShellStrict only
        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
        No (strict only)
        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.
            # Final model (v5): every entry is one anchored exact file path with full
            # provenance, mirroring banned_words_exceptions_v5.yaml. No prefix matching,
            # no directory-wide skips, no hardcoded exemptions.
            # 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)"
                # Each entry carries its exact path on a dedicated "path: ^...$" line
                # (first key of the entry, so possibly "- path: ..."); plain scalar
                # extraction keeps parsing inside reviewed bash tooling.
                sed -n -E "s/^[[:space:]]*-?[[:space:]]*path:[[:space:]]*['\"]?(\^.*\\\$)['\"]?[[:space:]]*\$/\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
            while IFS= read -r file; do
                [[ -z "$file" || ! -f "$file" ]] && continue
                # Per-project exact-file exemptions only; every other tracked text
                # file is scanned (no directory or extension skips exist).
                local _excluded=0 _pat
                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-commitShellStrict only
        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
        No (strict only)
        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_v5.yaml with granular path and pattern scoping.

        Pre-commitShellStrict only
        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_v5.yaml with granular path and pattern scoping.

        Stage
        pre-commit
        Kind
        shell
        Mandatory
        Yes
        Safety tier
        No (strict only)
        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)Strict only
        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
        No (strict only)
        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
        
            ci_check_security_commit_disclosure "$msg_file" || return 1
        
            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)Strict only
        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
        No (strict only)
        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-pushShellStrict only
        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
        No (strict only)
        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-commitShellStrict only
        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
        No (strict only)
        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 and quality_exceptions.yaml is schema-valid.

        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 and quality_exceptions.yaml is schema-valid.

        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
        
            project_name = project_dir.name
        
            print(f"Self-check: {project_name}")
        
            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,
                    manifest,
                    quiet=args.quiet,
                ),
            )
            rendered_issues = _check_hooks_rendered(project_dir, manifest)
            issues.extend(rendered_issues)
            if not rendered_issues and not args.quiet:
                _emit("OK", "rendered hooks carry required markers")
        
            entry_issues = _run_invariant_4_entries(manifest, quiet=args.quiet)
            issues.extend(entry_issues)
        
            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 moduleStrict only
        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
        No (strict only)
        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 unit 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 unit 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 unit 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) _policy-impl && $(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

        ci_scan_vulnerabilities

        Pre-pushShellStrict only
        Applicable to
        any

        ci_scan_vulnerabilities()

        lib/checks_security.sh

        !/usr/bin/env bash Bounded, fail-closed dependency vulnerability scan.

        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 root scanner config validator lockfiles output errors rc=0 size first_lockfile
            root="$(git rev-parse --show-toplevel)" || return 1
            scanner="$(ci_resolve_tool_path "$root" osv-scanner)" || {
                ci_fail "osv-scanner is required"
                return 1
            }
            config="$root/osv-scanner.toml"
            validator="$CI_PROJECT_ROOT/ci/validate_osv_config.py"
            [[ -f "$config" ]] || { ci_fail "missing OSV configuration: $config"; return 1; }
            if ! ci_uv_run "$validator" "$config"; then
                ci_fail "invalid OSV suppression configuration"
                return 1
            fi
        
            lockfiles="$(mktemp)"
            output="$(mktemp)"
            errors="$(mktemp)"
            trap 'rm -f "$lockfiles" "$output" "$errors"' RETURN
            find "$root" \
                \( -name .git -o -name "$CI_BOOT_NAME" -o -name .venv -o -name node_modules -o -name target -o -name .next \) -prune \
                -o -type f \( -name uv.lock -o -name package-lock.json -o -name Cargo.lock -o -name go.mod \) -print \
                | sort > "$lockfiles"
            [[ -s "$lockfiles" ]] || { ci_fail "vulnerability scan found no lockfiles"; return 1; }
        
            # Refresh is visible but non-authoritative; validation below is always offline.
            # osv-scanner rejects --download-offline-databases unless offline mode is on,
            # and each ecosystem database is fetched only when its lockfile is scanned,
            # so refresh every lockfile with a cold-download-sized timeout.
            while IFS= read -r first_lockfile; do
                timeout --signal=TERM --kill-after=10s 300s "$scanner" scan source \
                    --offline --offline-vulnerabilities --download-offline-databases \
                    --config="$config" --lockfile="$first_lockfile" \
                    --format=json --output-file="$output" \
                    || ci_warn "OSV database refresh failed for $first_lockfile; using cached database"
            done < "$lockfiles"
        
            local args=(scan source --offline --offline-vulnerabilities --config="$config" --format=json --output-file="$output")
            while IFS= read -r lockfile; do args+=(--lockfile="$lockfile"); done < "$lockfiles"
            : > "$output"
            (
                ulimit -f 2048
                ulimit -v 2097152
                timeout --signal=TERM --kill-after=5s 120s "$scanner" "${args[@]}"
            ) 2> "$errors" || rc=$?
            size="$(wc -c < "$output")"
            if [[ "$size" -gt 1048576 ]]; then
                ci_fail "OSV output exceeded 1 MiB"
                return 1
            fi
            if [[ "$rc" -ne 0 ]]; then
                cat "$errors" >&2
                ci_fail "vulnerability scan failed or advisories found (exit $rc)"
                return 1
            fi
            [[ -s "$output" ]] || { ci_fail "OSV scanner produced no result"; return 1; }
            ci_pass "offline vulnerability scan completed without findings"
        }

        Share feedback

        You voted thumbs down

        check-dependency-versions

        Command entry point for dependency validation modes.

        Pre-commitPython moduleStrict only
        Applicable to
        any

        ci.check_dependency_versions

        ci/check_dependency_versions.py

        Command entry point for dependency validation modes.

        Stage
        pre-commit
        Kind
        python_module
        Mandatory
        Yes
        Safety tier
        No (strict only)
        Pass filenames
        No
        Always run
        Yes
        Applicable to
        any
        #!/usr/bin/env python3
        """Command entry point for dependency validation modes."""
        
        from __future__ import annotations
        
        import sys
        from pathlib import Path
        
        MODES = frozenset({"validate"})
        REJECTED_MODE_FLAGS = frozenset({"--upgrade"})
        VALIDATE_FLAGS = frozenset(
            {
                "--consumer-root",
                "--catalog",
                "--catalog-if-present",
                "--npm-lock",
                "--npm-workspace-lock",
                "--uv-lock",
                "--npm-workspaces",
                "--dockerfile",
                "--compose",
                "--exclusions",
                "--json-result",
            }
        )
        
        
        def _default_validation_arguments(root: Path) -> list[str]:
            arguments = ["validate", "--consumer-root", str(root)]
            optional_files = (
                ("--catalog", "res/dependency-pins.yaml"),
                ("--dockerfile", "web/Containerfile"),
                ("--dockerfile", "Containerfile"),
                ("--dockerfile", "Dockerfile"),
                ("--compose", "web/compose.prod.yaml"),
                ("--compose", "compose.yml"),
                ("--compose", "compose.yaml"),
                ("--compose", "docker-compose.yml"),
                ("--compose", "docker-compose.yaml"),
            )
            for flag, path in optional_files:
                if (root / path).exists():
                    arguments.extend((flag, path))
            dependency_arguments: list[str] = []
            for flag, declaration, lock in (
                ("--npm-workspace-lock", "package.json", "package-lock.json"),
                ("--uv-lock", "pyproject.toml", "uv.lock"),
            ):
                if (root / declaration).exists() and (root / lock).exists():
                    dependency_arguments.extend((flag, declaration, lock))
            if dependency_arguments and (root / "config/dependency_excludes.yaml").exists():
                arguments.extend(("--exclusions", "config/dependency_excludes.yaml"))
            arguments.extend(dependency_arguments)
            return arguments
        
        
        def _normalize_transition_arguments(arguments: list[str]) -> list[str]:
            """Translate the exact invocation installed by the predecessor hook."""
            match arguments:
                case []:
                    return _default_validation_arguments(Path.cwd())
                case ["--deterministic-only", "--catalog", _]:
                    return [
                        "validate",
                        "--consumer-root",
                        str(Path.cwd()),
                        "--exclusions",
                        "config/dependency_excludes.yaml",
                        "--catalog",
                        "res/dependency-pins.yaml",
                        "--npm-workspace-lock",
                        "package.json",
                        "package-lock.json",
                        "--uv-lock",
                        "pyproject.toml",
                        "uv.lock",
                        "--dockerfile",
                        "web/Containerfile",
                        "--compose",
                        "web/compose.prod.yaml",
                        "pyproject.toml",
                    ]
            return arguments
        
        
        def _usage() -> int:
            print(
                "usage: check_dependency_versions.py validate ...",
                file=sys.stderr,
            )
            return 2
        
        
        def _mode(arguments: list[str]) -> str | None:
            """Validate the entire dispatcher envelope before implementation import."""
            if not arguments or arguments[0] not in MODES:
                _usage()
                return None
            if any(
                argument.split("=", maxsplit=1)[0] in REJECTED_MODE_FLAGS
                for argument in arguments[1:]
            ):
                _usage()
                return None
            if any(
                argument.startswith("--")
                and argument.split("=", maxsplit=1)[0] not in VALIDATE_FLAGS
                for argument in arguments[1:]
            ):
                _usage()
                return None
            return arguments[0]
        
        
        def _validation_arguments(arguments: list[str]) -> list[str]:
            return arguments[1:]
        
        
        if __name__ == "__main__":
            arguments = _normalize_transition_arguments(sys.argv[1:])
            mode = _mode(arguments)
            if mode is None:
                sys.exit(2)
            if mode == "validate":
                from ci.dependency_policy import main as validate_dependencies
        
                sys.exit(validate_dependencies(_validation_arguments(arguments)))
        

        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)Strict only
        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
        No (strict only)
        Pass filenames
        Yes
        Always run
        No
        Applicable to
        any
        File types
        markdown
        def main() -> None:
            sys.exit(run())

        Share feedback

        You voted thumbs down

        check-resolution-shapes

        Delegates to lib/check_fallback_resolution.py to scan tracked shell files for multi-source resolution shapes: probe-backed ${VAR:-...} defaults, try-A-then-B reassignment after '! -x' probes, boot-then-PATH resolvers, and multi-candidate boot-dir arrays. Complements the banned-words catalog with shape detection.

        Pre-commitShellStrict only
        Applicable to
        any

        ci_check_fallback_resolution()

        lib/checks_core.sh

        --- ci_check_fallback_resolution --- Delegates to lib/check_fallback_resolution.py to scan tracked shell files for multi-source resolution shapes: probe-backed ${VAR:-...} defaults, try-A-then-B reassignment after '! -x' probes, boot-then-PATH resolvers, and multi-candidate boot-dir arrays. Complements the banned-words catalog with shape detection.

        Stage
        pre-commit
        Kind
        shell
        Mandatory
        Yes
        Safety tier
        No (strict only)
        Pass filenames
        No
        Always run
        No
        Applicable to
        any
        ci_check_fallback_resolution() {
            local script_path="${CI_LIB_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}/check_fallback_resolution.py"
            if [[ ! -f "$script_path" ]]; then
                ci_fail "Fallback-resolution: helper not found at $script_path"
                return 1
            fi
            local _scan_root="${CI_SCAN_ROOT:-$PWD}"
            CI_SCAN_ROOT="$_scan_root" \
                ci_uv_run "$script_path" "$@" </dev/null
        }

        Share feedback

        You voted thumbs down

        check-module-size

        Enforces physical source-file bytes, source files per module, direct submodules per module, and source-module depth. Structural limits are optional fields in file_length_limits.yaml; defaults keep the guard active for older configurations.

        Pre-commitShellStrict only
        Applicable to
        any

        ci_check_module_size()

        lib/check_module_size.sh

        !/usr/bin/env bash CI structural source-size checks. Sourced by checks.sh after checks_core.sh so ignore helpers are available. --- ci_check_module_size --- Enforces physical source-file bytes, source files per module, direct submodules per module, and source-module depth. Structural limits are optional fields in file_length_limits.yaml; defaults keep the guard active for older configurations.

        Stage
        pre-commit
        Kind
        shell
        Mandatory
        Yes
        Safety tier
        No (strict only)
        Pass filenames
        No
        Always run
        Yes
        Applicable to
        any
        ci_check_module_size() {
            [[ $# -eq 0 ]] || { ci_fail "ci_check_module_size does not accept positional narrowing"; return 1; }
            local config
            config="$(ci_config_path file_length_limits "./config/file_length_limits.yaml")" || return 1
        
            local max_bytes=262144 max_total_bytes=16777216 max_files=48 max_submodules=8 max_depth=8
            local exts=(.py .sh .js .ts .tsx .rs .css .lua)
            local value key
            if [[ -f "$config" ]]; then
                local configured_exts=()
                ci_capture_lines configured_exts -- ci_read_yaml_list "$config" extensions
                [[ ${#configured_exts[@]} -gt 0 ]] && exts=("${configured_exts[@]}")
                for key in max_file_bytes max_total_source_bytes max_source_files_per_module max_submodules_per_module max_module_depth; do
                    value="$(ci_read_yaml "$config" "$key")"
                    [[ -n "$value" ]] || continue
                    if [[ ! "$value" =~ ^[1-9][0-9]*$ ]]; then
                        ci_fail "Invalid $key in $config: $value (must be a positive integer)"
                        return 1
                    fi
                    case "$key" in
                        max_file_bytes) max_bytes="$value" ;;
                        max_total_source_bytes) max_total_bytes="$value" ;;
                        max_source_files_per_module) max_files="$value" ;;
                        max_submodules_per_module) max_submodules="$value" ;;
                        max_module_depth) max_depth="$value" ;;
                    esac
                done
            fi
        
            local root
            root="$(git rev-parse --show-toplevel)" || return 1
            local source_files=() candidates=()
            ci_capture_lines candidates -- git ls-files --cached
            local candidate ext is_source
            for candidate in "${candidates[@]}"; do
                is_source=0
                for ext in "${exts[@]}"; do
                    [[ "$candidate" == *"$ext" ]] && { is_source=1; break; }
                done
                [[ $is_source -eq 1 ]] && source_files+=("$candidate")
            done
        
            declare -A modules=() direct_files=() child_dirs=() override_files=() override_children=() override_depth=()
            local f module parent child bytes path depth total_bytes=0
            local violations=()
        
            if [[ -f "$config" ]]; then
                local override_data
                override_data="$(awk '
                  function emit() {
                      if (path != "") print path "|" files "|" children "|" depth
                      path=files=children=depth=""
                  }
                  /^module_overrides:[[:space:]]*$/ { in_mod=1; next }
                  in_mod && /^[^[:space:]-]/ { emit(); exit }
                  in_mod && /^[[:space:]]*-[[:space:]]+path:/ {
                      emit(); path=$0
                      sub(/^[[:space:]]*-[[:space:]]+path:[[:space:]]*/, "", path)
                      gsub(/^["'"'']|["'"'']$/, "", path); next
                  }
                  in_mod && /^[[:space:]]+max_source_files:/ {
                      val=$0; sub(/^[^:]+:[[:space:]]*/, "", val); files=val; next
                  }
                  in_mod && /^[[:space:]]+max_submodules:/ {
                      val=$0; sub(/^[^:]+:[[:space:]]*/, "", val); children=val; next
                  }
                  in_mod && /^[[:space:]]+max_depth:/ {
                      val=$0; sub(/^[^:]+:[[:space:]]*/, "", val); depth=val; next
                  }
                  END { if (in_mod) emit() }
                ' "$config")"
                while IFS='|' read -r path value key depth; do
                    [[ -n "$path" ]] || continue
                    [[ -z "$value" || "$value" =~ ^[1-9][0-9]*$ ]] || { ci_fail "Invalid module override max_source_files for $path"; return 1; }
                    [[ -z "$key" || "$key" =~ ^[1-9][0-9]*$ ]] || { ci_fail "Invalid module override max_submodules for $path"; return 1; }
                    [[ -z "$depth" || "$depth" =~ ^[1-9][0-9]*$ ]] || { ci_fail "Invalid module override max_depth for $path"; return 1; }
                    [[ -n "$value" ]] && override_files["$path"]="$value"
                    [[ -n "$key" ]] && override_children["$path"]="$key"
                    [[ -n "$depth" ]] && override_depth["$path"]="$depth"
                done <<< "$override_data"
            fi
        
            for f in "${source_files[@]}"; do
                [[ -n "$f" ]] || continue
                _in_ignored_dir "$f" && continue
                if [[ -L "$f" ]]; then
                    violations+=("symlink:$f")
                    continue
                fi
                if [[ ! -e "$f" ]] && [[ -n "$(git ls-files --deleted -- "$f")" ]]; then
                    continue
                fi
                if [[ ! -f "$f" || ! -r "$f" ]]; then
                    violations+=("unreadable:$f")
                    continue
                fi
                module="${f%/*}"
                [[ "$module" == "$f" ]] && module="."
                bytes="$(wc -c < "$f")"
                total_bytes=$((total_bytes + bytes))
                if [[ "$bytes" -gt "$max_bytes" ]]; then
                    violations+=("file:$f:$bytes bytes (limit $max_bytes)")
                fi
                direct_files["$module"]=$(( ${direct_files["$module"]:-0} + 1 ))
                modules["$module"]=1
        
                parent="${module%/*}"
                [[ "$parent" == "$module" || -z "$parent" ]] && parent="."
                while [[ "$parent" != "." ]]; do
                    modules["$parent"]=1
                    child="${module#"$parent"/}"
                    child="${child%%/*}"
                    child_dirs["$parent|$child"]=1
                    if [[ "$parent" == */* ]]; then
                        parent="${parent%/*}"
                    else
                        parent="."
                    fi
                done
                modules[.]=1
                [[ "$module" != "." ]] && child_dirs[".|${module%%/*}"]=1
            done
        
            [[ "$total_bytes" -le "$max_total_bytes" ]] || violations+=("total:$total_bytes bytes (limit $max_total_bytes)")
        
            local module_file_count child_count limit_files limit_children limit_depth
            for module in "${!modules[@]}"; do
                module_file_count="${direct_files[$module]:-0}"
                limit_files="$max_files"
                limit_children="$max_submodules"
                limit_depth="$max_depth"
                [[ -n "${override_files[$module]:-}" ]] && limit_files="${override_files[$module]}"
                [[ -n "${override_children[$module]:-}" ]] && limit_children="${override_children[$module]}"
                [[ -n "${override_depth[$module]:-}" ]] && limit_depth="${override_depth[$module]}"
        
                child_count=0
                for child in "${!child_dirs[@]}"; do
                    [[ "$child" == "$module|"* ]] && child_count=$((child_count + 1))
                done
                depth=0
                [[ "$module" != "." ]] && depth="${module//[^\/]/}" && depth=$(( ${#depth} + 1 ))
        
                [[ "$module_file_count" -le "$limit_files" ]] || violations+=("files:$module:$module_file_count (limit $limit_files)")
                [[ "$child_count" -le "$limit_children" ]] || violations+=("submodules:$module:$child_count (limit $limit_children)")
                [[ "$depth" -le "$limit_depth" ]] || violations+=("depth:$module:$depth (limit $limit_depth)")
            done
        
            if [[ ${#violations[@]} -gt 0 ]]; then
                ci_fail "Module-size guardrail violations:"
                printf '  %s\n' "${violations[@]}" | sort
                return 1
            fi
            ci_pass "All source files and modules are within size limits."
            return 0
        }

        Share feedback

        You voted thumbs down

        check-npm-lock-sync

        npm lockfile drift gate: verifies package-lock.json is in sync with every workspace package.json it serves.

        Pre-commitPython moduleStrict only
        Applicable to
        any

        ci.check_npm_lock_sync

        ci/check_npm_lock_sync.py

        npm lockfile drift gate: verifies package-lock.json is in sync with every workspace package.json it serves.

        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:
            parser = argparse.ArgumentParser(
                description="Verify package-lock.json is in sync with workspace manifests"
            )
            parser.add_argument(
                "root",
                nargs="?",
                default=".",
                help="Repository root containing package-lock.json (default: cwd)",
            )
            args = parser.parse_args(argv)
        
            root = Path(args.root).resolve()
            if not (root / "package-lock.json").is_file():
                print(f"No package-lock.json under {root}; nothing to check.")
                return 0
        
            lock_path = root / "package-lock.json"
            print(f"Checking npm lockfile sync under {root}...")
            drift = find_drift(root)
            if not drift:
                npm_drift = _validate_with_npm(root, lock_path)
                if npm_drift is not None:
                    drift.append(npm_drift)
            if drift:
                print(f"\n!!! PACKAGE LOCK DRIFT under {root} !!!")
                for issue in drift:
                    print(f"  - {issue.manifest}: {issue.message}")
                print("\nFix: npm install --package-lock-only && git add package-lock.json")
                return 1
            print("package-lock.json is in sync with all workspace manifests.")
            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 the repository for leaked secrets with one Gitleaks process. Git-ignored paths are represented as global Gitleaks allowlist entries so directory traversal can prune them before opening their contents. 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. The repository's .gitignore is the sole file-selection filter. --- ci_scan_secrets --- Scans the repository for leaked secrets with one Gitleaks process. Git-ignored paths are represented as global Gitleaks allowlist entries so directory traversal can prune them before opening their contents. 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_root _ss_tool_root
            _ss_root="$(git rev-parse --show-toplevel)"
            _ss_tool_root="${CI_PROJECT_ROOT:-$_ss_root}"
            _ss_gitleaks_bin="$(ci_resolve_tool_path "$_ss_tool_root" gitleaks)" || {
                ci_fail "gitleaks not found (boot dir or PATH)"
                return 1
            }
        
            local _ss_candidates_tmp _ss_ignored_tmp _ss_stderr_tmp _ss_config_tmp
            _ss_candidates_tmp="$(mktemp)"
            _ss_ignored_tmp="$(mktemp)"
            _ss_stderr_tmp="$(mktemp)"
            _ss_config_tmp="$(mktemp "${TMPDIR:-/tmp}/gitleaks-config.XXXXXX.toml")"
            local _ss_git_rc=0 _ss_path _ss_suffix
            git -C "$_ss_root" ls-files -co --exclude-standard -z >"$_ss_candidates_tmp" \
                2>"$_ss_stderr_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_candidates_tmp" "$_ss_ignored_tmp" "$_ss_stderr_tmp" "$_ss_config_tmp"
                return 1
            fi
        
            local _ss_total
            _ss_total=$(tr -cd '\0' <"$_ss_candidates_tmp" | wc -c)
        
            if [[ $_ss_total -eq 0 ]]; then
                ci_pass "gitleaks: no files to scan"
                rm -f "$_ss_candidates_tmp" "$_ss_ignored_tmp" "$_ss_stderr_tmp" "$_ss_config_tmp"
                return 0
            fi
        
            local _ss_ignored_paths_tmp
            _ss_ignored_paths_tmp="$(mktemp)"
            git -C "$_ss_root" ls-files -c -i --exclude-standard -z >>"$_ss_ignored_paths_tmp" \
                2>"$_ss_stderr_tmp" || _ss_git_rc=$?
            if [[ $_ss_git_rc -ne 0 ]]; then
                ci_fail "git ls-files tracked ignored-path listing failed"
                [[ -s "$_ss_stderr_tmp" ]] && cat "$_ss_stderr_tmp" >&2
                rm -f "$_ss_candidates_tmp" "$_ss_ignored_tmp" "$_ss_ignored_paths_tmp" "$_ss_stderr_tmp" "$_ss_config_tmp"
                return 1
            fi
            git -C "$_ss_root" ls-files -o -i --exclude-standard --directory -z >>"$_ss_ignored_paths_tmp" \
                2>"$_ss_stderr_tmp" || _ss_git_rc=$?
            if [[ $_ss_git_rc -ne 0 ]]; then
                ci_fail "git ls-files ignored-path listing failed"
                [[ -s "$_ss_stderr_tmp" ]] && cat "$_ss_stderr_tmp" >&2
                rm -f "$_ss_candidates_tmp" "$_ss_ignored_tmp" "$_ss_ignored_paths_tmp" "$_ss_stderr_tmp" "$_ss_config_tmp"
                return 1
            fi
            cat "$_ss_ignored_paths_tmp" >>"$_ss_ignored_tmp"
            rm -f "$_ss_ignored_paths_tmp" "$_ss_stderr_tmp"
            rm -f "$_ss_candidates_tmp"
        
            if [[ -f "$_ss_root/.gitleaks.toml" ]]; then
                printf "[extend]\npath = '''%s'''\n\n" "$_ss_root/.gitleaks.toml" >"$_ss_config_tmp"
            else
                printf '[extend]\nuseDefault = true\n\n' >"$_ss_config_tmp"
            fi
            if [[ -s "$_ss_ignored_tmp" ]]; then
                printf '[[allowlists]]\ndescription = "Git-ignored paths"\npaths = [\n' >>"$_ss_config_tmp"
                while IFS= read -r -d '' _ss_path; do
                    _ss_suffix=''
                    if [[ "$_ss_path" == */ ]]; then
                        _ss_path="${_ss_path%/}"
                        _ss_suffix='(?:/.*)?'
                    fi
                    _ss_path="$(printf '%s' "$_ss_path" | sed 's/[][\\.^$*+?(){}|]/\\&/g')"
                    printf "  '''(^|/)%s%s$''',\n" "$_ss_path" "$_ss_suffix" >>"$_ss_config_tmp"
                done <"$_ss_ignored_tmp"
                printf ']\n' >>"$_ss_config_tmp"
            fi
            rm -f "$_ss_candidates_tmp" "$_ss_stderr_tmp"
            local _ss_rc=0
            (cd "$_ss_root" && "$_ss_gitleaks_bin" dir . \
                --config "$_ss_config_tmp" --no-banner --redact --verbose --log-level=error) \
                || _ss_rc=$?
            rm -f "$_ss_config_tmp"
            rm -f "$_ss_ignored_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)"
                elif [[ $_ss_rc -eq 139 || $_ss_rc -eq 245 ]]; then
                    ci_fail "gitleaks: CRASHED (SIGSEGV, exit $_ss_rc)"
                else
                    ci_fail "gitleaks: secrets found or error (exit $_ss_rc)"
                fi
                return 1
            fi
            ci_pass "gitleaks: no leaks found ($_ss_total files)"
            return 0
        }

        Share feedback

        You voted thumbs down

        check-policy-integrity

        Delegates to lib/check_policy_integrity.py: non-exemptible structural validation of banned_words.yaml, banned_words_exceptions_v5.yaml, and file_classifications.yaml against the final v5 model, run BEFORE ordinary exemptions load. Every exemption must be one exact anchored file + one rule id with full provenance; classification entries must be exact tracked files.

        Pre-commitShellStrict only
        Applicable to
        any

        ci_check_policy_integrity()

        lib/checks_core.sh

        --- ci_check_policy_integrity --- Delegates to lib/check_policy_integrity.py: non-exemptible structural validation of banned_words.yaml, banned_words_exceptions_v5.yaml, and file_classifications.yaml against the final v5 model, run BEFORE ordinary exemptions load. Every exemption must be one exact anchored file + one rule id with full provenance; classification entries must be exact tracked files.

        Stage
        pre-commit
        Kind
        shell
        Mandatory
        Yes
        Safety tier
        No (strict only)
        Pass filenames
        No
        Always run
        Yes
        Applicable to
        any
        ci_check_policy_integrity() {
            local script_path="${CI_LIB_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}/check_policy_integrity.py"
            if [[ ! -f "$script_path" ]]; then
                ci_fail "Policy-integrity: helper not found at $script_path"
                return 1
            fi
            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

        The Digital and AI Workspace Guardrails Platform2026 ◆ Independent AI Labs