[v2 Bug] Fusion `dbt lint --fix` returns exit code 0 even when it modified files — pre-commit can't detect the fix
- Dominant language
- Rust
- Stars
- 13.8k
- Forks
- 2.6k
- Avg merge
- 21h 31m
- Merged PRs (30d)
- 56
Description
## Summary
`dbt lint --fix` exits with status **`0`** even when it applied auto-fixes and modified files on disk. This is the standard "auto-formatter" pre-commit contract violation: hooks like `prettier`, `black`, `ruff --fix`, and `sqlfluff fix` all exit **non-zero when they rewrite a file**, so pre-commit can catch the fix, block the commit, and prompt the user to re-stage. With `dbt lint --fix` exiting `0`, a pre-commit hook silently rewrites tracked files and lets the commit through with un-staged changes on disk.
## Environment
- **OS:** macOS 26.5 (build 25F71)
- **CPU:** ARM (Apple M1 Pro, `arm64`)
- **dbt distribution and version:** dbt-fusion `2.0.0-preview.196` — native Rust binary, installed via `pip install dbt` (pip package `dbt==2.0.0rc196`)
- **Adapter:** Snowflake
## Reproduction
The project's `.sqlfluff` explicitly configures both CP01 (keyword case) and CP02 (identifier case) to `lower` — so these are not defaults being silently loosened, they are an actively-declared style policy:
```ini
# .sqlfluff (excerpt)
[sqlfluff]
dialect = snowflake
templater = dbt
[sqlfluff:rules:capitalisation.keywords]
# Keywords — drives CP01 / KeywordCaseMismatch (dbt0107)
capitalisation_policy = lower
[sqlfluff:rules:capitalisation.identifiers]
# Unquoted identifiers — drives CP02 / IdentifierCaseMismatch (dbt0167)
extended_capitalisation_policy = lower
```
Start with a file that has fixable violations (uppercase keywords, trailing whitespace):
```sql
-- models/base/beehive/beehive_deals/_LINT_REPRO.sql
SELECT *
FROM {{ ref('beehive_deals') }}
WHERE ID = 1
```
Run `dbt lint --fix` on the project and check the exit code and the file:
```bash
$ md5 models/base/beehive/beehive_deals/_LINT_REPRO.sql
MD5 (...) = c214df7df40b80a431506b73f0953c26
$ dbt lint --fix; echo "exit=$?"
...
Fixed 4 violation(s) in models/base/beehive/beehive_deals/_LINT_REPRO.sql
Finished 'lint' with 1 warning for target 'development' [2.9s]
exit=0
$ md5 models/base/beehive/beehive_deals/_LINT_REPRO.sql
MD5 (...) = dbd7c0e2fe7b4c408aeac97bfec8ceff # file was rewritten
$ cat models/base/beehive/beehive_deals/_LINT_REPRO.sql
select *
from {{ ref('beehive_deals') }}
where id = 1 # SELECT → select, FROM → from, WHERE → where
```
The file was modified on disk (4 violations fixed, keywords lowercased) and dbt still exits `0`.
For contrast, the sibling behaviors are correct:
| Scenario | Files modified? | Exit code | Correct? |
|---|---|---|---|
| `--fix` applied fixes | **Yes** | **0** | ❌ this bug |
| `--fix` clean file, nothing to fix | No | 0 | ✅ |
| `--fix` with unfixable errors (e.g. `SyntaxInvalid`) | No | 1 | ✅ |
## Expected behavior
Match the standard auto-formatter contract: **exit non-zero (e.g. `1`) whenever `--fix` modified at least one file**. This is how `prettier --write`, `ruff format`, `black`, and `sqlfluff fix` all behave, and it's what the `pre-commit` framework relies on to detect "files were rewritten, block the commit, re-stage".
Concretely, the exit-code table should become:
| Scenario | Files modified? | Exit code |
|---|---|---|
| `--fix` applied fixes | Yes | **≠ 0** |
| `--fix` clean, nothing to fix | No | 0 |
| `--fix` unfixable errors remain | No | ≠ 0 |
| `--fix` applied fixes AND unfixable errors remain | Yes | ≠ 0 |
(No change requested to the exit code of plain `dbt lint` without `--fix` — warnings-only staying `0` there is fine.)
## Impact
- **Pre-commit hooks**: A `.pre-commit-config.yaml` entry like `entry: dbt lint --fix` cannot function as a normal auto-fixer. The hook rewrites tracked files but reports success, so the commit goes through with the *original* (pre-fix) content staged and the fixed content sitting un-staged in the working tree. This is the exact failure mode `pre-commit` was designed to prevent.
- **CI auto-format jobs**: A CI step that runs `dbt lint --fix` and then `git diff --exit-code` to detect drift works, but the more idiomatic `dbt lint --fix || exit 1` pattern does not.
- **Editor "format on save" tooling**: Tools that key off exit code cannot tell whether the file was rewritten.
## Related
- [#15519](https://github.com/dbt-labs/dbt-core/issues/15519) — `dbt lint --select` flag is ignored (same command, different bug)
Contributor guide
Assessment
This issue has not been assessed yet.