Claude CodeでSQLをsqlfluffで整える — dbtプロジェクトのlintをhookに入れる
Claude Codeが.sqlを編集した直後にsqlfluff fixを走らせるPostToolUse hookと、dbtプロジェクトでjinjaとdbtのtemplaterを使い分ける設定をまとめます。
sqlfluffをhookに載せると何が固定されるか
SQLFluffは、方言ごとの違いに対応するSQLのリンターです。sqlfluff lintで指摘を出し、sqlfluff fixで直せる分を書き換えます。これをClaude CodeのPostToolUse hookに載せると、Claudeが.sqlファイルを書くたびに、空白・インデント・大文字小文字といった整形規約が機械的にそろいます。
規約をCLAUDE.mdに文章で書いても、守られるかどうかはモデル任せです。hookなら、守られなかった分を編集の直後に直します。直せない指摘だけをClaudeへ返せば、人間のレビューに回る前に潰せます。
dbtプロジェクトでは、もう一つ判断が要ります。SQLFluffにはJinjaとdbtの2種類のtemplater(テンプレートを展開する仕組み)があり、hookに向くのは前者です。この記事では、動作確認、.sqlfluffの設定、hookスクリプト、templaterの使い分け、直らなかった指摘の返し方を順に扱います。言語を問わないLint自動修正の考え方はPostToolUse hookのLint自動修正の早見表にまとめてあります。
まず手元でlintの出力を読む
SQLFluffはpipで入ります。公式のGetting Startedにある確認手順をそのまま実行します。
pip install sqlfluff
echo " SELECT a + b FROM tbl; " > test.sql
sqlfluff lint test.sql --dialect ansi公式ページに載っている出力は、次のとおりです。
== [test.sql] FAIL
L: 1 | P: 1 | LT01 | Expected only single space before 'SELECT' keyword.
| Found ' '. [layout.spacing]
L: 1 | P: 1 | LT02 | First line should not be indented.
| [layout.indent]
L: 1 | P: 1 | LT13 | Files must not begin with newlines or whitespace.
| [layout.start_of_file](以降のLT01、LT12の行は省略しています。)1行が「行番号 | 桁 | ルールコード | 内容 | ルール名」の並びです。LTはlayoutグループのコードで、ルール名のlayout.spacingのようなドット表記でも指定できます。この形式なら、Claudeに返す指摘としてもそのまま読めます。
lint・fix・formatの違い
SQLFluffのCLIには、hookで使い分けられる3つのコマンドがあります。
| コマンド | 働き | hookでの位置づけ |
|---|---|---|
lint | 働き指摘を出すだけで、ファイルは書き換えない | hookでの位置づけ修正後の再確認、CIでの検査 |
fix | 働き直せる違反をファイルに書き込む | hookでの位置づけ編集直後の自動修正 |
format | 働き安定したルールの一部に絞ってfixを強制適用する | hookでの位置づけ整形だけでよいときの軽い選択肢 |
formatは有効にしたルールの指定を無視し、除外指定だけに従います。rulesで絞った規約検査をしたいならfixを使います。
fixは3.0以降、確認の入力待ちを挟まずにファイル単位で修正を適用します。以前の-f(--force)オプションは非推奨で、付けなくても同じ動きです。編集直後に非対話で走らせるhookに向いています。
.sqlfluffでチームの規約を1か所にまとめる
hookが守らせる中身は.sqlfluffに書きます。新規プロジェクトの設定ファイルは最小に保ち、チームの判断を記録する役目を持たせる、というのが推奨です。dbtプロジェクトの出発点は次のような形です。
[sqlfluff]
dialect = snowflake
templater = dbt
encoding = utf-8
exclude_rules = ambiguous.column_count, structure.column_order
max_line_length = 120
processes = -1
[sqlfluff:templater:dbt]
project_dir = ./
[sqlfluff:templater:jinja]
apply_dbt_builtins = True
[sqlfluff:rules:capitalisation.keywords]
capitalisation_policy = lowerdialectは使っているデータウェアハウスに合わせて変えます。選べる方言はsqlfluff dialectsで確認できます。exclude_rulesの2つは、新規プロジェクト例が「議論の分かれやすいルール」として挙げているものです。外すかどうかはチームで決めます。
大文字小文字のルールは、既定がconsistentで、ファイルの残りから自動判定します。新規プロジェクトではlowerやupperに固定した方が、Claudeが書いたファイルの揺れを拾えます。
encoding = utf-8は、dbt templaterを使うときの推奨設定です。sqlfluff fixが書いたファイルをdbtがあとで読むときに、文字コードの自動判定に頼らずに済み、UnicodeDecodeErrorを避けられます。dbt_packages/やtarget/はlintの対象から外します。
cat > .sqlfluffignore <<'EOF'
target/
dbt_packages/
macros/
EOFdbt_modules/は1.0.0より前のdbt向けです。1.0.0以降はdbt_packages/を指定します。
hookの設定とスクリプト
PostToolUseは、Claudeがツールを成功させた直後に動くイベントです。Edit|Writeにマッチさせ、スクリプトに編集したファイルのパスを渡します。.claude/settings.jsonは次のとおりです。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/sqlfluff-fix.sh",
"timeout": 120
}
]
}
]
}
}${CLAUDE_PROJECT_DIR}はセッションを始めたプロジェクトのルートを指します。スクリプトはstdinのJSONからtool_input.file_pathを取り出し、.sql以外なら何もせず終わります。
mkdir -p .claude/hooks
cat > .claude/hooks/sqlfluff-fix.sh <<'SH'
#!/bin/bash
file_path=$(jq -r '.tool_input.file_path // empty')
case "$file_path" in
*.sql) ;;
*) exit 0 ;;
esac
[ -f "$file_path" ] || exit 0
output=$(sqlfluff fix --templater jinja \
--show-lint-violations "$file_path" 2>&1)
status=$?
if [ "$status" -ne 0 ]; then
echo "$output" >&2
exit 2
fi
exit 0
SH
chmod +x .claude/hooks/sqlfluff-fix.sh--show-lint-violationsはfixのオプションで、直せなかった違反だけを一覧にします。--templater jinjaは、.sqlfluffにtemplater = dbtと書いてあっても、このhookの実行だけJinjaに切り替えます。理由は次の節で説明します。jqが無いとパスを取り出せず、hookは何もせずに終わります。
dbtプロジェクトではtemplaterをどう選ぶか
dbtのSQLは{{ ref('stg_orders') }}のようなJinjaを含むため、SQLFluffは先にテンプレートを展開してから検査します。展開を担うのがtemplaterで、公式は使い分けの目安を次のように示しています。
| 使う場面 | 向くtemplater | 理由 |
|---|---|---|
| CI/CD(速度より展開の正確さ) | 向くtemplaterdbt | 理由ほぼすべてのマクロが動く |
| IDEやgit hook(応答の速さ) | 向くtemplaterjinja | 理由dbtの複雑さを持ち込まず、速い |
dbt templaterは、コンパイル時にデータベースへ問い合わせるモデルがあると、検査にもデータベース接続が要ります。接続先は本番でなくテスト用で構いません。実行も遅くなります。1ファイルを編集するたびに走るhookには重い構成です。
そこで、hookではJinja templaterを使い、ref・source・var・is_incremental()といったdbtの組み込み関数はapply_dbt_builtins = Trueで補います。この設定が前掲の.sqlfluffにある[sqlfluff:templater:jinja]です。dbt templaterを使う検査は、CIのsqlfluff lintで別に回します。
dbt templaterを使う側の準備は次のとおりです。アダプターの選択は方言とそろえます。
pip install dbt-snowflake sqlfluff-templater-dbttemplaterの設定は、実行ディレクトリの.sqlfluffにだけ書けます。サブディレクトリの.sqlfluffでは変更できません。hook側の切り替えにCLIの--templaterを使うのは、この制約を避ける意味もあります。
既存プロジェクトでは規約を絞って始める
すでにあるモデルにいきなり全ルールをかけると、Claudeが触っていない行まで指摘が並びます。rulesとexclude_rulesで対象を絞れます。どちらもカンマ区切りで、ルールコード(LT01)、ルール名(layout.indent)、グループ名(layout、capitalisation)を混ぜて書けます。rulesが空なら全ルールが対象で、exclude_rulesはその後に差し引かれます。
注意点は継承です。子ディレクトリの.sqlfluffでrulesかexclude_rulesを書くと、親の値は合成されず丸ごと置き換わります。推奨されるのは、プロジェクトのルートでrulesを1回だけ決め、サブディレクトリではexclude_rulesだけを使う運用です。hookを追加する前に、この方針で設定を整理しておくと、Claudeが編集する場所によって規約が変わる事態を避けられます。
特定のファイルだけ例外にしたいときは、ファイル冒頭に-- sqlfluff:indentation:tab_space_size:2のようなコメントを置く方法もあります。ファイル全体にかかる設定なので、1ファイルで完結する例外に限るのが前提です。
直らなかった指摘をClaudeに返す
PostToolUseのhookは、終了コードが0なら標準エラー出力をClaudeに見せません。終了コード2にすると、ツールはすでに実行済みですが、標準エラーの内容がClaudeに届きます。前掲のスクリプトは、sqlfluff fixが0以外で終わったときだけexit 2にしています。
sqlfluff fixは、直せない違反(unfixable)が残ると0以外で終わります。空白のような機械的に直せる違反はfixが書き換え、書き換えでは解けない違反だけが残って、Claudeに返ります。
Claudeが指摘を消すために-- noqaを書き足すことがあります。-- noqaは全ルール、-- noqa: CP02,CP03のようにコードを付けると特定ルールだけを無視する書き方です。便利な一方で、規約を回避する手段にもなります。次のCLAUDE.mdの断片で、使い方に歯止めをかけます。
## SQL
- 規約の正は `.sqlfluff`。モデルを書いたら hook の出力を読み、指摘を直す
- `-- noqa` で指摘を消さない。除外したいルールは理由を添えて提案する
- `target/` と `dbt_packages/` のSQLは編集しない書き換えの範囲にも注意します。sqlfluff fixは、テンプレートの展開エラーや構文エラーがあるファイルでは、既定で何も直しません。noqaや--ignoreで表示を消していても同じです。--FIX-EVEN-UNPARSABLEで直せますが、公式は、SQLを壊す可能性があるため、適用後に差分を確認するよう警告しています。hookには付けない構成が無難です。
コミット時にもう一段かける
ClaudeがBashのリダイレクトなどでSQLを書き換えた場合、PostToolUseのEdit|Writeには届きません。この抜けは、PostToolUse hookのLint自動修正の早見表でも扱っています。コミット直前の検査を足すと、取りこぼしを拾えます。
SQLFluffは公式にpre-commitのフックを2つ用意しています。sqlfluff-lintは指摘を返し、sqlfluff-fixは違反を直します。dbtの検査をかけたいときは、additional_dependenciesにアダプターとsqlfluff-templater-dbtを足します。
repos:
- repo: https://github.com/sqlfluff/sqlfluff
rev: <使うバージョンのタグ>
hooks:
- id: sqlfluff-lint
# additional_dependencies: ['dbt-snowflake', 'sqlfluff-templater-dbt']pre-commitは変更したファイルだけをsqlfluff lint a.sql b.sqlの形で渡すため、全体を走査せずに済みます。.sqlfluffignoreと併用するときは、pre-commit側にもexcludeを設けると、無視設定を上書きした旨の警告が出なくなります。
dbtモデルそのものの実行やリネージはhookの役目ではなく、dbt MCPサーバーの使い方で扱うMCP経由の操作が受け持ちます。
よくあるつまずき
- hookが動いた形跡がない:
${CLAUDE_PROJECT_DIR}/.claude/hooks/sqlfluff-fix.shに実行権限があるか、jqとsqlfluffがhookを起動するシェルのPATHにあるかを確認します。 - dbtの
refでテンプレートエラーになる: hookで--templater jinjaを付けているか、apply_dbt_builtins = Trueが.sqlfluffにあるかを見ます。 - マクロのディレクトリが大量に指摘される:
macros/は.sqlfluffignoreに入れます。dbt向けの設定例にも含まれています。 - dbt templaterの検査だけ落ちる: コンパイル時にテーブルを参照するマクロが原因のことがあります。
dbt_skip_compilation_errorは既定でTrueのため、エラーが隠れています。原因を見たいときはFalseにします。 - 実行が遅い:
processes = -1はCPUコア数から1を引いた数のプロセスで並列に検査する設定です。複数ファイルをまとめて検査するCIで効きます。1ファイルずつ走るhookでは、遅さを左右するのはtemplaterの選択です。
まとめ
SQLの整形規約は、.sqlfluffに1か所、hookに1か所だけ書けば、Claudeの編集のたびに自動で守られます。hookはJinja templaterで速く回し、dbt templaterによる厳密な検査はCIとpre-commitに任せる分担が、templaterの使い分けの目安にも沿っています。直せない指摘は終了コード2でClaudeに返し、-- noqaによる回避は、CLAUDE.mdで一言歯止めをかけます。