Set up a clang-format pre-commit hook for C and C++¶
Formatting comments are the cheapest part of a code review to get rid of. A pre-commit hook runs clang-format on the files in each commit, so badly formatted code never reaches a pull request, and CI stops failing for a missing space.
With the pre-commit framework, nobody on the team has to install LLVM, and everyone gets the same clang-format version.
Step 1: Have a .clang-format in the repository¶
If the project already has one, keep it. If not, start from a built-in style and change as little as possible:
BasedOnStyle: Google
ColumnLimit: 100
The built-in styles are LLVM, Google, Chromium, Mozilla, WebKit, Microsoft and GNU.
clang-format --style=Google --dump-config prints every option of a style, and the
clang-format configurator shows the effect of each
option on sample code.
Step 2: Install pre-commit and add the hook¶
pre-commit is a Python tool. Install it once per machine:
pip install pre-commit # or: pipx install pre-commit, brew install pre-commit
Add the hook to the repository:
repos:
- repo: https://github.com/cpp-linter/cpp-linter-hooks
rev: v1.6.0
hooks:
- id: clang-format
args: [--style=file, --version=21]
--style=filemakes clang-format read the.clang-formatfile.--version=21selects the clang-format version. The hook downloads it as a Python wheel, so a system LLVM is neither needed nor used.revis the version of the hook, not of clang-format.21means the newest 21.x release (21.1.8 at the time of writing); write--version=21.1.8to pin the exact release.
Then enable it in your clone:
pre-commit install
Every contributor runs pre-commit install once after cloning. Put that line in
CONTRIBUTING.md.
Step 3: Commit something¶
Stage a badly formatted file and commit:
$ git commit -m "add main"
clang-format.............................................................Failed
- hook id: clang-format
- files were modified by this hook
"Failed" here means the hook reformatted the file in your working tree and stopped the commit, so you can look at what changed:
$ git status --short
AM src/main.c
$ git diff # the formatting changes
$ git add src/main.c
$ git commit -m "add main"
clang-format.............................................................Passed
Two things to know about the scope:
- Only the files staged for the commit are checked, which keeps the hook fast.
- Those files are formatted completely, not just the lines you changed. Step 5 covers what that does to old code.
The first run takes a little longer, because pre-commit builds an environment for the hook and downloads clang-format. Both are cached afterwards.
Step 4: Format only your own code¶
By default the hook runs on C and C++ sources and headers. Vendored code should be left alone, because reformatting it makes every later update a merge conflict. There are two ways to exclude it.
In the hook configuration:
- id: clang-format
args: [--style=file, --version=21]
exclude: ^(third_party|external)/
Or with a .clang-format inside the vendored directory, which also covers editors and anyone
running clang-format by hand:
DisableFormat: true
SortIncludes: Never
clang-format also handles CUDA, Protobuf and a few other languages. The hook skips them unless you widen its file types:
- id: clang-format
args: [--style=file, --version=21]
types_or: [c++, c, cuda, proto]
Step 5: Decide what to do with existing code¶
Because the hook formats whole files, the first person to touch an old file gets a diff where three lines of logic are buried in three hundred lines of whitespace changes. Pick one of these before turning the hook on for everyone.
Reformat once. This is the better option when you can afford it.
pre-commit run clang-format --all-files
git commit -am "style: apply clang-format to the whole tree"
git rev-parse HEAD >> .git-blame-ignore-revs
git add .git-blame-ignore-revs
git commit -m "chore: ignore the reformat commit in git blame"
.git-blame-ignore-revs keeps git blame useful: lines are attributed to the commit that last
changed their content, not to the reformat. GitHub's blame view reads the file automatically.
Locally, each clone needs:
git config blame.ignoreRevsFile .git-blame-ignore-revs
Open branches will conflict with the reformat commit, so do it when few pull requests are open,
and announce it. After resolving the conflicts, a branch author runs
pre-commit run clang-format --files <their files> to bring the branch in line.
Format as you go. Leave the hook on and let files get formatted when somebody touches them. Ask contributors to put the formatting change in a separate commit from the logic change, so reviewers can skip it. The code base converges more slowly and the noisy diffs last longer.
To format only the changed lines, use git clang-format, which ships with LLVM. It does not run
through the pre-commit framework.
Step 6: Enforce it in CI¶
A local hook can be skipped with git commit --no-verify, and new contributors may not have run
pre-commit install. Run the same configuration in CI:
name: pre-commit
on:
pull_request:
push:
branches: [main]
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: pipx run pre-commit run --all-files --show-diff-on-failure
When a file is not formatted, the job fails and prints the required change:
clang-format.............................................................Failed
- hook id: clang-format
- files were modified by this hook
pre-commit hook(s) made changes.
If you are seeing this message in CI, reproduce locally with: `pre-commit run --all-files`.
To run `pre-commit` as part of git workflow, use `pre-commit install`.
All changes made by hooks:
diff --git a/src/add.c b/src/add.c
index b9e228e..1f76448 100644
--- a/src/add.c
+++ b/src/add.c
@@ -1 +1 @@
-int add(int a,int b){return a+b;}
+int add(int a, int b) { return a + b; }
If you would rather have the fix offered in the pull request, use
cpp-linter-action with format-review: true
and the same version. It posts the formatting differences as review suggestions that can be
committed from the browser.
Moving to cpp-linter has a complete workflow.
Troubleshooting¶
| Symptom | Cause and fix |
|---|---|
| The hook "fails" on every commit that needs formatting | It fixed the files and stopped the commit. git add them and commit again. |
| Formatting differs between two machines | Someone formats with another clang-format version, for example from an editor plugin or a system package. Point the editor at the same version, for example pip install clang-format==21.1.8. See One clang-format version everywhere. |
Could not find any stable versions of clang-format on PyPI |
The hook looks the version up on pypi.org each time it runs, so it needs network access. |
Unsupported clang-format version '...' |
There is no wheel for that version. The message lists the available ones. |
| Vendored or generated code gets reformatted | Exclude it; see step 4. |
| One block must keep its manual layout | Wrap it in // clang-format off and // clang-format on. |
| You need to commit without the hook once | SKIP=clang-format git commit ... skips this hook only; git commit --no-verify skips all hooks. CI still checks the result. |
Adding clang-tidy later¶
The same repository provides a clang-tidy hook:
- id: clang-tidy
args: [--checks=.clang-tidy, --version=21]
clang-tidy needs a compile_commands.json to find your headers, which the hook picks up from
build/ and a few other common directories. It is also much slower than clang-format, because it
parses every header a file includes. We suggest keeping clang-format in the hook and running
clang-tidy in CI.
Where to go next¶
- All hook options, including the clang-tidy ones, are in the
cpp-linter-hooks README. It also
compares the hook with
mirrors-clang-format, which is a good choice when clang-format is all you will ever need and you wantrevto be the clang-format version. - One clang-format version everywhere covers keeping the hook, CI, editors and Docker images on one LLVM version.