- pre-commit-tools
- Using pre-commit-tools with pre-commit
- Hooks available
- format-dockerfiles
- python-print-detection
- python-pprint-detection
- console-debug-detection
- console-log-detection
- console-table-detection
- react-console-error-detection
- pylint-html-report
- yaml-sorter
- debugger-detection
- json-sorter
- requirements-sort
- env-file-check
- env-example-sync
- python-logger-detection
- python-unreachable-code
- python-dead-code
- no-bare-except
- no-print-in-migration
- django-hardcoded-secret
- dockerfile-no-latest
- ts-unreachable-code
- css-duplicate-property
- css-unused-variable
- no-console-warn
- react-direct-dom
- import-no-relative-parent
- no-debug-in-settings
- django-no-raw-sql
- no-sync-in-async
- fastapi-missing-response-model
- js-syntax-check
- ts-hardcoded-secret-detection
- helm-lint
- makefile-check
- generate-changelog
Some out-of-the-box hooks for pre-commit.
Add this to your .pre-commit-config.yaml
- repo: https://github.com/chrysa/pre-commit-tools
- rev: v0.1.1-37
hooks:
- id: console-debug-detection
- id: console-log-detection
- id: console-table-detection
- id: react-console-error-detection
- id: format-dockerfiles
- id: dockerfile-no-latest
- id: python-print-detection
- id: python-pprint-detection
- id: no-bare-except
- id: no-print-in-migration
- id: django-hardcoded-secret
- id: yaml-sorter
- id: debugger-detection
- id: json-sorter
- id: requirements-sort
- id: env-file-check
- id: env-example-sync
- id: python-logger-detection
- id: python-unreachable-code
# optional — run manually: pre-commit run python-dead-code --hook-stage manual --all-files
- id: python-dead-code
stages: [manual]
- id: ts-unreachable-code
- id: css-duplicate-property
- id: css-unused-variable
- id: no-console-warn
- id: react-direct-dom
- id: import-no-relative-parent
- id: no-debug-in-settings
- id: django-no-raw-sql
- id: no-sync-in-async
- id: fastapi-missing-response-model
- id: js-syntax-check
- id: ts-hardcoded-secret-detection
# requires helm in PATH
- id: helm-lint
# generate-changelog uses git-cliff; run manually or on CI
- id: generate-changelog
stages: [manual]
# Optional — guideline-checker (structural coding guidelines)
# Validates project structure, naming conventions, and coding standards.
# See https://github.com/chrysa/guideline-checker
#- repo: https://github.com/chrysa/guideline-checker
# rev: '' # Use the ref you want to point at
# hooks:
# - id: guideline-check
# stages: [pre-push, manual]- Add shebang
# syntax=docker/dockerfile:1.4if missing - Accept any pinned
# syntax=docker/dockerfile:<version>header (e.g.1,1.7,1.7.0) and keep it as-is — only a missing header is rewritten/blocked - Warn (non-blocking) when the pinned version trails the latest stable
docker/dockerfilerelease on Docker Hub by 3 or more releases. The tag list is fetched best-effort (3 s timeout, cached 24 h) and any network failure is ignored, so the hook never breaks an offline commit - Group consecutive same-instruction blocks without blank lines
- Merge consecutive
RUNorENVinstructions on one command line with continuation
New options:
| Option | Description |
|---|---|
--sort-args |
Sort ARG instructions alphabetically |
--sort-envs |
Sort ENV instructions alphabetically |
--separate-arg-blocks |
Separate literal ARG from variable-dependent ARG (e.g. ARG FOO=${BAR}) |
-c / --config |
Path to a .format-dockerfiles.toml config file |
Config file example (.format-dockerfiles.toml):
[format-dockerfiles]
sort_args = true
sort_envs = true
separate_arg_blocks = trueDetect print() calls in Python files. Use # print-detection: disable to ignore a specific line.
Detect pprint() calls in Python files. Use # pprint-detection: disable to ignore a specific line.
Detect console.debug() calls in JavaScript/Google AppScript (.js, .gs) files.
Use // console-debug-detection: disable to ignore a specific line.
Detect console.log() calls in JavaScript/Google AppScript files.
Use // console-log-detection: disable to ignore a specific line.
Detect console.table() calls in JavaScript/Google AppScript files.
Use // console-table-detection: disable to ignore a specific line.
Run Pylint over Python files and generate an HTML report via pylint-report.
- id: pylint-with-html-report
additional_dependencies: [pylint, pylint-report]
args:
- '--output-html=reports/pylint' # directory for the HTML report (default: ./html)
- '--output-json=pylint.json' # keep the intermediate JSON (omit to auto-delete)| Option | Default | Description |
|---|---|---|
--output-html |
./html |
Directory where the HTML report is written |
--output-json |
auto-deleted | Path for the intermediate JSON report; deleted after conversion unless specified |
Sort YAML file keys alphabetically (recursive). Modifies files in-place and returns 1 if any file was changed.
To skip a file, exclude it in your .pre-commit-config.yaml.
Boolean and mixed-type keys are handled safely.
Detect debugger statements (breakpoint(), pdb.set_trace(), ipdb.set_trace(), pudb.set_trace()) in Python files.
Use # debugger-detection: disable to ignore a specific line.
Sort JSON file keys alphabetically (recursive). Modifies files in-place and returns 1 if any file was changed.
Sort Python dependency files alphabetically. Modifies files in-place and returns 1 if any file was changed.
Supported files
| File pattern | What is sorted |
|---|---|
requirements*.txt |
All package lines (case-insensitive); comments and blank lines are moved to the top |
setup.cfg |
install_requires entries; extras keys in [options.extras_require] and their dependencies |
Example — setup.cfg
# Before
[options.extras_require]
yaml =
PyYAML==6.0.1
dead_code =
vulture>=2.0
aardvark>=1.0
# After
[options.extras_require]
dead_code =
aardvark>=1.0
vulture>=2.0
yaml =
PyYAML==6.0.1Detect potential secrets committed in .env files (passwords, tokens, API keys, etc.).
Placeholder values (<value>, ${VAR}, changeme, etc.) are ignored.
Detect direct use of the root logging module (e.g. logging.info(...)) instead of a named logger.
Use # logger-detection: disable to ignore a specific line.
Detect explicit unreachable code — statements after return, raise, break, or continue — using Python's ast module. Works with Python 3.12, 3.13 and 3.14.
Language: Python only (AST-based, no external dependency).
No configuration needed. Returns 1 if any unreachable statement is found.
def f():
return 1
x = 2 # ← detected: unreachable code after returnUse # unreachable-code: disable on the unreachable line to suppress a specific violation.
Detect implicit dead/unused code (unused functions, variables, imports, classes) using vulture.
Language: Python only. Not language-agnostic — vulture analyses Python ASTs.
Dynamic-import awareness (default-on): names reached only through importlib.import_module, __import__, getattr/setattr/hasattr, globals()/vars()/locals() subscripts or entry_points are collected from the AST and filtered out of vulture's report, removing those false positives automatically. Disable with --no-dynamic-imports. A whitelist file still covers names this heuristic cannot reach (e.g. __all__, decorators).
Test-only detection (--detect-test-only): flags production symbols that are referenced only from test files (test helpers/backdoors leaking into prod). These findings fail the hook by default; --warn-only downgrades them to a report (genuinely dead code still fails). Detection scans both production and test files in one invocation, so set pass_filenames: false (or run over the whole tree at the manual stage) when enabling it. Test files are recognised by --test-pattern (default tests/ test_*.py *_test.py conftest.py).
This hook runs in the manual stage by default to avoid false positives on entry points.
- id: python-dead-code
stages: [manual]
args:
- '--min-confidence=80'
# Exclude paths matching these glob patterns
- '--exclude=tests/ migrations/ **/conftest.py'
# Whitelist file listing names used dynamically (suppresses false positives)
- '--whitelist=whitelist.py'
# Opt-in: also flag prod symbols used only by tests
# - '--detect-test-only'
additional_dependencies: [vulture>=2.0]| Option | Default | Description |
|---|---|---|
--min-confidence |
80 |
Minimum confidence % to report (0–100) |
--exclude |
— | Space-separated glob patterns of paths to skip |
--whitelist |
— | Vulture whitelist .py files to suppress false positives |
--no-dynamic-imports |
off | Disable dynamic-import awareness (report names reached via importlib/getattr/…) |
--detect-test-only |
off | Also flag production symbols referenced only from test files |
--test-pattern |
tests/ test_*.py *_test.py conftest.py |
File patterns marking test files |
--warn-only |
off | Report test-only findings without failing the hook |
Run manually:
pre-commit run python-dead-code --hook-stage manual --all-filesDetect explicit unreachable code — statements after return, throw, break, or continue — in TypeScript (.ts), TSX (.tsx), JavaScript (.js) and JSX (.jsx) files, using a real AST via tree-sitter.
Language: TypeScript / TSX / JavaScript / JSX (React). The TSX parser is used for .tsx/.jsx to handle JSX syntax.
Dynamic code generation: tree-sitter performs syntactic analysis only, so dynamically evaluated code is unaffected.
Use // unreachable-code: disable on the unreachable line to suppress a specific violation.
function f() {
throw new Error('not implemented');
return 42; // ← detected: unreachable code after throw
}- id: ts-unreachable-code
additional_dependencies:
- tree-sitter>=0.23
- tree-sitter-typescript>=0.23Detect duplicate CSS property declarations within the same rule block, and duplicate #id selectors across the stylesheet. Duplicate properties hide the first declaration; duplicate IDs violate CSS specificity best-practices and make maintenance harder.
Language: CSS (.css). SCSS/Less are not currently supported.
Use /* css-duplicate-property: disable */ on the duplicate property line to suppress a specific violation.
Use /* css-duplicate-id: disable */ on the duplicate ID selector line to suppress a specific violation.
.foo {
color: red; /* ← first declaration */
color: blue; /* ← detected: duplicate property "color" (first at line 2) */
}
#hero { color: red; } /* ← first occurrence */
#hero { color: blue; } /* ← detected: duplicate ID selector "hero" (first at line 6) */Nested rule blocks are tracked independently (each {…} scope has its own seen-properties map).
Detect CSS custom properties (--var-name) that are declared but never used anywhere in the file via var(--var-name).
Language: CSS (.css). SCSS/Less are not currently supported.
Use /* css-unused-variable: disable */ on the declaration line to suppress a specific violation.
:root {
--color-primary: #007bff; /* ← used below */
--color-ghost: #aaa; /* ← detected: declared but never used */
}
.btn { color: var(--color-primary); }Detect console.error() calls in JavaScript/TypeScript/React files. Complements console-log-detection, console-debug-detection and console-table-detection.
Language: .js, .ts, .jsx, .tsx.
Use // console-error-detection: disable on the line to suppress a specific violation.
Detect bare except: clauses (without specifying an exception type) in Python files. Bare excepts catch all exceptions including SystemExit, KeyboardInterrupt and GeneratorExit, which can mask serious errors.
Use # no-bare-except: disable to suppress a specific line.
# WRONG
try:
do_something()
except: # ← detected
pass
# OK
try:
do_something()
except ValueError:
passDetect print() calls in Django migration files (migrations/*.py). Print statements committed in migrations will be shown every time migrations run.
Use # no-print-in-migration: disable to suppress a specific line.
Detect hardcoded secrets directly assigned in Python files. Catches patterns like SECRET_KEY = "...", PASSWORD = "...", API_KEY = "...", TOKEN = "..." and PRIVATE_KEY = "...".
Values referencing environment variables (os.environ, os.getenv, env(), config(), etc.) are not flagged.
Use # django-hardcoded-secret: disable to suppress a specific line.
# WRONG — detected
SECRET_KEY = 'django-insecure-abc123'
# OK — not flagged
SECRET_KEY = os.environ['SECRET_KEY']
SECRET_KEY = env('SECRET_KEY')Detect FROM image:latest instructions in Dockerfiles. Using :latest makes builds non-reproducible.
FROM scratch is always allowed.
Use # dockerfile-no-latest: disable to suppress a specific line.
# WRONG
FROM python:latest
# OK
FROM python:3.12-slimCheck that .env and .env.example files contain the same set of keys. Ensures that new environment variables added to .env are documented in .env.example (with empty or placeholder values), and vice-versa.
- id: env-example-sync
args:
- '--env-file=.env' # default
- '--example-file=.env.example' # default| Option | Default | Description |
|---|---|---|
--env-file |
.env |
Path to the private .env file |
--example-file |
.env.example |
Path to the public example file |
Detect console.warn() calls in JavaScript/TypeScript/React files (.js, .gs, .ts, .jsx, .tsx).
Use // no-console-warn: disable to suppress a specific line.
Detect direct DOM manipulation (document.getElementById, document.querySelector, etc.) in React files (.jsx, .tsx).
Direct DOM access bypasses React's virtual DOM — use refs or state instead.
Use // react-direct-dom: disable to suppress a specific line.
Detect deep relative parent imports (../../) in TypeScript/JavaScript files.
Use path aliases (@/) instead. Two or more ../ levels trigger a violation.
Use // import-no-relative-parent: disable to suppress a specific line.
Detect DEBUG = True in Django settings files (settings*.py).
Use # no-debug-in-settings: disable to suppress a specific line.
Detect raw SQL queries (.raw() and cursor.execute()) in Django Python files.
These patterns bypass the ORM and risk SQL injection.
Use # django-no-raw-sql: disable to suppress a specific line.
Detect synchronous blocking calls inside async def functions:
| Blocked call | Suggested async replacement |
|---|---|
time.sleep() |
asyncio.sleep() |
requests.get/post/put/delete/patch() |
httpx.AsyncClient or aiohttp |
subprocess.run/call/check_output() |
asyncio.create_subprocess_exec |
Use # no-sync-in-async: disable to suppress a specific line.
Detect FastAPI route handlers (@app.get, @app.post, etc.) that do not declare a response_model= parameter.
This prevents automatic response schema generation and validation.
Use # fastapi-missing-response-model: disable on the decorator line to suppress.
- id: fastapi-missing-response-model
files: '(routers?|views?|api)/.*\.py$'Validate JavaScript and Google Apps Script (.js, .gs) file syntax using node --check.
Requires Node.js to be installed in PATH. If Node.js is not available, the hook exits 0 (safe skip).
- id: js-syntax-check
files: '\.(js|gs)$'Detect hardcoded API keys, tokens and passwords in TypeScript/JavaScript files (.js, .ts, .jsx, .tsx).
Catches patterns like const API_KEY = "...", const token = "sk_live_...", AWS key prefixes (AKIA...), GitHub tokens (ghp_...), and Stripe keys (sk_live_...).
Values referencing environment variables (process.env, import.meta.env) are not flagged. Short values (< 8 chars) are ignored.
Use // ts-hardcoded-secret: disable on the line to suppress a specific violation.
// WRONG — detected
const API_KEY = 'my-secret-api-key-value-hardcoded';
// OK — not flagged
const API_KEY = process.env.API_KEY;Run helm lint --strict on all charts found in charts/<namespace>/<service>/ (depth 2 under charts/).
Requires helm to be installed in PATH.
Expected directory structure:
charts/
<namespace>/
<service>/ ← helm lint --strict is run here
Chart.yaml
values.yaml
- id: helm-lint
files: ^charts/
pass_filenames: falseEnforce the chrysa archetype-tiered Makefile contract (shared-standards
EXECUTION_STANDARD.md §1). Each Makefile must declare its tier on a marker line:
# makefile-tier: lib # one of: lib | python-app | fullstack | infraThe hook then fails on: a missing/invalid tier marker, a required target absent for the
tier, a forbidden target name (fmt/type-check/tests), a legacy docker-compose <cmd>
invocation, a glued docker compose typo, a missing help target or .PHONY line, or a
lint/test/format rule that references a directory which does not exist. Targets missing from
.PHONY and the absence of ## self-documenting comments are reported as warnings.
- id: makefile-check
files: (^|/)Makefile$Generate or update a CHANGELOG.md using git-cliff on every commit to main.
Requires git-cliff to be installed in PATH.
This hook runs in the manual stage by default — trigger it via CI or explicitly:
pre-commit run generate-changelog --hook-stage manual- id: generate-changelog
stages: [manual]Capture screenshots of the UI screens affected by a commit and publish them to the README and/or a Notion page. Two composable hooks linked by a manifest.
Note on Notion: the Notion publisher appends image blocks on each run, so
committing repeatedly with notion.enabled will accumulate duplicate images on
the page. The README target is idempotent; the Notion target is not (idempotent
Notion publishing is a planned follow-up). Enable Notion when you want an
append-only log of screenshots, or publish to Notion deliberately rather than on
every commit.
- repo: https://github.com/chrysa/pre-commit-tools
rev: v0.0.34
hooks:
- id: screenshot-capture
- id: screenshot-publishAdd a .screenshot-sync.yaml to the consuming repo (absent → both hooks are
no-ops):
strategy: glob-url # glob-url | storybook | fixed-routes
base_url: http://localhost:5173
output_dir: docs/screenshots
viewport: { width: 1280, height: 800 }
strict: false # true = blocking on failure; false = warn + skip
routes:
- { match: "src/pages/Login.*", url: /login, name: login }
publish:
readme: { enabled: true, file: README.md, marker: screenshots }
notion: { enabled: false, page_id: "", image_base_url: "" } # NOTION_API_KEY via envscreenshot-capture renders the routes whose source files changed (Playwright),
writes PNGs + a manifest under output_dir, and stages them. screenshot-publish
reads the manifest and updates the README section between
<!-- screenshots:start --> / <!-- screenshots:end --> and/or appends image
blocks to the Notion page. By default neither hook blocks the commit; set
strict: true to make environment/network failures blocking. Playwright browser
binaries are not auto-installed — run playwright install chromium once per repo.
| Hook id | Purpose | Disable escape |
|---|---|---|
python-no-external-tool-config |
Detect forbidden standalone tool config files (ruff.toml, mypy.ini, pytest.ini, .coveragerc) — configuration must live in pyproject.toml |
— |
python-no-setup-files |
Detect setup.py / setup.cfg packaging files — use pyproject.toml instead |
— |
python-cors-allow-all |
Detect wildcard CORS (allow_origins=['*'] / CORS_ALLOW_ALL_ORIGINS=True) |
# cors-allow-all: disable |
python-no-create-all |
Detect Base.metadata.create_all() / engine.create_all() calls outside migrations/alembic |
# no-create-all: disable |
python-os-environ-direct |
Detect direct os.environ[...] / os.getenv(...) access outside settings/config modules |
# os-environ-direct: disable |
python-file-too-long |
Detect Python files exceeding 500 lines (split the module) | — |
python-function-too-long |
Detect Python functions exceeding 50 lines (extract helpers) | — |
docker-compose-missing-restart |
Detect Docker Compose services missing restart: unless-stopped |
no inline escape; suppress via repo exclude: or SKIP=docker-compose-missing-restart |
react-useeffect-fetch |
Detect fetch/axios calls inside a React useEffect callback (use a query library instead) |
// react-useeffect-fetch: disable |
