# Pre-commit hooks for signal-bot. # # Install with: pre-commit install # Run manually: pre-commit run --all-files # # Every hook is `language: system` because the tools already live in .venv; # pre-commit must not build isolated environments for them. fail_fast so a # later check cannot run after an earlier one has already rejected the commit. fail_fast: true repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: trailing-whitespace args: [--markdown-linebreak-ext=md] - id: end-of-file-fixer - id: check-yaml - id: check-json - id: check-toml - id: check-merge-conflict - id: check-case-conflict - id: check-executables-have-shebangs - id: check-shebang-scripts-are-executable - id: detect-private-key - id: debug-statements - id: check-ast - id: mixed-line-ending args: [--fix=lf] - repo: local hooks: # Suppressions are blocked mechanically, not by discipline. This repo # has zero of them and the only way it stays that way is a gate: the # rule is "fix the finding, or ask first", and a habit is not a gate. - id: no-noqa name: block noqa / type:ignore comments entry: '(?i)#\s*(noqa|type:\s*ignore)' language: pygrep types: [python] - id: no-ruff-noqa name: block file-level ruff noqa entry: '(?i)#\s*ruff:\s*noqa' language: pygrep types: [python] # This repository is public and holds a private message database. The # database is gitignored, but three binary SQLite files reached the # history anyway before this gate existed, so .gitignore is evidently # not the whole defence. Exceptions go in .binary-allowlist. # This repository is public and leaked once: real names, two phone # numbers and a member's Signal uuid reached GitHub and only came off # it by deleting the repository. The checker learns what to refuse from # OUTSIDE the repo -- the photo pack, the message database, .env -- so # the list of real names is never in the thing being checked. - id: personal-data name: no personal data entry: scripts/check_personal_data.sh language: system types: [text] # The same check on the commit message, because that is where two of # the leaks actually lived. - id: personal-data-message name: no personal data in the commit message entry: scripts/check_personal_data.sh --message language: system stages: [commit-msg] always_run: true pass_filenames: true - id: no-binaries name: no binary or image files entry: scripts/check_no_binaries.sh language: script pass_filenames: true # The shared 250-line cap. Checks only the files in the commit, so an # unrelated commit never breaks. No baseline, no allowlist -- those are # suppressions. The exemption list lives with the checker in ~/utils so # this repo's idea of the rule cannot drift from every other repo's. - id: file-length name: file length <= 250 lines entry: scripts/check_file_length.sh language: system # Four markdown namespaces (README / CLAUDE* / DOCS* / TODO*), plus the # rule that a TODO carries "REMOVE ME AFTER FINISH" and nothing else may. - id: md-naming name: markdown naming convention entry: scripts/check_md_naming.sh language: system files: \.md$ # Exact pins at newest stable. Runs repo-wide: a pin goes stale because # upstream released, not because this commit touched the manifest. - id: dependency-freshness name: dependencies are newest stable entry: scripts/check_dependency_freshness.sh --all language: system pass_filenames: false always_run: true - id: ruff-format name: ruff format entry: .venv/bin/ruff format --check language: system types_or: [python, pyi] - id: ruff name: ruff (ALL rules) entry: .venv/bin/ruff check --force-exclude language: system types_or: [python, pyi] - id: mypy name: mypy --strict entry: .venv/bin/mypy language: system pass_filenames: false args: [signal_bot, fake_signal] - id: shellcheck name: shellcheck # Same severity floor as CI: a stricter runner version must not be # the thing that discovers a finding this hook let through. entry: shellcheck -x --severity=style language: system types: [shell] # The watchdog is bash, so pytest cannot reach it. Its own suite stubs # curl and docker and drives every branch, including the give-up cap. - id: watchdog-tests name: watchdog behaviour (bash) entry: scripts/test_watchdog.sh language: system pass_filenames: false files: ^scripts/(signal_api_watchdog|test_watchdog)\.sh$ # 100% branch coverage, no omits. Runs on every commit: coverage is the # whole point of the suite, and a partial run cannot prove it. - id: pytest name: pytest (100% branch coverage) entry: .venv/bin/python -m pytest -q language: system pass_filenames: false always_run: true