Skip to content
Ultracite
Esc
↑↓navigate↵open⌘Jpreview
On this page

Git Hooks

Run Ultracite automatically before each commit with Husky, Lefthook, lint-staged, or pre-commit so staged files stay clean and consistent.

Ultracite integrates with popular Git hook tools to automatically format and lint your code before every commit. This ensures all committed code follows your project’s standards without manual intervention.

Setup

During initialization, you can select which Git hook tool to use:

npx ultracite init --integrations husky lint-staged

Or select them interactively when prompted. In a project that’s already set up, this keeps its current linter and your changes to its configs.

Supported Tools

Every hook runs the ultracite installed in your project rather than downloading the latest release, so commits use the version your lockfile pins. The command depends on your package manager: npx ultracite fix (npm), yarn ultracite fix, pnpm exec ultracite fix, bunx ultracite fix, deno run -A npm:ultracite fix, nub exec ultracite fix, or aube exec ultracite fix. The examples below use npm.

Re-running ultracite init updates a hook it wrote earlier (for example, one that used pnpm dlx or yarn dlx) instead of adding a second one, and leaves commands you wrote yourself alone.

Husky

Husky is a popular tool for managing Git hooks. Ultracite adds a marker-wrapped section to your .husky/pre-commit file. When files are staged, it runs ultracite fix on the whole project, re-stages the files that were already staged, and fails the commit if any issues couldn’t be auto-fixed:

# ultracite
#!/bin/sh
# Check if there are any staged files
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR)
if [ -z "$STAGED_FILES" ]; then
  echo "No staged files to format"
else
  # Run formatter, capturing the exit code so we can still re-stage and report
  FORMAT_EXIT_CODE=0
  npx ultracite fix || FORMAT_EXIT_CODE=$?

  # Re-stage files that were already staged
  echo "$STAGED_FILES" | while IFS= read -r file; do
    if [ -f "$file" ]; then
      git add -- "$file"
    fi
  done

  if [ $FORMAT_EXIT_CODE -ne 0 ]; then
    echo "Ultracite found issues that could not be auto-fixed."
    exit $FORMAT_EXIT_CODE
  fi

  echo "✨ Files formatted by Ultracite"
fi
# ultracite end

Anything you’ve added to the hook outside the # ultracite markers is preserved when Ultracite updates it, and still runs when nothing is staged. If you also select lint-staged, the section runs lint-staged (npx lint-staged) instead of the script above.

Ultracite also adds husky to your prepare script so hooks are installed on npm install. An existing prepare script is kept, with && husky appended.

Lefthook

Lefthook is a fast Git hooks manager written in Go. If your project has no Lefthook config, Ultracite creates a lefthook.yml:

pre-commit:
  jobs:
    - run: npx ultracite fix
      glob:
        - "*.js"
        - "*.jsx"
        - "*.ts"
        - "*.tsx"
        - "*.json"
        - "*.jsonc"
        - "*.css"
      stage_fixed: true

With Lefthook’s default glob matcher, *.js matches files in every directory, including the project root. If your config sets glob_matcher: doublestar, Ultracite writes **/*.js-style globs instead.

If you already have a Lefthook config, Ultracite edits the one Lefthook actually loads (lefthook.yml, .lefthook.yml, .config/lefthook.yml, or their .yaml variants) and adds the job first in pre-commit, keeping your comments and formatting. JSON and TOML configs aren’t edited: Ultracite prints the job to add instead. Ultracite also appends lefthook install to your prepare script, keeping any existing command.

lint-staged

lint-staged runs linters only on staged files. If your project has no lint-staged config, Ultracite creates a .lintstagedrc.json:

{
  "*.{js,jsx,ts,tsx,json,jsonc,css,scss,md,mdx}": ["npx ultracite fix"]
}

If you already have one, Ultracite adds its task to the config lint-staged actually uses: a dedicated config file (.lintstagedrc in any format, including .ts, .mts, and .cts, or lint-staged.config.*) wins over a "lint-staged" key in package.json, which wins over one in package.yaml. YAML configs keep their comments, ES module and TypeScript configs are edited in place, and CommonJS configs are rewritten from their loaded values. If a config can’t be merged safely (for example, one that exports a function or wraps its object in defineConfig()), Ultracite leaves it untouched and prints the command to add, rather than creating a second config that would shadow yours.

lint-staged needs a Git hook to trigger it, so it’s typically used alongside Husky or Lefthook.

pre-commit

pre-commit is a Python-based framework for managing Git hooks. Ultracite creates a .pre-commit-config.yaml, or adds a local repo entry to the top of your existing one, keeping its comments and indentation style:

repos:
  - repo: local
    hooks:
      - id: ultracite
        name: ultracite
        entry: npx ultracite fix
        language: system
        types_or: [javascript, jsx, ts, tsx, json, css]
        pass_filenames: false

After setup, run pre-commit install to activate the hooks.

How It Works

  1. You make changes and stage files with git add
  2. You run git commit
  3. The pre-commit hook runs ultracite fix: on the files you staged with lint-staged, or on the whole project with Husky, Lefthook, and pre-commit
  4. Code is formatted, auto-fixable issues are resolved, and the fixed files are re-staged (Husky, Lefthook, and lint-staged)
  5. The commit proceeds, or stops if issues remain that couldn’t be auto-fixed

Benefits

  • Consistency: All committed code follows the same standards
  • Automation: No need to remember to format code manually
  • Clean History: Formatting issues never enter your repository
  • Team Collaboration: Everyone follows the same rules automatically

Bypassing Hooks

In rare cases where you need to skip the pre-commit hook:

git commit --no-verify

Use this sparingly, as it bypasses the automated formatting.

Was this page helpful?