Using pre-commit for Linters/Formatters

During my tenure at Grab, I witnessed the project’s transformation from an iOS exclusive to a multi-language initiative. Initially, the project primarily comprised iOS code. Over time, we developed various scripts (Ruby, Python, Bash, etc.) to enhance project build time and facilitate CI/CD integration. However, the linter setup was not sufficient for such a multi-language project.

At a glance, we used SwiftLint, Rubocop, and Flake8 to lint Swift, Ruby, and Python code, respectively. Dealing with these tools ranged from installing them (which might vary between on local and CI environments) to executing them. As the project expanded, we introduced additional linting use cases, such as validating YAML, JSON, etc. Given the project’s scale, we aimed to lint against changed files while still having the option to lint against all files to identify issues in the project. Not only that, we desired to selectively run specific linters (this is often the case for CI). All those requirements come with a complex and high-maintenance integration.

Fortunately, pre-commit comes to our rescue. This framework is to manage multi-language pre-commit hooks. Setting up this tool is straightforward. Refer to this doc for more details. All we need to do is specify the hooks being used in a configuration file (see: example). Each hook corresponds to a linter or formatter, and there’s an extensive list of supported hooks, including both built-in and community-driven options.


Fig. pre-commit hooks execute when committing code.

The beauty of this approach is that it incorporates into our dev workflows. Linters and formatters automatically execute upon code commit and we don’t run into the “oops, I forgot to lint the code” situation. Leveraging git hooks to lint and format code is not a novel concept. While many recommend creating git hooks to handle such tasks, maintaining such hooks is not easy especially when scripting is not of our expertise, and when the requirements for project linters and formatters continue to expand. The pre-commit tool excels in handling this complexity, hence significantly reducing maintenance efforts.

What makes this tool stand out is its support for many languages, making it easy to set up linters and formatters. For instance, to integrate SwiftLint, we simply need to add this setup to the configuration file. The pre-commit tool then takes care of installing SwiftLint and caching it for subsequent executions.

  - repo:
    rev: 0.50.3
      - id: swiftlint

In contrast, the traditional method requires us to manually install SwiftLint on the machine. The installation may differ depending on the environment, for example, on CI (with docker) versus on local (Mac). With pre-commit, engineers no longer bear such maintenance costs.

For me, when working on a new initiative, the very first thing to do after initializing the git repo is integrating pre-commit to the project. Check out this sample pre-commit configuration for references.

Final word, as a pre-commit’s user for quite some time, I highly recommend this tool to seamlessly integrate the best industry-standard linters and formatters into your project.