pre-commit local hooks: Run Your Own Scripts
A repo: local block defines hooks inline with your own entry command instead of pulling them from a remote repo.
When no published hook does what you need, define one locally. You supply the entry command, the language, and the file filters.
What it does
With repo: local, each hook needs an id, a name, an entry (the command), and a language. pre-commit runs entry against the matched files. language: system runs the command as-is on the runner; language: python builds a venv from additional_dependencies.
Common usage
repos:
- repo: local
hooks:
- id: no-print
name: forbid print statements
entry: grep -nE "print\("
language: system
types: [python]
exclude: '^scripts/'Required keys
| Key | What it does |
|---|---|
| id | Unique identifier for the hook |
| name | Human-readable name shown in output |
| entry | The command pre-commit runs |
| language | system, python, node, script, etc. |
| files / types | Which files to pass to entry |
| pass_filenames | Whether matched filenames are appended to entry |
In CI
language: system assumes the tool is already installed on the runner; install it as a separate step or the hook fails with "Executable not found". For self-contained hooks, prefer language: python with additional_dependencies so pre-commit builds and caches the environment. Set pass_filenames: false for commands that should not receive a file list.
Common errors in CI
"[ERROR] Executable <cmd> not found" with language: system means entry points at a binary not installed on the runner. A local hook with no name or entry raises InvalidConfigError. If a grep-style hook exits 1 because it found a match, that correctly fails the run; invert the logic if you meant the opposite.