Sharon Ben-Moshe is the founder of sqlfmt, a browser-based SQL formatter and validator with misspelled-keyword suggestions across PostgreSQL, MySQL, SQLite, SQL Server, and BigQuery.
A pre-commit hook that formats and validates SQL catches bad queries before they ever reach a pull request — earlier and cheaper than CI. Here's a working setup using the sqlfmt API.
Quick answer
A Git pre-commit hook can format and validate every staged .sql file automatically by sending each file's contents to a formatting API and blocking the commit on syntax errors — catching mistakes seconds after they're written instead of minutes later in CI.
Yes — the sqlfmt REST API (POST /api/v1/analyze) is a Pro feature, since it's the same unlimited, programmatic access that powers CI and editor integrations.
Will this slow down every commit?
For a typical commit touching a few files, no — each request is a small JSON round trip. If a single commit touches dozens of SQL files at once, consider running the check only above a size threshold or moving it to CI instead.
What's the difference between this and running sqlfmt in CI?
A pre-commit hook catches problems before the commit even exists, on the developer's machine. CI catches whatever slips through — a hook skipped with --no-verify, or a change made without the hook installed. Most teams run both.
Should typo warnings block a commit?
Usually not. Typo warnings are suggestions, not proof of a real error — printing them as advisory output while only blocking on formatError and syntaxErrors keeps the hook useful without becoming annoying enough that developers bypass it.
Can the hook fix the SQL instead of just flagging it?
Yes — the API response includes the fully formatted SQL in formatted, so the hook can overwrite the file and re-stage it automatically, the same pattern tools like Prettier use with lint-staged.
Inconsistent naming — orders vs order, customer_id vs customerId, is_active vs active_flag — costs a team more review time than almost any formatting choice. Here's a standard worth adopting before the schema outgrows a cheap fix.
SQL linting in CI catches problems before a merge, but by then a developer has already pushed, waited for a pipeline, and context-switched away. A pre-commit hook catches the same problems locally, before the commit even exists.
Key takeaways
A pre-commit hook should format automatically and only block the commit on real syntax errors — a noisy hook gets bypassed with --no-verify.
sqlfmt's REST API (POST /api/v1/analyze) is a Pro feature and needs an API key that stays on the developer's machine.
Send each staged .sql file's contents as the sql field; formatError and syntaxErrors in the response are what should actually fail the commit.
Husky is the most common way to wire a Node script into Git's pre-commit stage without hand-managing .git/hooks.
What the hook needs to do
Find the staged .sql files.
Send each file's contents to the API.
Fail the commit if formatError or syntaxErrors comes back non-empty.
Optionally, overwrite the file with the formatted output and re-stage it.
Getting an API key
API access is a Pro feature — generate a key from your account page once you've upgraded, or read the full API reference first.
The pre-commit script
.husky/pre-commit
#!/usr/bin/env shnode scripts/check-sql.mjs
scripts/check-sql.mjs
import { execSync } from "node:child_process";import { readFileSync } from "node:fs";const apiKey = process.env.SQLFMT_API_KEY;const staged = execSync("git diff --cached --name-only --diff-filter=ACM") .toString() .split("\n") .filter((f) => f.endsWith(".sql"));let hasErrors = false;for (const file of staged) { const sql = readFileSync(file, "utf8"); const res = await fetch("https://www.sqlfmt.app/api/v1/analyze", { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ sql, options: { dialect: "postgresql" } }), }); const result = await res.json(); if (result.formatError || result.syntaxErrors?.length) { console.error(`✗ ${file}`); for (const err of result.syntaxErrors ?? []) { console.error(` line ${err.line}:${err.column} — ${err.message}`); } hasErrors = true; }}if (hasErrors) { console.error("\nFix the SQL above, or run `git commit --no-verify` to skip this check."); process.exit(1);}
Making it fail fast, not annoying
The API allows 60 requests per minute per key, which a normal commit touching a handful of files won't come close to — but a commit that adds dozens of SQL files at once (a bulk migration import, for example) could. For those, it's worth short-circuiting after the first batch or moving the check to CI instead. It's also worth treating typoWarnings as advisory output rather than a blocker — a flagged keyword is often an intentional custom function name, and failing the commit on it trains developers to reach for --no-verify.
Extending it to auto-format instead of just check
Since the response includes the fully formatted SQL in result.formatted, the hook can overwrite the file and re-stage it automatically — the same pattern tools like Prettier use with lint-staged, turning the hook from a gate into an auto-formatter.
Auto-format and re-stage
import { writeFileSync } from "node:fs";import { execSync } from "node:child_process";writeFileSync(file, result.formatted);execSync(`git add ${file}`);