# ============================================================================== # Pre-commit Configuration - Multi-language Linting & Formatting # ============================================================================== # Install: pre-commit install && pre-commit install --hook-type pre-push # Fast lint: pre-commit run --all-files (linters only, ~10 s) # Full suite: pre-commit run --all-files --hook-stage pre-push (+ tests) # Update hooks: pre-commit autoupdate # ============================================================================== # Global settings default_language_version: python: python3 # By default every hook runs ONLY at commit-time. Pre-push runs only the # hooks that explicitly opt in via `stages: [pre-push]` (currently # pytest-coverage + prettier). This keeps `git push` cheap; commit-time # already enforces the full lint suite. default_stages: [pre-commit] # Fail fast on first error (set to false to see all errors) fail_fast: false # Configuration ci: autofix_commit_msg: "style: auto-fix by pre-commit hooks" autoupdate_commit_msg: "chore: update pre-commit hooks" repos: # =========================================================================== # GENERAL HOOKS - File formatting and validation # =========================================================================== - 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 args: [--unsafe] - id: check-json # Exclude JSONC files (VS Code configs, TypeScript configs) exclude: ^(\.vscode/|.*/\.vscode/|.*tsconfig.*\.json) - id: check-toml - id: check-xml - id: check-added-large-files args: [--maxkb=2000] - id: check-merge-conflict - id: check-case-conflict - id: check-symlinks - id: check-executables-have-shebangs - id: check-shebang-scripts-are-executable - id: detect-private-key - id: debug-statements - id: name-tests-test args: [--pytest-test-first] - id: check-ast - id: check-builtin-literals - id: check-docstring-first - id: fix-byte-order-marker - id: mixed-line-ending args: [--fix=lf] - id: requirements-txt-fixer # =========================================================================== # BINARY BLOCKER - Prevent binary/image files from being committed # =========================================================================== - repo: local hooks: - id: no-binaries name: Block binary/image files entry: meta/scripts/check_no_binaries.sh language: script always_run: false - id: file-length-cap name: Enforce the 250-line file cap # Repo-wide (--all), not per-file: a file that grows past the cap # without being staged in this commit must still be caught. Wired only # after the campaign that brought every file under it — turning this on # earlier would have blocked every unrelated commit in the repo. entry: bash meta/scripts/check_file_length.sh --all language: system pass_filenames: false always_run: true - id: directory-depth-cap name: Enforce the directory-depth cap # Repo-wide and ENFORCING. It ran --warn while the 46 paths under # periodic_background/{hosts,digital_wellbeing} were over the cap; # both units are now their own repos (kuhyx/hosts-blocker, # kuhyx/digital-wellbeing) and the count is 0, so the cap is real. # Re-adding --warn would silently re-open the hole it was there to # close: put the offending path somewhere legal instead. entry: bash meta/scripts/check_directory_depth.sh --all language: system pass_filenames: false always_run: true - id: repo-size-cap name: Enforce the source-file ceiling # Counts tracked files only, excluding vendored and hook-generated # trees. Prints the count on every run so the ceiling is auditable. entry: bash meta/scripts/check_repo_size.sh language: system pass_filenames: false always_run: true - id: ci-baseline-green name: Refuse to build on a red CI baseline # Checks the last COMPLETED run of each required workflow on main. # Fails closed on a definitively red baseline; warns and passes on # anything it cannot determine (offline, no gh, no runs yet), because # the real per-change guarantee is the pre-push ci-mirror hook and # blocking every commit on network reachability is not worth it. # Bypass a deliberate fix-forward commit with CI_GREEN_SKIP=1. entry: bash meta/scripts/check_ci_green.sh language: system pass_filenames: false always_run: true - id: ai-evidence-contract name: Require AI evidence artifacts for code changes entry: meta/scripts/check_ai_evidence.sh language: script pass_filenames: false always_run: true - id: ai-multifile-contract name: Require workflow contract for multi-file code changes entry: meta/scripts/check_agent_contract.sh language: script pass_filenames: false always_run: true - id: append-only-sessions name: Enforce append-only session logs entry: meta/scripts/check_append_only_sessions.sh language: script pass_filenames: false always_run: true # =========================================================================== # POLLING SCRIPT LINTER - Detect fork-storm anti-patterns in shell scripts # =========================================================================== - repo: local hooks: - id: no-inline-python name: Block Python logic embedded in shell scripts entry: meta/scripts/check_no_inline_python.sh language: script types_or: [shell, zsh] - id: no-polling-antipatterns name: Block polling script anti-patterns entry: meta/scripts/check_polling_antipatterns.sh language: script types: [shell] require_serial: true exclude: ^(\.git/|tests/) - id: shell-coverage-ratchet name: Every shell library has a test suite beside it # The ratchet from meta/shell-coverage-allowlist.txt: a lib under a # lib/ directory needs a tests/run_all.sh, unless it is allowlisted. # Wired here on 2026-08-23. It existed but was never in pre-commit, # so it had been silently unenforced -- a new untested lib could land # freely. That was only affordable to fix once 578c4a6c dropped the # jail-measured 100% bar: --all went from >600s to 0.31s, and the # per-file mode this hook uses is milliseconds. entry: bash meta/scripts/check_shell_coverage.sh language: system types: [shell] exclude: ^third_party/ # =========================================================================== # NOQA BLOCKER - Zero tolerance for noqa/type:ignore suppression comments # =========================================================================== - repo: local hooks: - id: no-noqa name: Block noqa comments entry: '(?i)#\s*(noqa|type:\s*ignore)' language: pygrep types: [python] - id: no-ruff-noqa name: Block ruff noqa file-level comments entry: '(?i)#\s*ruff:\s*noqa' language: pygrep types: [python] # =========================================================================== # RUFF - Fast Python linter and formatter (replaces black, isort, flake8, etc.) # =========================================================================== - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.2 hooks: # Linter - run first to catch issues - id: ruff args: - --fix - --unsafe-fixes - --exit-non-zero-on-fix - --show-fixes types_or: [python, pyi] # Formatter - run after linting - id: ruff-format types_or: [python, pyi] # =========================================================================== # MYPY - Static type checking (per-commit on changed files only) # Was on pre-push, but force-push diffs caused full-repo scans + OOM. On # pre-commit it sees only the file(s) currently staged → near-instant. # =========================================================================== - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.13.0 hooks: - id: mypy stages: [pre-commit] args: - --ignore-missing-imports - --no-error-summary - --disable-error-code=no-untyped-def - --disable-error-code=no-untyped-call - --disable-error-code=var-annotated - --disable-error-code=no-any-unimported - --disable-error-code=type-arg - --disable-error-code=no-any-return - --disable-error-code=misc - --disable-error-code=unused-ignore - --disable-error-code=unreachable - --disable-error-code=assignment - --disable-error-code=no-redef - --disable-error-code=attr-defined - --disable-error-code=arg-type - --disable-error-code=union-attr - --disable-error-code=call-overload - --disable-error-code=return-value - --disable-error-code=redundant-cast - --disable-error-code=empty-body - --disable-error-code=list-item exclude: >- (?x)^( Bash/.*| \.venv/.*| linux_configuration/misc/tools/.* )$ additional_dependencies: - types-requests - types-PyYAML - types-python-dateutil # =========================================================================== # PYLINT - Comprehensive Python linter (per-commit on changed files only) # Was on pre-push, but force-push diffs caused full-repo scans + OOM. # =========================================================================== - repo: https://github.com/pylint-dev/pylint rev: v3.3.2 hooks: - id: pylint stages: [pre-commit] args: - --rcfile=pyproject.toml - --fail-under=10 - --jobs=4 additional_dependencies: - pytest - python-chess - requests - pygame - pillow # archwiki_rag parses the local arch-wiki-docs HTML dump. pylint runs # in its own venv, so a package being in meta/requirements.txt is not # enough for it to resolve the import. - beautifulsoup4 - markdownify # android_ui parses `uiautomator dump` output. Same reason as # beautifulsoup4 above: requirements.txt is not on pylint's path. - defusedxml # artgate computes image gates over pixel arrays. Same reason again: # it is the first package here to import numpy. - numpy # js_anki builds the Anki .apkg; same isolated-venv reason as above. - genanki - gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.1.0#subdirectory=gatelock - sql_drills @ git+https://github.com/kuhyx/utils@sql-drills-v0.2.2#subdirectory=sql_drills - anthropic - rich - typer # linux_configuration/installers: verify_helium drives Helium # over the DevTools websocket; migrate_helium_passwords_to_basic re-encrypts # Chromium os_crypt blobs (AES-CBC). - websockets - cryptography - secretstorage # Test suites and conftest fixtures are linted separately; they use # patterns (protected-access, missing docstrings, fixture shadowing) # that don't belong in the source-code 10/10 gate. exclude: ^(Bash/|\.venv/)|(^|/)(tests/|conftest\.py) # =========================================================================== # BANDIT - Security linter (per-commit on changed files only) # Was on pre-push, but force-push diffs caused full-repo scans + OOM. # =========================================================================== - repo: https://github.com/PyCQA/bandit rev: 1.7.10 hooks: - id: bandit stages: [pre-commit] args: - -c - pyproject.toml - --severity-level=high - --confidence-level=medium - --skip=B113 additional_dependencies: ["bandit[toml]"] exclude: ^(Bash/|\.venv/|tests/|.*test.*\.py$) # =========================================================================== # PYTEST + COVERAGE - Run tests and enforce 100% code coverage # Only tests for subpackages with changed files are run (see script). # Uses pytest-xdist (-n auto) to parallelize across all CPUs. # =========================================================================== - repo: local hooks: - id: pytest-coverage name: pytest with coverage enforcement # Serialised against builds and pacman transactions on this 7.6 GB # machine via the heavy-job lock. run_with_heavy_lock.sh is a plain # passthrough on any clone that has not installed one. entry: bash meta/scripts/run_with_heavy_lock.sh pytest python meta/scripts/pytest_changed_packages.py language: system types: [python] pass_filenames: true require_serial: true stages: [pre-commit] - id: requirements-sync name: requirements.txt covers all python_pkg imports entry: python3 meta/scripts/check_requirements_sync.py --package python_pkg language: system pass_filenames: false files: ^(python_pkg/.*\.py|requirements\.txt)$ - id: requirements-test-sync name: requirements-test.txt covers all python_pkg imports # python-tests.yml installs this SUBSET rather than the full list, so # it needs the same completeness guarantee. Without this hook a new # third-party import would pass locally and fail in CI with a bare # ModuleNotFoundError. entry: python3 meta/scripts/check_requirements_sync.py --package python_pkg --requirements meta/requirements-test.txt language: system pass_filenames: false files: ^(python_pkg/.*\.py|meta/requirements-test\.txt)$ - id: session-autopsy-hook-mutations name: session_autopsy hook tests are non-vacuous (mutation) entry: python3 meta/scripts/mutate_session_autopsy_hooks.py language: system pass_filenames: false require_serial: true # Scoped to the two files that can invalidate the proof: the suite # itself and the mutation definitions. Takes ~40s, so it must not run # on unrelated commits. Skips itself where ~/.claude is absent (CI). files: ^(linux_configuration/tests/test_session_autopsy_hooks\.py|meta/scripts/mutate_session_autopsy_hooks\.py)$ - id: ci-mirror name: CI mirror (background; a failed verdict blocks the next push) entry: bash meta/scripts/ci_mirror.sh language: system pass_filenames: false always_run: true require_serial: true stages: [pre-push] - id: repo-contract name: repo contract (CLAUDE.md Commands + test_changed) entry: scripts/check_repo_contract.sh language: system pass_filenames: false files: ^(CLAUDE\.md|AGENTS\.md|scripts/test_changed\.sh)$ # =========================================================================== # VULTURE - Dead code detection (disabled - doesn't work well with pre-commit) # =========================================================================== # - repo: https://github.com/jendrikseipp/vulture # rev: v2.13 # hooks: # - id: vulture # args: # - --min-confidence=80 # - --exclude=.venv,Bash,__pycache__ # exclude: ^(Bash/|\.venv/) # =========================================================================== # PYUPGRADE - Upgrade Python syntax (disabled - incompatible with Python 3.14) # =========================================================================== # - repo: https://github.com/asottile/pyupgrade # rev: v3.19.0 # hooks: # - id: pyupgrade # args: # - --py310-plus # =========================================================================== # CODESPELL - Spell checking in code (expanded ignore list for non-English) # =========================================================================== - repo: https://github.com/codespell-project/codespell rev: v2.3.0 hooks: - id: codespell args: - --skip=*.json,*.lock,*-lock.yaml,*.min.js,*.min.css,.git,__pycache__,.venv,*.txt # Many entries below are Polish words, not typos: they are field # names from Polish data formats (e.g. `dokument` in the e-paragon / # JPK e-receipt schema that billsplit parses). "Correcting" them to # English breaks the parsers that match on them. # `daa` is MediaTek's Download Agent Authentication, a BROM security # feature named in the mtk_root notes - not a misspelling of "data". # `artic` is SearXNG's engine id for the Art Institute of Chicago # (setup_searxng.sh), not a misspelling of "arctic" - renaming it # would stop matching the engine. # `imput` is Helium's vendor (imput.net): its profile dir is # `~/.config/net.imput.helium` and the installers/*helium* scripts spell it out. - --ignore-words-list=als,ans,ect,nd,som,sur,te,nam,numer,lew,sie,wil,postion,clen,ther,folow,derrive,ony,tje,noe,theses,crate,doubleclick,wile,tabel,pary,blok,bloc,proces,serwer,parametr,adres,hart,dout,metod,tekst,synonim,grup,mosty,lokal,skalar,milion,nowe,tre,hel,alph,iif,dokument,daa,artic,imput exclude: ^(Bash/ffmpeg-build/|LaTeX/|.*\.geojson$) # =========================================================================== # DOCFORMATTER - Format docstrings (disabled - causes recursion errors) # =========================================================================== # - repo: local # hooks: # - id: docformatter # name: docformatter # entry: docformatter # language: system # types: [python] # args: # - --in-place # - --wrap-summaries=88 # - --wrap-descriptions=88 # =========================================================================== # INTERROGATE - Docstring coverage (disabled - causes recursion on large files) # =========================================================================== # - repo: https://github.com/econchick/interrogate # rev: 1.7.0 # hooks: # - id: interrogate # args: # - --fail-under=0 # - --verbose # - --ignore-init-method # - --ignore-init-module # - --ignore-magic # - --ignore-private # - --ignore-semiprivate # - --exclude=Bash,.venv,__pycache__ # pass_filenames: false # =========================================================================== # AUTOFLAKE - Remove unused imports/variables # Disabled: fully redundant with ruff (F401, F841, F811) + --fix # =========================================================================== # - repo: https://github.com/PyCQA/autoflake # rev: v2.3.1 # hooks: # - id: autoflake # args: # - --in-place # - --remove-all-unused-imports # - --remove-unused-variables # - --remove-duplicate-keys # - --expand-star-imports # =========================================================================== # SAFETY - Check for security vulnerabilities in dependencies # =========================================================================== # Note: Safety requires API key for full functionality, disabled by default # - repo: https://github.com/Lucas-C/pre-commit-hooks-safety # rev: v1.3.2 # hooks: # - id: python-safety-dependencies-check # files: requirements.*\.txt$ # =========================================================================== # PYRIGHT - Microsoft's type checker (very strict, optional) # =========================================================================== # Uncomment to enable - can be slow and very strict # - repo: https://github.com/RobertCraiworthy/pyright-action # rev: v1.1.350 # hooks: # - id: pyright # =========================================================================== # CHECK JSON/YAML/TOML formatting (runs on push only — slow Node.js startup) # =========================================================================== - repo: local hooks: - id: prettier name: prettier (capped scope) # Wrapper runs prettier inside its own systemd-run scope so its # memory budget is independent of the outer pre-push cgroup that # has already accumulated page cache from pytest/mypy/pylint. entry: meta/scripts/run_prettier_capped.sh language: system types_or: [yaml, json, markdown] exclude: >- (?x)^( Bash/.*| \.venv/.*| third_party/.*| \.github/skills/.*| docs/superpowers/(plans|specs)/.*| linux_configuration/report/.*| .*\.lock )$ stages: [pre-push] # =========================================================================== # SHELLCHECK - Shell script linting # Wrapper batches files to avoid OOM on large repos. # =========================================================================== - repo: local hooks: - id: shellcheck name: shellcheck # NO --severity filter: every finding shellcheck reports, down to info # and style, fails the commit. The previous --severity=warning quietly # accepted 222 findings across 94 of 244 shell files -- and among them # were real defects, not noise: two scripts that reported SUCCESS on an # unknown command, five that sourced a path off by one directory (so # they failed at runtime), a stale `source=` directive that had silently # disabled following for six i3blocks scripts, and a diagnostic that # would hang on stdin when its file list came back empty. # Settings live in .shellcheckrc (external-sources, source-path). entry: bash -c 'printf "%s\0" "$@" | xargs -0 -n 40 shellcheck' -- language: system types: [shell] # shellcheck only understands sh/bash/dash/ksh; it cannot parse zsh # syntax (e.g. extended globs like (#s)). zsh files are checked by the # zsh-syntax hook below instead. exclude_types: [zsh] # Vendored upstream code: we do not own it and patches here would be # lost on the next vendor sync. Matches the prettier hook's exclusion. # # PKGBUILDs are makepkg manifests, not standalone scripts: they have no # shebang by design, and pkgname/pkgver/source/sha512sums are read by # makepkg while pkgdir/srcdir are injected by it. shellcheck therefore # reports every one of them as unused-or-undefined (SC2148/SC2034/ # SC2154) -- all false positives, and none suppressible without # littering the file with directives. Same reasoning as zsh above: # the dialect is outside what shellcheck can judge. exclude: ^third_party/|(^|/)PKGBUILD$ # =========================================================================== # ZSH SYNTAX - zsh -n parse check (shellcheck has no zsh mode) # =========================================================================== - repo: local hooks: - id: zsh-syntax name: zsh syntax check entry: bash -c 'for f in "$@"; do zsh -n "$f" || exit 1; done' -- language: system types: [zsh] # =========================================================================== # CHECK PYTHON LOCATION - All Python files must be under python_pkg/ # =========================================================================== - repo: local hooks: - id: check-python-location name: check Python files are under python_pkg/ entry: meta/scripts/check_python_location.sh language: script types: [python] # =========================================================================== # REMOVE EMPTY DIRECTORIES - Clean up empty folders in the repo # =========================================================================== - repo: local hooks: - id: remove-empty-dirs name: remove empty directories entry: find . -type d -empty -not -path './.git/*' -delete -print language: system pass_filenames: false always_run: true # =========================================================================== # SECRET PATTERNS - Block commits containing sensitive data # =========================================================================== - repo: local hooks: - id: check-no-secrets name: check for leaked secrets entry: meta/scripts/check_no_secrets.sh language: script exclude: ^(\.secret-patterns|\.pre-commit-config\.yaml|.*\.geojson)$ - id: check-no-dufs-conf name: block dufs secret config (.dufs_cloud.conf) entry: meta/scripts/check_no_dufs_conf.sh language: script always_run: true pass_filenames: false # =========================================================================== # COMMITIZEN - Conventional commits (optional) # =========================================================================== # - repo: https://github.com/commitizen-tools/commitizen # rev: v3.13.0 # hooks: # - id: commitizen # - id: commitizen-branch # stages: [push] # Four markdown namespaces (README / CLAUDE* / DOCS* / TODO*), plus the # rule that a TODO carries "REMOVE ME AFTER FINISH" and nothing else may. # Shared gate in ~/src/utils owns the namespaces and the exemption list, so # one repo's idea of a valid name cannot drift from every other repo's. - id: md-naming name: markdown naming convention entry: scripts/check_md_naming.sh language: system files: \.(md|markdown)$ # Every dependency on its ecosystem's newest STABLE release, exact-pinned # so the question is mechanically answerable at all. # # `files:` is what keeps this cheap: a commit touching only source makes # ZERO network requests, and manifest commits mostly hit a 6h disk cache. # # Runs WITHOUT --strict on purpose. Offline it degrades to the cached # answer and passes with a loud banner; `--no-verify` is banned here, so # a hook that hard-failed on a flaky connection would leave no way to # commit at all. CI, always online, is the strict authority. - id: dependency-freshness name: dependencies on newest stable entry: scripts/check_dependency_freshness.sh language: system pass_filenames: true files: >- (pubspec\.(yaml|lock)|pyproject\.toml|requirements.*\.txt|uv\.lock |package\.json|package-lock\.json|pnpm-lock\.yaml |Cargo\.(toml|lock)|go\.(mod|sum) |\.fvmrc|\.nvmrc|\.python-version |\.github/workflows/.*\.ya?ml)$