Run your existing pre-commit hooks without installing Python.
Same .pre-commit-config.yaml, same hook repositories, same commands — as a
single binary. An independent Go reimplementation of
pre-commit, not a fork and not
affiliated with it.
Measured against Python pre-commit 4.7.0 on every pull request that changes code: 554 of 554 differential checks pass (how that is measured).
Requires git 2.31 or newer, as upstream pre-commit does: run --all-files
lists files with git ls-files --deduplicate, which 2.31 added.
Probably not, and that is worth two minutes of your time: should you use this instead of Python pre-commit?
The short version — if you already have Python and pre-commit working, keep them. The case for this tool is a repo whose only reason to install a Python toolchain is to run its hooks.
- Drop-in — the same CLI, config format, hook repositories and cache location as the Python tool
- One binary — no interpreter, no virtualenv, no
pip - All hook types: pre-commit, pre-merge-commit, pre-push, commit-msg, post-checkout, post-commit, post-merge, post-rewrite, prepare-commit-msg, pre-rebase
- All 21 of upstream's languages implemented — though not equally proven; the parity grading says which are well-trodden and which you would be the first to try
- File type identification by extension, filename, and shebang
- Parallel hook execution with xargs-style batching
It installs a binary called
pre-commit, and that is deliberate.pre-commit installwrites a git hook that invokespre-commitby name, so a drop-in has to answer to that name. On Homebrew it shadowshomebrew/core/pre-commit, andbrewwill say so. If you keep both tools, know which one you are getting:pre-commit --versionprints a(build …)suffix here and nothing of the sort upstream. The cache directory is shared with the Python tool too — details, including whatcleanremoves, are in docs/parity.md.
brew install blairham/tap/pre-commitDownload the latest release from the Releases page. Archives are available for Linux, macOS, and Windows (amd64/arm64).
On Windows,
python,node,golang,rubyandrusthooks work from v4.6.10, and CI runs one of each there on every pull request. The other languages that install an environment have not been run on Windows. See platform support.
# Example: macOS arm64
curl -Lo pre-commit.tar.gz https://github.com/blairham/go-pre-commit/releases/latest/download/pre-commit_Darwin_arm64.tar.gz
tar xzf pre-commit.tar.gz
sudo mv pre-commit /usr/local/bin/go install github.com/blairham/go-pre-commit/v4@latestNote: go install names the binary go-pre-commit (Go strips the /v4 suffix). Rename it to pre-commit on your PATH — the git hooks it installs invoke it by that name.
git clone https://github.com/blairham/go-pre-commit.git
cd go-pre-commit
make build
# Binary is at build/pre-commitRun your hooks in CI without setting up Python — this repo doubles as a composite action that installs the release binary and runs pre-commit run. It is a drop-in replacement for pre-commit/action:
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: blairham/go-pre-commit@v4Inputs:
- uses: blairham/go-pre-commit@v4
with:
version: latest # release to install, e.g. "v4.6.6"
extra_args: --all-files # passed to `pre-commit run`
cache: 'true' # cache hook environments between runs
install-only: 'false' # set 'true' to install the binary but skip `pre-commit run`Hooks run as pre-commit run --show-diff-on-failure --color=always <extra_args>, and hook environments (~/.cache/pre-commit) are cached keyed on .pre-commit-config.yaml, so warm runs skip environment setup entirely. Hooks that need extra tools on PATH (e.g. language: system hooks) still require you to install those tools in earlier steps. With install-only: 'true' the action puts the binary on PATH (and still caches) but skips the run step, for workflows that drive pre-commit themselves.
# Install git hooks into the current repo
pre-commit install
# Run all hooks against staged files
pre-commit run
# Run all hooks against all files
pre-commit run --all-files
# Run a specific hook
pre-commit run <hook-id>
# Auto-update hook repos to latest versions
pre-commit autoupdate
# Try a repo without adding it to config
pre-commit try-repo <repo> [hook-id]
# Generate sample config
pre-commit sample-config
# Validate config
pre-commit validate-config .pre-commit-config.yaml
# Clean cached repos
pre-commit clean
# Garbage collect unused repos
pre-commit gcCreate a .pre-commit-config.yaml in your repository root:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- repo: local
hooks:
- id: my-local-hook
name: My Local Hook
entry: ./scripts/check.sh
language: script
files: '\.go$'| Command | Description |
|---|---|
run |
Run hooks against staged files (or specified files) |
install |
Install the git hook script |
uninstall |
Uninstall the git hook script |
install-hooks |
Install all hook environments |
autoupdate |
Auto-update hook repo revisions |
clean |
Clean out cached repos |
gc |
Garbage collect unused repos |
sample-config |
Print a sample configuration |
validate-config |
Validate a config file |
validate-manifest |
Validate a manifest file |
try-repo |
Try hooks from a repo |
init-templatedir |
Install hook into a template directory |
migrate-config |
Migrate config from old format |
Measured against Python pre-commit 4.6.2 on a scratch repository with a fixed config (macOS, Apple M5 Max, warm hook environments, separate caches per tool, median of 15 runs alternating which tool goes first):
| Case | this tool | Python | |
|---|---|---|---|
startup (run, nothing staged) |
0.034s | 0.172s | 5.1x |
trailing-whitespace |
0.059s | 0.205s | 3.5x |
end-of-file-fixer |
0.063s | 0.226s | 3.6x |
check-yaml |
0.075s | 0.208s | 2.8x |
check-added-large-files |
0.072s | 0.225s | 3.1x |
check-merge-conflict |
0.060s | 0.198s | 3.3x |
go vet (system hook), warm build cache |
0.101s | 0.185s | 1.8x |
go vet (system hook), cold build cache |
1.255s | 1.291s | 1.0x |
Read the last two rows first. The difference is a roughly fixed 0.15–0.2s
per run — Python's interpreter startup and imports. On a hook that finishes in
a tenth of a second that is most of the time, so the ratio looks large; on a
tool that does real work it disappears into the noise. If your slow hook is
golangci-lint or eslint, the tool doing the work is the same tool and this
will not make it faster.
The machine was not idle (load average 12–16 from other work), so treat the absolute numbers as indicative; the ratios held across three separate runs. Run it yourself — it builds the scratch repo, and refuses a Python pre-commit that is not the version the parity suite targets:
PRE_COMMIT_PY=/path/to/python-pre-commit bash .github/bench.shmake build # Build binary to build/pre-commit
make test # Run tests (with -race)
make test-cover # Tests + HTML coverage report
make fmt # Format code (gofumpt)
make vet # Run go vet
make tidy # go mod tidy
make check # vet + test + buildThere is no lint target: golangci-lint runs as a pre-commit hook (pre-commit install) and in CI.
Releases are automated with GoReleaser via the shared
release workflow in blairham/.github. Move
CHANGELOG.md's [Unreleased] section under ## [X.Y.Z] - <date> (the
release notes are that section, and a release without one fails), then tag:
git tag -s v4.6.17
git push origin v4.6.17The release workflow also moves the v4 alias tag to the new release, which is
what uses: blairham/go-pre-commit@v4 resolves to. Its tag trigger deliberately
matches full versions only, so moving the alias does not start a second release.
CI builds, signs, and notarizes cross-platform binaries, publishes a GitHub release, and updates the Homebrew formula in blairham/homebrew-tap automatically. Versions track upstream parity: v4.7.x means feature parity with Python pre-commit 4.7.
| Should you use this? | The case for staying on Python pre-commit, and the narrow case against it |
| Parity | What is measured, what is not, and which languages are actually proven |
| Stability | What the version number means, and what is frozen |
| Contributing | How to report a divergence — the most useful thing you can send |
| Security | What is in scope, and how to report privately |
| Launch playbook | What is claimed, what is not, and the weak points stated up front |
This is an independent reimplementation, not a fork and not an official project. It is not affiliated with, endorsed by, or supported by the pre-commit project or its maintainers.
The behavior it copies — the CLI, the config and manifest schemas, the cache
layout, exit codes and output formatting — is the design of two MIT-licensed
projects, and the file-type tag tables in internal/identify follow upstream
identify's data:
- pre-commit — © 2014 pre-commit dev team: Anthony Sottile, Ken Struys
- identify — © 2017 Chris Kuehl, Anthony Sottile
Their MIT license is reproduced in NOTICE. Bugs you find here are this project's bugs, not theirs — please report them here rather than upstream.
The most valuable contribution is a divergence report: upstream does X, this does Y, with both commands and both outputs. That is the one thing this project cannot generate for itself — see CONTRIBUTING.md.
Divergence is a bug here, never a feature. If you want behavior Python pre-commit does not have, upstream is the place to ask; if they ship it, it arrives here as parity work.
Apache-2.0, with third-party notices in NOTICE.