Skip to content

Git Hooks Library

Overview

Native git hooks are shell scripts that git executes at specific points in the development workflow. Unlike the pre-commit framework, native hooks require no external dependencies and provide direct control over the entire git lifecycle.

Git Hook Lifecycle

flowchart LR
    subgraph Commit
        A[pre-commit] --> B[prepare-commit-msg]
        B --> C[commit-msg]
        C --> D[post-commit]
    end
    subgraph Push
        E[pre-push]
    end
    subgraph Branch
        F[post-checkout]
        G[post-merge]
    end
    D --> E

When to Use Native Hooks vs Pre-commit Framework

Criteria Native Hooks Pre-commit Framework
Dependencies None (bash only) Requires Python/pip
Language support Any executable Plugin ecosystem
Configuration Shell scripts YAML configuration
Sharing Manual or git templates .pre-commit-config.yaml
Hook types All git hooks pre-commit, commit-msg, pre-push
Auto-updates Manual pre-commit autoupdate
Best for Custom workflows, post-* hooks Standardized linting/formatting

Hook Directory Structure

.githooks/
├── pre-commit
├── commit-msg
├── pre-push
├── prepare-commit-msg
├── post-checkout
├── post-merge
├── post-commit
└── install.sh

Hook Installation and Management

Manual Installation

#!/bin/bash
## Copy a single hook to the git hooks directory
cp .githooks/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
#!/bin/bash
## Point git to a shared hooks directory (Git 2.9+)
## This is the recommended approach for team distribution
git config core.hooksPath .githooks

## Verify the configuration
git config --get core.hooksPath
## Output: .githooks

Git Template Directory

#!/bin/bash
## Set up a global template directory for all new repositories
## Hooks in this directory are copied to every new clone/init

## Create the template structure
mkdir -p ~/.git-templates/hooks

## Copy hooks to template directory
cp .githooks/* ~/.git-templates/hooks/
chmod +x ~/.git-templates/hooks/*

## Configure git to use the template
git config --global init.templateDir ~/.git-templates

## New repositories will now include these hooks automatically
git clone https://github.com/org/repo.git
## Hooks are automatically copied to repo/.git/hooks/

Automated Installer Script

#!/bin/bash
## .githooks/install.sh
## Installs all git hooks for the project

set -euo pipefail

HOOKS_DIR=".githooks"
HOOK_NAMES=(
    "pre-commit"
    "commit-msg"
    "pre-push"
    "prepare-commit-msg"
    "post-checkout"
    "post-merge"
)

main() {
    echo "Installing git hooks..."

    ## Use core.hooksPath if available (Git 2.9+)
    git_version=$(git --version | grep -oE '[0-9]+\.[0-9]+')
    if awk "BEGIN{exit !($git_version >= 2.9)}"; then
        git config core.hooksPath "${HOOKS_DIR}"
        echo "Configured core.hooksPath to ${HOOKS_DIR}"
    else
        ## Fallback: symlink hooks individually
        for hook in "${HOOK_NAMES[@]}"; do
            if [[ -f "${HOOKS_DIR}/${hook}" ]]; then
                ln -sf "../../${HOOKS_DIR}/${hook}" ".git/hooks/${hook}"
                echo "Linked ${hook}"
            fi
        done
    fi

    ## Ensure hooks are executable
    chmod +x "${HOOKS_DIR}"/*

    echo "Git hooks installed successfully."
}

main "$@"

Uninstaller Script

#!/bin/bash
## .githooks/uninstall.sh
## Removes all project git hooks

set -euo pipefail

main() {
    echo "Removing git hooks..."

    ## Remove core.hooksPath configuration
    git config --unset core.hooksPath 2>/dev/null || true

    ## Remove any symlinked hooks
    for hook in .git/hooks/*; do
        if [[ -L "${hook}" ]]; then
            rm "${hook}"
            echo "Removed $(basename "${hook}")"
        fi
    done

    echo "Git hooks removed successfully."
}

main "$@"

Makefile Integration

## Makefile targets for hook management

.PHONY: hooks hooks-install hooks-uninstall

## Install git hooks (run after cloning)
hooks: hooks-install

hooks-install:
    @bash .githooks/install.sh

hooks-uninstall:
    @bash .githooks/uninstall.sh

## Include hooks installation in project setup
setup: hooks-install
    uv sync
    @echo "Project setup complete."

pre-commit Hook

Linting and Formatting

#!/bin/bash
## .githooks/pre-commit
## Runs linting and formatting checks on staged files before commit

set -euo pipefail

## Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m'

## Get list of staged files
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)

if [[ -z "${STAGED_FILES}" ]]; then
    exit 0
fi

ERRORS=0

## Python linting
PYTHON_FILES=$(echo "${STAGED_FILES}" | grep '\.py$' || true)
if [[ -n "${PYTHON_FILES}" ]]; then
    echo -e "${YELLOW}Running Python checks...${NC}"

    ## Format check with black
    if command -v black &>/dev/null; then
        echo "${PYTHON_FILES}" | xargs black --check --quiet 2>/dev/null || {
            echo -e "${RED}Python formatting errors found. Run 'black .' to fix.${NC}"
            ERRORS=$((ERRORS + 1))
        }
    fi

    ## Lint with flake8
    if command -v flake8 &>/dev/null; then
        echo "${PYTHON_FILES}" | xargs flake8 --max-line-length=100 || {
            echo -e "${RED}Python linting errors found.${NC}"
            ERRORS=$((ERRORS + 1))
        }
    fi
fi

## Shell script linting
SHELL_FILES=$(echo "${STAGED_FILES}" | grep '\.sh$' || true)
if [[ -n "${SHELL_FILES}" ]]; then
    echo -e "${YELLOW}Running shell checks...${NC}"

    if command -v shellcheck &>/dev/null; then
        echo "${SHELL_FILES}" | xargs shellcheck || {
            echo -e "${RED}Shell linting errors found.${NC}"
            ERRORS=$((ERRORS + 1))
        }
    fi
fi

## YAML validation
YAML_FILES=$(echo "${STAGED_FILES}" | grep -E '\.(yml|yaml)$' || true)
if [[ -n "${YAML_FILES}" ]]; then
    echo -e "${YELLOW}Running YAML checks...${NC}"

    if command -v yamllint &>/dev/null; then
        echo "${YAML_FILES}" | xargs yamllint -c .yamllint.yml || {
            echo -e "${RED}YAML validation errors found.${NC}"
            ERRORS=$((ERRORS + 1))
        }
    fi
fi

## Terraform formatting
TF_FILES=$(echo "${STAGED_FILES}" | grep '\.tf$' || true)
if [[ -n "${TF_FILES}" ]]; then
    echo -e "${YELLOW}Running Terraform checks...${NC}"

    if command -v terraform &>/dev/null; then
        terraform fmt -check -recursive || {
            echo -e "${RED}Terraform formatting errors. Run 'terraform fmt -recursive'.${NC}"
            ERRORS=$((ERRORS + 1))
        }
    fi
fi

if [[ ${ERRORS} -gt 0 ]]; then
    echo -e "\n${RED}Pre-commit checks failed with ${ERRORS} error(s).${NC}"
    exit 1
fi

echo -e "${GREEN}All pre-commit checks passed.${NC}"

Secret Detection

#!/bin/bash
## .githooks/pre-commit.d/check-secrets
## Detects secrets and sensitive data in staged files

set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'

STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)

if [[ -z "${STAGED_FILES}" ]]; then
    exit 0
fi

ERRORS=0

## Check for common secret patterns in staged content
PATTERNS=(
    'AKIA[0-9A-Z]{16}'                    ## AWS Access Key ID
    '[0-9a-zA-Z/+]{40}'                    ## AWS Secret Key (heuristic)
    'ghp_[0-9a-zA-Z]{36}'                  ## GitHub Personal Access Token
    'glpat-[0-9a-zA-Z\-]{20}'              ## GitLab Personal Access Token
    'sk-[0-9a-zA-Z]{48}'                   ## OpenAI API Key
    'xox[bpors]-[0-9a-zA-Z\-]+'            ## Slack Token
    '-----BEGIN (RSA |EC )?PRIVATE KEY-----' ## Private keys
)

for pattern in "${PATTERNS[@]}"; do
    matches=$(git diff --cached -G"${pattern}" --name-only || true)
    if [[ -n "${matches}" ]]; then
        echo -e "${RED}Potential secret detected matching pattern: ${pattern}${NC}"
        echo "${matches}" | while read -r file; do
            echo "  - ${file}"
        done
        ERRORS=$((ERRORS + 1))
    fi
done

## Check for sensitive file names
SENSITIVE_PATTERNS=(
    '\.env$'
    '\.env\.local$'
    'credentials\.json$'
    '\.pem$'
    '\.key$'
    'id_rsa$'
    'id_ed25519$'
    '\.tfvars$'
    'secret'
)

for pattern in "${SENSITIVE_PATTERNS[@]}"; do
    matches=$(echo "${STAGED_FILES}" | grep -E "${pattern}" || true)
    if [[ -n "${matches}" ]]; then
        echo -e "${RED}Sensitive file staged for commit:${NC}"
        echo "${matches}" | while read -r file; do
            echo "  - ${file}"
        done
        ERRORS=$((ERRORS + 1))
    fi
done

if [[ ${ERRORS} -gt 0 ]]; then
    echo -e "\n${RED}Secret detection found ${ERRORS} issue(s).${NC}"
    echo "If this is a false positive, use: git commit --no-verify"
    exit 1
fi

echo -e "${GREEN}No secrets detected.${NC}"

Large File Check

#!/bin/bash
## .githooks/pre-commit.d/check-file-size
## Prevents committing files over a size threshold

set -euo pipefail

MAX_FILE_SIZE_KB=1000
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'

STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)

if [[ -z "${STAGED_FILES}" ]]; then
    exit 0
fi

OVERSIZED=0

while IFS= read -r file; do
    if [[ -f "${file}" ]]; then
        file_size_kb=$(du -k "${file}" | cut -f1)
        if [[ ${file_size_kb} -gt ${MAX_FILE_SIZE_KB} ]]; then
            echo -e "${RED}File exceeds ${MAX_FILE_SIZE_KB}KB limit: ${file} (${file_size_kb}KB)${NC}"
            OVERSIZED=$((OVERSIZED + 1))
        fi
    fi
done <<< "${STAGED_FILES}"

if [[ ${OVERSIZED} -gt 0 ]]; then
    echo -e "\n${RED}${OVERSIZED} file(s) exceed the size limit.${NC}"
    echo "Consider using Git LFS for large files: git lfs track '*.bin'"
    exit 1
fi

echo -e "${GREEN}File size check passed.${NC}"

Composable pre-commit with Hook Runner

#!/bin/bash
## .githooks/pre-commit
## Runs all scripts in pre-commit.d/ directory

set -euo pipefail

HOOK_DIR="$(dirname "$0")/pre-commit.d"

if [[ ! -d "${HOOK_DIR}" ]]; then
    exit 0
fi

ERRORS=0

for hook in "${HOOK_DIR}"/*; do
    if [[ -x "${hook}" ]]; then
        echo "Running $(basename "${hook}")..."
        if ! "${hook}"; then
            ERRORS=$((ERRORS + 1))
        fi
    fi
done

if [[ ${ERRORS} -gt 0 ]]; then
    echo "pre-commit failed: ${ERRORS} hook(s) reported errors."
    exit 1
fi

commit-msg Hook

Conventional Commit Validation

#!/bin/bash
## .githooks/commit-msg
## Validates commit messages against conventional commit format
##
## Format: type(scope): subject
##
## Types: feat, fix, docs, style, refactor, test, chore, ci, perf, build, revert
## Scope: optional, lowercase
## Subject: lowercase start, no period, max 72 chars

set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m'

COMMIT_MSG_FILE="$1"
COMMIT_MSG=$(head -1 "${COMMIT_MSG_FILE}")

## Skip merge commits
if echo "${COMMIT_MSG}" | grep -qE '^Merge '; then
    exit 0
fi

## Valid conventional commit types
VALID_TYPES="feat|fix|docs|style|refactor|test|chore|ci|perf|build|revert"

## Pattern: type(optional-scope): subject
## OR: type!: subject (breaking change)
PATTERN="^(${VALID_TYPES})(\([a-z0-9\-]+\))?!?: .+"

if ! echo "${COMMIT_MSG}" | grep -qE "${PATTERN}"; then
    echo -e "${RED}Invalid commit message format.${NC}"
    echo ""
    echo "Expected format: type(scope): subject"
    echo ""
    echo "Valid types: feat, fix, docs, style, refactor, test, chore, ci, perf, build, revert"
    echo ""
    echo "Examples:"
    echo "  feat(auth): add JWT token refresh"
    echo "  fix: resolve null pointer in user service"
    echo "  docs(readme): update installation instructions"
    echo "  refactor(api)!: restructure endpoint naming"
    echo ""
    echo -e "Your message: ${YELLOW}${COMMIT_MSG}${NC}"
    exit 1
fi

## Check subject line length (max 72 characters)
SUBJECT_LENGTH=${#COMMIT_MSG}
if [[ ${SUBJECT_LENGTH} -gt 72 ]]; then
    echo -e "${RED}Commit subject exceeds 72 characters (${SUBJECT_LENGTH}).${NC}"
    echo "Shorten the subject line and use the body for details."
    exit 1
fi

## Check subject does not end with period
if echo "${COMMIT_MSG}" | grep -qE '\.$'; then
    echo -e "${RED}Commit subject should not end with a period.${NC}"
    exit 1
fi

echo -e "${GREEN}Commit message is valid.${NC}"

Ticket Reference Enforcement

#!/bin/bash
## .githooks/commit-msg.d/check-ticket-reference
## Ensures commit messages reference a ticket/issue number
##
## Accepts patterns:
##   - #123 (GitHub issue)
##   - JIRA-123 (Jira ticket)
##   - Closes #123, Fixes #123, Refs #123

set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'

COMMIT_MSG_FILE="$1"
COMMIT_MSG=$(cat "${COMMIT_MSG_FILE}")

## Skip merge commits and fixup commits
if echo "${COMMIT_MSG}" | grep -qE '^(Merge|fixup!|squash!) '; then
    exit 0
fi

## Patterns that indicate a ticket reference
TICKET_PATTERNS=(
    '#[0-9]+'                              ## GitHub issue: #123
    '[A-Z]{2,10}-[0-9]+'                   ## Jira-style: PROJ-123
    '(Closes|Fixes|Refs|Resolves) #[0-9]+' ## GitHub keywords
)

FOUND=0
for pattern in "${TICKET_PATTERNS[@]}"; do
    if echo "${COMMIT_MSG}" | grep -qE "${pattern}"; then
        FOUND=1
        break
    fi
done

if [[ ${FOUND} -eq 0 ]]; then
    echo -e "${RED}Commit message must reference a ticket or issue.${NC}"
    echo ""
    echo "Accepted formats:"
    echo "  feat(auth): add login endpoint #123"
    echo "  fix: resolve timeout PROJ-456"
    echo "  docs: update API reference"
    echo ""
    echo "  Closes #123"
    echo "  Fixes #456"
    echo "  Refs JIRA-789"
    exit 1
fi

echo -e "${GREEN}Ticket reference found.${NC}"

Spell Check on Commit Message

#!/bin/bash
## .githooks/commit-msg.d/check-spelling
## Basic spell check on commit message subject line

set -euo pipefail

COMMIT_MSG_FILE="$1"
SUBJECT=$(head -1 "${COMMIT_MSG_FILE}")

## Extract words from the subject (skip type prefix)
WORDS=$(echo "${SUBJECT}" | sed 's/^[a-z]*(\?[a-z\-]*)\?!*: //' | tr ' ' '\n')

## Common misspellings to catch
MISSPELLINGS=(
    "teh:the"
    "adn:and"
    "hte:the"
    "taht:that"
    "recieve:receive"
    "occured:occurred"
    "seperate:separate"
    "definately:definitely"
    "enviroment:environment"
    "dependancy:dependency"
)

ERRORS=0
for entry in "${MISSPELLINGS[@]}"; do
    wrong="${entry%%:*}"
    correct="${entry##*:}"
    if echo "${WORDS}" | grep -qiw "${wrong}"; then
        echo "Possible misspelling: '${wrong}' -> '${correct}'"
        ERRORS=$((ERRORS + 1))
    fi
done

if [[ ${ERRORS} -gt 0 ]]; then
    echo "Found ${ERRORS} possible misspelling(s) in commit message."
    echo "Fix the message or use 'git commit --no-verify' to skip."
    exit 1
fi

pre-push Hook

Run Tests Before Push

#!/bin/bash
## .githooks/pre-push
## Runs the test suite before allowing a push

set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m'

echo -e "${YELLOW}Running pre-push checks...${NC}"

ERRORS=0

## Run Python tests if pytest is available
if [[ -f "pyproject.toml" ]] || [[ -f "setup.py" ]]; then
    if command -v pytest &>/dev/null; then
        echo "Running pytest..."
        pytest tests/ --quiet --tb=short || {
            echo -e "${RED}Python tests failed.${NC}"
            ERRORS=$((ERRORS + 1))
        }
    fi
fi

## Run JavaScript/TypeScript tests if package.json exists
if [[ -f "package.json" ]]; then
    if command -v npm &>/dev/null; then
        echo "Running npm test..."
        npm test --silent 2>/dev/null || {
            echo -e "${RED}JavaScript/TypeScript tests failed.${NC}"
            ERRORS=$((ERRORS + 1))
        }
    fi
fi

## Run Go tests if go.mod exists
if [[ -f "go.mod" ]]; then
    if command -v go &>/dev/null; then
        echo "Running go test..."
        go test ./... -count=1 -short || {
            echo -e "${RED}Go tests failed.${NC}"
            ERRORS=$((ERRORS + 1))
        }
    fi
fi

## Run Terraform validation if .tf files exist
if ls *.tf &>/dev/null 2>&1; then
    if command -v terraform &>/dev/null; then
        echo "Running terraform validate..."
        terraform validate || {
            echo -e "${RED}Terraform validation failed.${NC}"
            ERRORS=$((ERRORS + 1))
        }
    fi
fi

if [[ ${ERRORS} -gt 0 ]]; then
    echo -e "\n${RED}Pre-push checks failed. Push aborted.${NC}"
    echo "Fix the issues above or use 'git push --no-verify' to skip."
    exit 1
fi

echo -e "${GREEN}All pre-push checks passed.${NC}"

Branch Protection

#!/bin/bash
## .githooks/pre-push.d/protect-branches
## Prevents direct pushes to protected branches

set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'

CURRENT_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)

## Protected branches that should not receive direct pushes
PROTECTED_BRANCHES=(
    "main"
    "master"
    "production"
    "release"
)

for branch in "${PROTECTED_BRANCHES[@]}"; do
    if [[ "${CURRENT_BRANCH}" == "${branch}" ]]; then
        echo -e "${RED}Direct push to '${branch}' is not allowed.${NC}"
        echo "Create a feature branch and open a pull request instead."
        echo ""
        echo "  git checkout -b feat/my-feature"
        echo "  git push -u origin feat/my-feature"
        echo "  gh pr create"
        exit 1
    fi
done

echo -e "${GREEN}Branch protection check passed.${NC}"

Prevent Force Push

#!/bin/bash
## .githooks/pre-push.d/prevent-force-push
## Blocks force pushes to protected branches

set -euo pipefail

RED='\033[0;31m'
NC='\033[0m'

PROTECTED_BRANCHES=("main" "master" "production")

## Read push information from stdin
while read -r local_ref local_sha remote_ref remote_sha; do
    remote_branch=$(echo "${remote_ref}" | sed 's|refs/heads/||')

    for protected in "${PROTECTED_BRANCHES[@]}"; do
        if [[ "${remote_branch}" == "${protected}" ]]; then
            ## Check if this is a force push (remote_sha is not ancestor of local_sha)
            if [[ "${remote_sha}" != "0000000000000000000000000000000000000000" ]]; then
                if ! git merge-base --is-ancestor "${remote_sha}" "${local_sha}" 2>/dev/null; then
                    echo -e "${RED}Force push to '${protected}' is blocked.${NC}"
                    echo "This would rewrite history on a protected branch."
                    exit 1
                fi
            fi
        fi
    done
done

WIP Commit Detection

#!/bin/bash
## .githooks/pre-push.d/check-wip
## Prevents pushing commits marked as work-in-progress

set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'

## Read push information from stdin
while read -r local_ref local_sha remote_ref remote_sha; do
    if [[ "${local_sha}" == "0000000000000000000000000000000000000000" ]]; then
        ## Branch deletion, skip
        continue
    fi

    ## Determine commit range
    if [[ "${remote_sha}" == "0000000000000000000000000000000000000000" ]]; then
        ## New branch
        RANGE="${local_sha}"
    else
        RANGE="${remote_sha}..${local_sha}"
    fi

    ## Check for WIP markers in commit messages
    WIP_COMMITS=$(git log --oneline "${RANGE}" --grep="^WIP" --grep="^wip" --grep="^fixup!" \
        --grep="^squash!" --grep="^FIXME" 2>/dev/null || true)

    if [[ -n "${WIP_COMMITS}" ]]; then
        echo -e "${RED}Work-in-progress commits detected:${NC}"
        echo "${WIP_COMMITS}"
        echo ""
        echo "Squash or amend these commits before pushing."
        echo "Use 'git push --no-verify' to override."
        exit 1
    fi
done

echo -e "${GREEN}No WIP commits found.${NC}"

post-checkout Hook

Auto-install Dependencies

#!/bin/bash
## .githooks/post-checkout
## Automatically updates dependencies after branch switch or clone

set -euo pipefail

YELLOW='\033[0;33m'
GREEN='\033[0;32m'
NC='\033[0m'

PREV_HEAD="$1"
NEW_HEAD="$2"
BRANCH_CHECKOUT="$3"

## Only run on branch checkouts (not file checkouts)
if [[ "${BRANCH_CHECKOUT}" != "1" ]]; then
    exit 0
fi

## Skip if this is the initial clone (prev_head is all zeros)
if [[ "${PREV_HEAD}" == "0000000000000000000000000000000000000000" ]]; then
    exit 0
fi

## Check if dependency files changed between branches
check_file_changed() {
    local file="$1"
    git diff --name-only "${PREV_HEAD}" "${NEW_HEAD}" -- "${file}" | grep -q . 2>/dev/null
}

## Python (uv)
if check_file_changed "pyproject.toml" || check_file_changed "uv.lock"; then
    echo -e "${YELLOW}Python dependencies changed. Running 'uv sync'...${NC}"
    uv sync --quiet
    echo -e "${GREEN}Python dependencies updated.${NC}"
fi

## Python (pip)
if check_file_changed "requirements.txt"; then
    echo -e "${YELLOW}requirements.txt changed. Running 'pip install'...${NC}"
    pip install -r requirements.txt --quiet
    echo -e "${GREEN}Python dependencies updated.${NC}"
fi

## Node.js (npm)
if check_file_changed "package-lock.json" || check_file_changed "package.json"; then
    echo -e "${YELLOW}Node dependencies changed. Running 'npm install'...${NC}"
    npm install --silent
    echo -e "${GREEN}Node dependencies updated.${NC}"
fi

## Node.js (yarn)
if check_file_changed "yarn.lock"; then
    echo -e "${YELLOW}Yarn dependencies changed. Running 'yarn install'...${NC}"
    yarn install --silent
    echo -e "${GREEN}Yarn dependencies updated.${NC}"
fi

## Go
if check_file_changed "go.sum" || check_file_changed "go.mod"; then
    echo -e "${YELLOW}Go dependencies changed. Running 'go mod download'...${NC}"
    go mod download
    echo -e "${GREEN}Go dependencies updated.${NC}"
fi

## Ruby
if check_file_changed "Gemfile.lock"; then
    echo -e "${YELLOW}Ruby dependencies changed. Running 'bundle install'...${NC}"
    bundle install --quiet
    echo -e "${GREEN}Ruby dependencies updated.${NC}"
fi

## Terraform
if check_file_changed "*.tf"; then
    echo -e "${YELLOW}Terraform files changed. Running 'terraform init'...${NC}"
    terraform init -upgrade -input=false -no-color >/dev/null 2>&1
    echo -e "${GREEN}Terraform providers updated.${NC}"
fi

Environment Configuration Check

#!/bin/bash
## .githooks/post-checkout.d/check-env
## Alerts when environment configuration files change between branches

set -euo pipefail

YELLOW='\033[0;33m'
NC='\033[0m'

PREV_HEAD="$1"
NEW_HEAD="$2"
BRANCH_CHECKOUT="$3"

if [[ "${BRANCH_CHECKOUT}" != "1" ]]; then
    exit 0
fi

## List of environment-related files to monitor
ENV_FILES=(
    ".env.example"
    "docker-compose.yml"
    "docker-compose.override.yml"
    "Dockerfile"
    ".editorconfig"
    "tsconfig.json"
    "pyproject.toml"
    "Makefile"
)

CHANGED_FILES=()

for file in "${ENV_FILES[@]}"; do
    if git diff --name-only "${PREV_HEAD}" "${NEW_HEAD}" -- "${file}" | grep -q . 2>/dev/null; then
        CHANGED_FILES+=("${file}")
    fi
done

if [[ ${#CHANGED_FILES[@]} -gt 0 ]]; then
    echo ""
    echo -e "${YELLOW}Environment configuration files changed:${NC}"
    for file in "${CHANGED_FILES[@]}"; do
        echo "  - ${file}"
    done
    echo ""
    echo "Review these changes and update your local environment if needed."
    echo ""
fi

post-merge Hook

Dependency Auto-update

#!/bin/bash
## .githooks/post-merge
## Runs dependency updates and migrations after pulling/merging changes

set -euo pipefail

YELLOW='\033[0;33m'
GREEN='\033[0;32m'
NC='\033[0m'

## Check if a file changed in the merge
file_changed() {
    local file="$1"
    git diff --name-only HEAD@{1} HEAD -- "${file}" 2>/dev/null | grep -q .
}

## Python dependencies
if file_changed "pyproject.toml" || file_changed "uv.lock"; then
    echo -e "${YELLOW}Python dependencies changed. Syncing...${NC}"
    uv sync --quiet
    echo -e "${GREEN}Python dependencies synced.${NC}"
fi

if file_changed "requirements.txt"; then
    echo -e "${YELLOW}requirements.txt changed. Installing...${NC}"
    pip install -r requirements.txt --quiet
    echo -e "${GREEN}Python dependencies installed.${NC}"
fi

## Node.js dependencies
if file_changed "package-lock.json" || file_changed "package.json"; then
    echo -e "${YELLOW}Node dependencies changed. Installing...${NC}"
    npm install --silent
    echo -e "${GREEN}Node dependencies installed.${NC}"
fi

## Go dependencies
if file_changed "go.sum" || file_changed "go.mod"; then
    echo -e "${YELLOW}Go dependencies changed. Downloading...${NC}"
    go mod download
    echo -e "${GREEN}Go dependencies downloaded.${NC}"
fi

Database Migration Runner

#!/bin/bash
## .githooks/post-merge.d/run-migrations
## Detects and runs pending database migrations after merge

set -euo pipefail

YELLOW='\033[0;33m'
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m'

## Check if migration files were added or modified
MIGRATION_DIRS=(
    "migrations/"
    "db/migrate/"
    "alembic/versions/"
    "src/migrations/"
)

MIGRATIONS_CHANGED=0

for dir in "${MIGRATION_DIRS[@]}"; do
    changes=$(git diff --name-only HEAD@{1} HEAD -- "${dir}" 2>/dev/null || true)
    if [[ -n "${changes}" ]]; then
        MIGRATIONS_CHANGED=1
        echo -e "${YELLOW}New migrations detected in ${dir}:${NC}"
        echo "${changes}" | while read -r file; do
            echo "  - ${file}"
        done
    fi
done

if [[ ${MIGRATIONS_CHANGED} -eq 1 ]]; then
    echo ""
    echo -e "${YELLOW}Database migrations may need to be applied.${NC}"
    echo ""

    ## Auto-detect and suggest the migration command
    if [[ -f "alembic.ini" ]]; then
        echo "Run: alembic upgrade head"
    elif [[ -f "manage.py" ]]; then
        echo "Run: python manage.py migrate"
    elif [[ -f "Gemfile" ]] && [[ -d "db/migrate" ]]; then
        echo "Run: bundle exec rails db:migrate"
    elif [[ -f "package.json" ]]; then
        if grep -q "prisma" package.json 2>/dev/null; then
            echo "Run: npx prisma migrate deploy"
        elif grep -q "knex" package.json 2>/dev/null; then
            echo "Run: npx knex migrate:latest"
        fi
    fi

    echo ""
fi

Lock File Drift Detection

#!/bin/bash
## .githooks/post-merge.d/check-lockfiles
## Warns when lock files change, indicating dependency updates are needed

set -euo pipefail

YELLOW='\033[0;33m'
NC='\033[0m'

LOCK_FILES=(
    "uv.lock"
    "package-lock.json"
    "yarn.lock"
    "pnpm-lock.yaml"
    "Gemfile.lock"
    "go.sum"
    "Cargo.lock"
    "composer.lock"
    "poetry.lock"
)

CHANGED_LOCKS=()

for lock_file in "${LOCK_FILES[@]}"; do
    if git diff --name-only HEAD@{1} HEAD -- "${lock_file}" 2>/dev/null | grep -q .; then
        CHANGED_LOCKS+=("${lock_file}")
    fi
done

if [[ ${#CHANGED_LOCKS[@]} -gt 0 ]]; then
    echo ""
    echo -e "${YELLOW}Lock files changed after merge:${NC}"
    for lock in "${CHANGED_LOCKS[@]}"; do
        echo "  - ${lock}"
    done
    echo ""
    echo "Run your package manager's install command to update local dependencies."
    echo ""
fi

prepare-commit-msg Hook

Auto-insert Branch Name as Ticket Reference

#!/bin/bash
## .githooks/prepare-commit-msg
## Automatically prefixes commit messages with ticket number from branch name
##
## Branch naming convention: type/TICKET-123-description
## Result: TICKET-123 original commit message

set -euo pipefail

COMMIT_MSG_FILE="$1"
COMMIT_SOURCE="${2:-}"

## Skip for merge commits, amends, and squashes
if [[ "${COMMIT_SOURCE}" == "merge" ]] || \
   [[ "${COMMIT_SOURCE}" == "squash" ]] || \
   [[ "${COMMIT_SOURCE}" == "commit" ]]; then
    exit 0
fi

BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || true)

if [[ -z "${BRANCH}" ]]; then
    exit 0
fi

## Extract ticket number from branch name
## Supports: feat/PROJ-123-description, fix/PROJ-123, PROJ-123-description
TICKET=$(echo "${BRANCH}" | grep -oE '[A-Z]{2,10}-[0-9]+' | head -1 || true)

if [[ -z "${TICKET}" ]]; then
    exit 0
fi

COMMIT_MSG=$(cat "${COMMIT_MSG_FILE}")

## Only add ticket if not already present in the message
if ! echo "${COMMIT_MSG}" | grep -q "${TICKET}"; then
    ## Append ticket reference to first line
    sed -i.bak "1s/$/ ${TICKET}/" "${COMMIT_MSG_FILE}"
    rm -f "${COMMIT_MSG_FILE}.bak"
fi

Commit Message Template

#!/bin/bash
## .githooks/prepare-commit-msg.d/add-template
## Adds a commit message template when creating new commits

set -euo pipefail

COMMIT_MSG_FILE="$1"
COMMIT_SOURCE="${2:-}"

## Only add template for new commits (not amend, merge, squash)
if [[ -n "${COMMIT_SOURCE}" ]]; then
    exit 0
fi

## Check if the message is the default (empty or just comments)
if grep -qv '^#' "${COMMIT_MSG_FILE}" 2>/dev/null && \
   grep -qv '^$' "${COMMIT_MSG_FILE}" 2>/dev/null; then
    ## User already provided a message via -m flag
    exit 0
fi

BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || true)

cat > "${COMMIT_MSG_FILE}" << 'TEMPLATE'

# Conventional Commit Format:
#   type(scope): subject
#
# Types: feat, fix, docs, style, refactor, test, chore, ci, perf, build, revert
# Scope: optional, describes the section of the codebase
# Subject: imperative mood, no period, max 72 chars
#
# Body (optional): explain WHY, not WHAT
#
# Footer (optional):
#   BREAKING CHANGE: description
#   Closes #123
#   Refs PROJ-456
TEMPLATE

Co-author Insertion

#!/bin/bash
## .githooks/prepare-commit-msg.d/add-coauthor
## Adds co-author trailer for pair programming sessions
##
## Set co-author: git config --local hooks.coauthor "Name <email>"
## Clear co-author: git config --local --unset hooks.coauthor

set -euo pipefail

COMMIT_MSG_FILE="$1"
COMMIT_SOURCE="${2:-}"

## Skip for merge and squash commits
if [[ "${COMMIT_SOURCE}" == "merge" ]] || [[ "${COMMIT_SOURCE}" == "squash" ]]; then
    exit 0
fi

COAUTHOR=$(git config --get hooks.coauthor 2>/dev/null || true)

if [[ -z "${COAUTHOR}" ]]; then
    exit 0
fi

COMMIT_MSG=$(cat "${COMMIT_MSG_FILE}")

## Only add if not already present
if ! echo "${COMMIT_MSG}" | grep -q "Co-authored-by: ${COAUTHOR}"; then
    echo "" >> "${COMMIT_MSG_FILE}"
    echo "Co-authored-by: ${COAUTHOR}" >> "${COMMIT_MSG_FILE}"
fi

Hook Management Framework

Complete Installer with Configuration

#!/bin/bash
## .githooks/install.sh
## Comprehensive hook installer with configuration options

set -euo pipefail

GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m'

HOOKS_DIR=".githooks"
CONFIG_FILE=".githooks.conf"

## Default configuration
ENABLE_PRE_COMMIT=true
ENABLE_COMMIT_MSG=true
ENABLE_PRE_PUSH=true
ENABLE_POST_CHECKOUT=true
ENABLE_POST_MERGE=true
ENABLE_PREPARE_COMMIT_MSG=true

## Load project configuration if it exists
if [[ -f "${CONFIG_FILE}" ]]; then
    # shellcheck source=/dev/null
    source "${CONFIG_FILE}"
fi

usage() {
    echo "Usage: $0 [OPTIONS]"
    echo ""
    echo "Options:"
    echo "  --all           Install all hooks (default)"
    echo "  --only HOOK     Install only the specified hook"
    echo "  --skip HOOK     Skip the specified hook"
    echo "  --global        Install to git template directory"
    echo "  -h, --help      Show this help message"
    echo ""
    echo "Available hooks:"
    echo "  pre-commit, commit-msg, pre-push,"
    echo "  post-checkout, post-merge, prepare-commit-msg"
}

install_hooks() {
    local target_dir="$1"

    echo -e "${BLUE}Installing git hooks...${NC}"

    ## Ensure hooks are executable
    chmod +x "${HOOKS_DIR}"/* 2>/dev/null || true
    if [[ -d "${HOOKS_DIR}/pre-commit.d" ]]; then
        chmod +x "${HOOKS_DIR}/pre-commit.d"/* 2>/dev/null || true
    fi
    if [[ -d "${HOOKS_DIR}/commit-msg.d" ]]; then
        chmod +x "${HOOKS_DIR}/commit-msg.d"/* 2>/dev/null || true
    fi
    if [[ -d "${HOOKS_DIR}/pre-push.d" ]]; then
        chmod +x "${HOOKS_DIR}/pre-push.d"/* 2>/dev/null || true
    fi
    if [[ -d "${HOOKS_DIR}/post-checkout.d" ]]; then
        chmod +x "${HOOKS_DIR}/post-checkout.d"/* 2>/dev/null || true
    fi
    if [[ -d "${HOOKS_DIR}/post-merge.d" ]]; then
        chmod +x "${HOOKS_DIR}/post-merge.d"/* 2>/dev/null || true
    fi

    ## Configure core.hooksPath
    git config core.hooksPath "${target_dir}"

    echo -e "${GREEN}Hooks installed successfully.${NC}"
    echo ""
    echo "Installed hooks:"
    for hook in "${target_dir}"/*; do
        if [[ -x "${hook}" ]] && [[ ! -d "${hook}" ]]; then
            echo -e "  ${GREEN}${NC} $(basename "${hook}")"
        fi
    done
}

install_global() {
    local template_dir="${HOME}/.git-templates/hooks"
    mkdir -p "${template_dir}"

    echo -e "${BLUE}Installing hooks globally to ${template_dir}...${NC}"
    cp "${HOOKS_DIR}"/* "${template_dir}/" 2>/dev/null || true
    chmod +x "${template_dir}"/* 2>/dev/null || true

    git config --global init.templateDir "${HOME}/.git-templates"

    echo -e "${GREEN}Global hooks installed.${NC}"
    echo "New repositories will include these hooks automatically."
}

## Parse arguments
GLOBAL=false
while [[ $# -gt 0 ]]; do
    case "$1" in
        --global) GLOBAL=true; shift ;;
        -h|--help) usage; exit 0 ;;
        *) echo "Unknown option: $1"; usage; exit 1 ;;
    esac
done

if [[ "${GLOBAL}" == true ]]; then
    install_global
else
    install_hooks "${HOOKS_DIR}"
fi

Hook Configuration File

## .githooks.conf
## Configuration for git hooks
## Source this file to customize hook behavior

## Enable/disable specific hooks
ENABLE_PRE_COMMIT=true
ENABLE_COMMIT_MSG=true
ENABLE_PRE_PUSH=true
ENABLE_POST_CHECKOUT=true
ENABLE_POST_MERGE=true
ENABLE_PREPARE_COMMIT_MSG=true

## Pre-commit settings
MAX_FILE_SIZE_KB=1000
ENABLE_SECRET_DETECTION=true
ENABLE_LINT_PYTHON=true
ENABLE_LINT_SHELL=true
ENABLE_LINT_YAML=true
ENABLE_LINT_TERRAFORM=true

## Commit message settings
REQUIRE_CONVENTIONAL_COMMITS=true
REQUIRE_TICKET_REFERENCE=false
MAX_SUBJECT_LENGTH=72

## Pre-push settings
RUN_TESTS_BEFORE_PUSH=true
PROTECT_BRANCHES="main master production"
BLOCK_FORCE_PUSH=true
BLOCK_WIP_COMMITS=true

Hook Runner Pattern

#!/bin/bash
## .githooks/lib/hook-runner.sh
## Shared hook runner that executes scripts from a .d directory
## Usage: source this file and call run_hook_dir

set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m'

run_hook_dir() {
    local hook_name="$1"
    shift
    local hook_dir
    hook_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/${hook_name}.d"

    if [[ ! -d "${hook_dir}" ]]; then
        return 0
    fi

    local errors=0

    for script in "${hook_dir}"/*; do
        if [[ -x "${script}" ]] && [[ -f "${script}" ]]; then
            local script_name
            script_name=$(basename "${script}")
            echo -e "${YELLOW}Running ${hook_name}/${script_name}...${NC}"

            if "${script}" "$@"; then
                echo -e "${GREEN}${script_name} passed${NC}"
            else
                echo -e "${RED}${script_name} failed${NC}"
                errors=$((errors + 1))
            fi
        fi
    done

    if [[ ${errors} -gt 0 ]]; then
        echo -e "\n${RED}${hook_name}: ${errors} check(s) failed.${NC}"
        return 1
    fi

    return 0
}

Using the Hook Runner

#!/bin/bash
## .githooks/pre-commit
## Delegates to scripts in pre-commit.d/ using the shared runner

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

# shellcheck source=lib/hook-runner.sh
source "${SCRIPT_DIR}/lib/hook-runner.sh"

run_hook_dir "pre-commit" "$@"
#!/bin/bash
## .githooks/commit-msg
## Delegates to scripts in commit-msg.d/ using the shared runner

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

# shellcheck source=lib/hook-runner.sh
source "${SCRIPT_DIR}/lib/hook-runner.sh"

run_hook_dir "commit-msg" "$@"
#!/bin/bash
## .githooks/pre-push
## Delegates to scripts in pre-push.d/ using the shared runner

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

# shellcheck source=lib/hook-runner.sh
source "${SCRIPT_DIR}/lib/hook-runner.sh"

run_hook_dir "pre-push" "$@"

Complete Directory Structure

.githooks/
├── lib/
│   └── hook-runner.sh              # Shared hook runner
├── pre-commit                      # Delegates to pre-commit.d/
├── pre-commit.d/
│   ├── 01-lint                     # Linting and formatting
│   ├── 02-check-secrets            # Secret detection
│   └── 03-check-file-size          # Large file check
├── commit-msg                      # Delegates to commit-msg.d/
├── commit-msg.d/
│   ├── 01-conventional-commit      # Format validation
│   ├── 02-check-ticket             # Ticket reference
│   └── 03-check-spelling           # Spell check
├── pre-push                        # Delegates to pre-push.d/
├── pre-push.d/
│   ├── 01-run-tests                # Test suite
│   ├── 02-protect-branches         # Branch protection
│   ├── 03-prevent-force-push       # Force push guard
│   └── 04-check-wip                # WIP commit detection
├── prepare-commit-msg              # Branch-based ticket insertion
├── post-checkout                   # Dependency auto-install
├── post-checkout.d/
│   └── 01-check-env                # Environment change alerts
├── post-merge                      # Dependency sync
├── post-merge.d/
│   ├── 01-run-migrations           # Database migrations
│   └── 02-check-lockfiles          # Lock file drift
├── install.sh                      # Hook installer
├── uninstall.sh                    # Hook remover
└── .githooks.conf                  # Configuration

Team Adoption and Best Practices

Onboarding with Makefile

## Makefile
## First command new developers run after cloning

.PHONY: setup hooks test

## Complete project setup including hooks
setup: hooks
    uv sync
    cp .env.example .env
    @echo ""
    @echo "Setup complete. Run 'make test' to verify."

## Install git hooks
hooks:
    @bash .githooks/install.sh

## Run the full test suite
test:
    pytest tests/ -v
## Developer onboarding flow
git clone https://github.com/org/repo.git
cd repo
make setup
## Git hooks are now installed automatically

CI/CD as Backstop

## .github/workflows/ci.yml
## CI enforces the same checks as local hooks
## This catches anything that bypasses hooks via --no-verify

name: CI
on:
  pull_request:
    branches: [main]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Conventional commit check
        run: |
          ## Same validation as commit-msg hook
          COMMIT_MSG=$(git log -1 --pretty=%s)
          PATTERN="^(feat|fix|docs|style|refactor|test|chore|ci|perf|build|revert)(\([a-z0-9\-]+\))?!?: .+"
          if ! echo "${COMMIT_MSG}" | grep -qE "${PATTERN}"; then
            echo "::error::Commit message does not follow conventional format"
            exit 1
          fi

      - name: Secret detection
        run: |
          ## Same patterns as pre-commit secret detection hook
          if git diff --cached -G'AKIA[0-9A-Z]{16}' --name-only | grep -q .; then
            echo "::error::Potential AWS key detected"
            exit 1
          fi

      - name: Run tests
        run: pytest tests/ --tb=short

Progressive Hook Adoption

#!/bin/bash
## .githooks/install.sh --progressive
## Install hooks in stages for gradual team adoption

set -euo pipefail

LEVEL="${1:-basic}"

case "${LEVEL}" in
    basic)
        ## Stage 1: Non-blocking, informational only
        echo "Installing basic hooks (informational)..."
        git config core.hooksPath .githooks
        ## Only install post-checkout and post-merge (non-blocking)
        ;;
    standard)
        ## Stage 2: Add commit message validation
        echo "Installing standard hooks..."
        git config core.hooksPath .githooks
        ## Adds commit-msg validation
        ;;
    strict)
        ## Stage 3: Full enforcement
        echo "Installing strict hooks (full enforcement)..."
        git config core.hooksPath .githooks
        ## All hooks enabled including pre-push tests
        ;;
    *)
        echo "Usage: $0 [basic|standard|strict]"
        exit 1
        ;;
esac

echo "Hooks installed at level: ${LEVEL}"

Documenting Hooks in README

## Git Hooks

This project uses native git hooks for development workflow automation.

### Quick Setup

    make setup

### What the Hooks Do

| Hook | Purpose | Blocking |
|------|---------|----------|
| pre-commit | Linting, formatting, secret detection | Yes |
| commit-msg | Conventional commit validation | Yes |
| pre-push | Runs tests, branch protection | Yes |
| post-checkout | Auto-installs dependencies | No |
| post-merge | Syncs dependencies, migration alerts | No |
| prepare-commit-msg | Auto-inserts ticket reference | No |

### Bypassing Hooks

    # Skip pre-commit and commit-msg hooks
    git commit --no-verify -m "emergency fix"

    # Skip pre-push hooks
    git push --no-verify

> **Note**: CI/CD enforces the same checks. Bypassing hooks locally
> does not bypass CI validation.

Troubleshooting

Hook Not Executing

## Check if hooks are executable
ls -la .githooks/
## Expected: -rwxr-xr-x for each hook file

## Fix permissions
chmod +x .githooks/*
chmod +x .githooks/**/*

## Verify core.hooksPath is set
git config --get core.hooksPath
## Expected: .githooks

## If not set, install hooks
bash .githooks/install.sh

Hook Fails with "Permission Denied"

## Diagnose permission issues
file .githooks/pre-commit
## Expected: .githooks/pre-commit: Bourne-Again shell script, ASCII text executable

## Fix shebang line - ensure it starts with:
head -1 .githooks/pre-commit
## Expected: #!/bin/bash

## Fix line endings (Windows CRLF causes issues)
sed -i 's/\r$//' .githooks/pre-commit

## Alternative: use dos2unix
dos2unix .githooks/pre-commit

Hook Path Issues

## Check if git can find the hooks
git rev-parse --git-dir
## Output: .git

## List active hooks
ls -la "$(git rev-parse --git-dir)/hooks/"

## If using core.hooksPath, check that path exists
HOOKS_PATH=$(git config --get core.hooksPath)
ls -la "${HOOKS_PATH}/"

## Reset to default hooks directory
git config --unset core.hooksPath

Debugging Hook Execution

#!/bin/bash
## Add to any hook for debug output

## Enable verbose mode
set -x

## Log hook execution
echo "[$(date)] Running $(basename "$0")" >> /tmp/git-hooks.log

## Show environment variables available to hooks
env | grep GIT_ >> /tmp/git-hooks.log

## Show the arguments passed to the hook
echo "Args: $*" >> /tmp/git-hooks.log
## Run a hook manually for testing
bash -x .githooks/pre-commit

## Test commit-msg hook with a sample message
echo "feat: add login endpoint" > /tmp/test-msg
bash -x .githooks/commit-msg /tmp/test-msg

## Test pre-push hook (reads from stdin)
echo "refs/heads/main abc123 refs/heads/main def456" | bash -x .githooks/pre-push

Conflicting with Pre-commit Framework

## If using both native hooks and pre-commit framework,
## the pre-commit framework installs its own hooks in .git/hooks/

## Option 1: Use pre-commit for pre-commit hook, native for others
## Let pre-commit manage .git/hooks/pre-commit
pre-commit install

## Manually install other native hooks
cp .githooks/pre-push .git/hooks/pre-push
cp .githooks/post-checkout .git/hooks/post-checkout
cp .githooks/post-merge .git/hooks/post-merge
chmod +x .git/hooks/pre-push .git/hooks/post-checkout .git/hooks/post-merge

## Option 2: Chain native hooks from pre-commit
## In .pre-commit-config.yaml:
## repos:
##   - repo: local
##     hooks:
##       - id: native-pre-commit
##         name: Native pre-commit checks
##         entry: bash .githooks/pre-commit.d/02-check-secrets
##         language: system
##         pass_filenames: false

Resources