A free, local health check for dbt projects. No warehouse connection, no credentials, no account, no signup. It reads your dbt project from disk and tells you what's wrong with it.
uvx fintel-scan # or: pipx run fintel-scanThat's it. Run it in any dbt project directory.
fintel-scan · jaffle_shop
5 models · 0 sources · 5 findings
Cost & FinOps ██████████ 10.0/10 0 findings
Data Quality ██████████ 10.0/10 0 findings
Security █████████░ 9.2/10 1 findings
Code Quality ████████░░ 8.0/10 4 findings
────────────────────────────────────────────────────────────────────────
MEDIUM CQ-002 Model is hard to reason about (6 chained CTEs)
models/customers.sql
customers is 70 lines with roughly 6 CTEs. Models this size are
difficult to test in isolation and tend to be rewritten rather than
modified.
Fix: Split the intermediate CTEs into their own models. Each becomes
independently testable and reusable, and the lineage graph
starts documenting itself.
Every finding names the file, explains why it matters in plain English, and gives you a concrete fix.
Most data quality tooling wants your warehouse credentials, a signup flow and a monthly invoice before it will tell you anything at all. A large share of what's actually wrong with a dbt project is visible in the code, and finding it shouldn't require any of that.
So this runs locally, statically, and for free. Nothing leaves your machine.
uvx fintel-scan # zero-install, recommended
pipx run fintel-scan # same idea
pip install fintel-scan # if you'd rather have it permanentlyPython 3.9+. The only dependency is PyYAML.
fintel-scan # scan the current dbt project
fintel-scan path/to/project # scan somewhere else
fintel-scan --verbose # show every finding, not just the top 12
fintel-scan --only cost,security # restrict to certain categories
fintel-scan --json # machine-readable output
fintel-scan --fail-under 7 # exit 1 if any category scores below 7It walks up from the given path looking for dbt_project.yml, so it works from anywhere inside
your project.
As a GitHub Action:
- uses: AbdoZwein/Fintel-scan@v1
with:
fail-under: 7It writes a scored summary to the workflow run page and fails the job if any category drops below your threshold. Or just call it directly:
- name: dbt health check
run: uvx fintel-scan --fail-under 7Scores are density-based, so a large project isn't penalised simply for being large.
| Rule | Severity | What it catches |
|---|---|---|
| Cost & FinOps | ||
CF-001 |
medium | SELECT * against a raw source |
CF-002 |
low | Table materialization nothing downstream reads |
CF-003 |
high | Unfiltered scan of a raw source — the classic full-table scan |
CF-004 |
critical | JOIN with no ON/USING — a cartesian product |
| Data Quality | ||
DQ-001 |
high | Model has no tests at all |
DQ-002 |
medium | The model's own primary key missing unique/not_null |
DQ-003 |
medium | Source with no freshness check |
DQ-004 |
low | Model that's unreferenced and undocumented |
| Security | ||
SEC-001 |
critical | Hardcoded credentials committed to the repo |
SEC-002 |
medium | PII-shaped columns with no masking applied |
SEC-003 |
high | Hardcoded project.dataset.table instead of source()/ref() |
| Code Quality | ||
CQ-001 |
low | Model with no description |
CQ-002 |
medium | Over 300 lines, or more than 5 chained CTEs |
CQ-003 |
low | Inconsistent naming conventions across models |
CQ-004 |
low | TODO/FIXME/HACK left in a production model |
The checks are deliberately narrow. A linter you can't trust gets uninstalled on day one and never gets a second chance, so where a rule would be ambiguous it's either tightened until it's safe or dropped entirely.
Two concrete examples of that principle:
CF-001only flagsSELECT *against a raw source, not against aref(). Selecting everything from an upstream model is idiomatic in staging layers and flagging it would be noise.DQ-002only flags a model's own primary key.customer_idinside anordersmodel is a foreign key, and telling you to put auniquetest on it would be actively wrong.
If you hit a false positive, please open an issue. It's treated as a bug, not a tuning preference.
- Runs entirely on your machine
- Reads
.sqland.ymlfiles in your dbt project, nothing else - No network calls. No telemetry. No analytics. No phoning home.
- Never asks for warehouse credentials, because it never connects to one
You can verify all of this — it's about 700 lines of Python and the source is right here.
fintel-scan is static analysis. Some things are genuinely invisible without query history and
table statistics:
- What a query actually costs in €/year
- Which tables are hot and which haven't been read in six months
- Runtime row counts, actual null rates, real cardinality
- Warehouse-level configuration — IAM roles, row-level security, grants
If you want those, Fintel Insight connects read-only to your warehouse and produces a full scored report with cost impact per finding. It's $99, one-time, no subscription. It's built by the same people and uses the same rule IDs, so findings line up.
This tool is genuinely useful on its own and is not crippled to sell you that. If static analysis is all you need, use it and ignore the rest.
New rules are welcome, especially warehouse-specific ones. The bar is:
- Deterministic — no LLM calls, no network, no guessing
- Near-zero false positives — if you can construct a reasonable project it misrepresents, tighten it
- Actionable — every finding must state a concrete fix
Rules live in fintel_scan/rules.py; each is a function decorated with @rule returning
Finding objects. tests/fixtures/demo_project is a deliberately broken dbt project used to
verify checks fire.
git clone https://github.com/AbdoZwein/Fintel-scan.git
cd Fintel-scan
pip install -e .
python -m pytest tests/ # or: fintel-scan tests/fixtures/demo_projectMIT. Built by Fintel Analytics.