Changelog¶
All notable changes to Scherlok are documented here.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased¶
Fixed¶
- Fixing a column no longer fails every later run — a new table with a column NULL in every row gets a WARNING
all_null_column, and a WARNING reading is learned, so the 100% NULL rate became the baseline. Once the column was filled, the drop to 0% was a CRITICALnull_rate_change, and since a CRITICAL reading is never learned,watch,ciandverifyfailed on every run until someone ranscherlok accept. A NULL rate that falls is now at most a WARNING, against the learned baseline and against the last profile alike. It is still reported, since a drop can also be a bug such as aCOALESCEdefault, and the next run learns it. A rise keeps its thresholds: WARNING from 10 points, CRITICAL from 30. (#136) - Duplicated keys are flagged after the fifth run too — the distinct ratio of a near-unique column, which shows a join that fans out, was only compared until a table had five readings. From then on the learned distinct count took over, and a fan-out adds rows, not distinct values, so 20% duplicated keys showed up as an INFO
volume_spikeat most. The ratio is now checked first at any history length, with the same thresholds (WARNING from a 10% change, CRITICAL from 50%), and the learned distinct count is compared only when the ratio held. (#137)
[1.3.0] — 2026-11-05¶
Added¶
- GitHub Action: data checks on the pull request — a failed gate was only a red job, so a reviewer had to dig through the log to learn what broke.
uses: rbmuller/scherlok@v1(action.ymlat the repository root, a composite action) runsscherlok verify --output jsonagainst the baseline instore, writes the job summary, fails the step when an anomaly reachesfail-on, and creates or updates one pull request comment (found again by a hidden marker) with the table, column, metric, baseline, current value, change and severity of each anomaly, falling back to the type and message for a Scherlok without the structured fields. Inputs:connection,store,tables,fail-on,comment,footer(the "Checked by Scherlok" line),github-token,extras,scherlok-version(pinned to the release of the action tag) andinstall-from(a local checkout, for testing the action); outputs:result,anomaly-count,reportandcomment-url. A missingstorefails with an error instead of passing with nothing to compare. The connection reaches Scherlok through the environment, never the command line, and is masked in the log with its password and credential query parameters; the step holding the connection never gets the GitHub token, and the step posting the comment never gets the connection. The comment holds names, aggregates and fixed hints only, never a row value. Fork pull requests, which get no secrets, are skipped with a notice, and the docs warn againstpull_request_targeton untrusted code. Needscontents: readandpull-requests: writeonly. CI runs the action from the checkout against a DuckDB fixture, on passing and on broken data. The release and Docker workflows now run onvX.Y.Ztags only, so moving the action'sv1tag publishes nothing. See GitHub Action. (#112) scherlok verify --against, a build compared with production — agents and developers build in a dev schema or database first, and there was no way to compare that build with production's baseline before publishing it.scherlok verify --against postgresql://db.example.com/shopcompares the connection's tables with the baseline another database left in the same store, and saves no profiles or anomalies under either; a dropped column or a changed type fails as CRITICAL.--againsttakes a scope, or a connection string reduced to its scope and never saved or printed. Only records saved under that scope count: records from before scopes existed are skipped, since they could have come from the build itself. A scope with no baseline in the store, or the database being verified, exits 1 instead of passing with nothing verified. Freshness is not compared, since a build is updated when it is built, not on production's schedule. Tables the baseline lacks get the new-table checks.--ignore volumeis for builds on sampled data: row counts are not compared (a table now empty is a WARNING), cardinality compares only the distinct ratio of near-unique columns and gains of 50% or more in distinct values, and NULL rates and means are compared with the latest baseline profile rather than the spread learned from history; it also works without--against.--output jsonand run records gainagainstandignored; the store gains the two run columns through a migration, and older runs read backnulland[]. Agents get the same comparison from MCPverify(tables, fail_on, against, ignore), whereagainstmust be a scope, so credentials never travel in a tool call. See Compare a build with production. (#110)- Claude Code plugin: the data is checked after every dbt run — an agent that finished
dbt runreported success unless someone told it to look at the data.integrations/claude-code/is a Claude Code plugin, installed with/plugin marketplace add rbmuller/scherlokand/plugin install scherlok@scherlok(the repository root now holds.claude-plugin/marketplace.json). ItsPostToolUsehook on Bash runs after adbt runordbt buildthat succeeds, in the foreground, and callsscherlok verify --output json --dbt-project <project>on the models that run built, finding the project and target directory the way dbt does (--project-dir,DBT_PROJECT_DIR, the nearestdbt_project.yml;--target-path,DBT_TARGET_PATH). A failed check reaches Claude asdecision: "block"with every finding (severity, table, column, message and hint, CRITICAL first, capped at 20) next to the command's output, plus a one-line message for the person; a pass arrives as a summary that also lists WARNING findings, tables without a baseline and models not checked; a check that could not run (timeout, connection error, missing artifacts) is reported as not verified, never as silence. The hook does nothing when Scherlok is not installed, predates this release, or has no connection, and exits 0 on every path; it is a standard-library Python script (3.8+) with no credentials, which asksscherlok config --output jsonwhether a connection exists instead of reading~/.scherlok.SCHERLOK_BINpoints it at an executable offPATH, andSCHERLOK_HOOK_TIMEOUT(150 s by default, under the hook's 180 s) bounds the check. The skillverify-data-pipelinegives the loop: verify what was built, read the structured findings, fix the cause (withscherlok reconcilewhen a join may have fanned out), run and verify again, report what was not checked; it never runsscherlok accept,investigate,watch,ci,checkordbtand asks a person when a finding is the intended result. The same instructions, without the hook, are inintegrations/AGENTS.snippet.mdfor other coding agents. Locked baselines and aBash(scherlok accept *)deny rule are recommended for agent sessions, and remain a guardrail, not a security boundary.examples/claude_code_demo/is a dbt project on DuckDB where a join fans out, with a script for recording the demo. See Coding agents. (#113) scherlok verify --dbt-project DIR, a check scoped to the last dbt run —verifychecked every table or a--tablelist, so checking after each dbt command meant profiling the whole project.--dbt-projectverifies the models that succeeded in the project's lastrun_results.json, named throughmanifest.jsonand matched to the connection's tables asdbt-run-and-watchmatches them (alias, then name), and they count as just built, like--table.--dbt-target-pathreads the artifacts from another target directory, relative to the project unless absolute. A built model the connection can't see is listed under a newdbt_models_not_foundkey of--output jsonand does not fail the gate; a run that built nothing verifies nothing instead of every table; missing or malformed artifacts exit 1.--tableadds to the scope. The manifest's adapter is not checked, since the connection is Scherlok's, so DuckDB and MySQL projects work.load_manifestandload_run_resultsgain atarget_pathargument andload_manifestacheck_adapterswitch;built_models()joins the two artifacts. (#113)scherlok config --output json— printsconnection_configured,connection_source(env,configornull),store(credentials masked) andbaseline_lockedas one JSON object, never the connection string, so a script or a hook can tell whether Scherlok is set up without reading~/.scherlok/config.json. It only shows settings: combined with--storeor--baseline-lockedit exits 1 and changes nothing. (#113)scherlok reconcile, an output table checked against its input — comparing a table with its own history can't see a join that fans out on the first build, or a total that changes while the row count stays the same.scherlok reconcile --source raw.orders --target mart.orders --key order_id --sum amount --tolerance 0.001compares the two tables in the same database and reportsrow_count_mismatch,duplicate_keys(a key on more than one row of the target),missing_keys(source keys the target lacks),extra_keys(target keys the source lacks) andsum_mismatch(totals apart by more than--tolerance), each CRITICAL with the structured fields and a fixed hint; any finding exits 1.--keyand--sumrepeat (composite keys, several totals); rows with a NULL key are counted apart and never matched; a total with no value counts as 0;--toleranceis absolute and inclusive, compared as decimals so aDECIMALtotal is exact and a float total is taken at the value it prints as. Table names may be schema-qualified, and every identifier is quoted by the connector.--output jsonprints one object on stdout (passed, per-tablerow_count,null_keys,distinct_keys,duplicate_keys,duplicate_rowsandsums,missing_keys,extra_keys, severity counts,anomalies). Only counts and totals are reported, never a key value, and a database error is reported by its type, not its text, which can quote a row value. Nothing is saved and no run is recorded. Connectors gainget_key_stats,get_column_sumsandcount_unmatched_keys, shared aggregate SQL quoted per dialect, for PostgreSQL, MySQL, DuckDB, BigQuery and Snowflake. On BigQuery and Snowflake every run scans both tables in full; see the reconcile guide. Agents get the same check as the read-only MCP toolreconcile(source, target, key_columns, sum_columns, tolerance), which only accepts tables listed bylist_tablesand columns present in both. (#111)scherlok accept: baseline changes need a person's approval — since a CRITICAL reading is no longer learned (see Changed), an intended change, such as a new column or a planned backfill, needs a way to become the new normal, and an agent should not be the one to approve its own work: the MCPinvestigateandwatchtools wrote baselines with an ordinary tool call.scherlok accept --table NAME [--table NAME ...] --reason "..."profiles each table, shows how its state compares with the current baseline (rows and columns before and after, and the anomalies the new state shows), asks for confirmation (--yesskips it, for a workflow a person triggers) and saves the new profiles as the baseline.--reasonis required. A table without a baseline gets its first one. Each table also gets one row in a new append-onlybaseline_approvalstable (updates and deletes are refused by triggers):table_name,scope,approved_by(git config user.email, else$USER),reason,before_summaryandafter_summary(row and column counts, profiling time, and per anomaly its type, severity and structured fields; never a column value or a connection string) andapproved_at.scherlok history --approvalslists them, with--output json. Locked mode, off by default:scherlok config --baseline-lockedorSCHERLOK_BASELINE_LOCKED=1(any value but empty,0,false,nooroff; the variable can't unlock a saved lock). When locked, onlyacceptwrites baselines, including the first one for a new table:watch,ci,check,dbt,dbt-run-and-watchand the MCPwatchandchecktools compare and still save anomalies and runs, but no profiles;investigateand MCPinvestigaterefuse.acceptis deliberately not an MCP tool. This is a guardrail, not a security boundary: an agent with shell access can run the CLI or edit the SQLite file. Real enforcement means runningverifyin CI against a store the agent has no credentials for, withacceptrun from a workflow a person triggers. The store gains the table through a migration. (#109)- Structured anomaly fields — an anomaly had four keys (
table,type,severity,message), so the column and the numbers were only in the sentence and an agent had to parse it to learn what broke. Every anomaly now also carriescolumn,metric(row_count,column_type,hours_since_update,null_rate,null_count,mean,distinct_countordistinct_ratio),baseline(the stored profile, or the learned median once there is enough history),current,change_pct(signed, one decimal) andhint, one fixed sentence per anomaly type on where to look first; a field that does not apply isnull.baselineandcurrentare aggregates or column types, never a value read from a row. The existing four keys and every message are unchanged. The fields appear inverify,history,dbtanddbt-run-and-watchwith--output json, indemo --output json, and in the MCPwatch,verifyandhistorytools, whose output is still capped at 200 anomalies. They are saved in a new nullabledetailscolumn of the anomaly history, added the first time a store is opened; existing rows are kept and read back with every fieldnull, and a runner on an older release can still write to the same store. See Structured anomaly fields. (#108) - New-table checks — a table seen for the first time has no baseline, so nothing could fire, even on an empty table or a 1,000-row table whose
revenuecolumn is NULL in every row. Agents often create tables instead of changing existing ones, so the first run is when a check matters most. A table's first profile now gets four checks, computed from the profiles already collected (no extra query):empty_table(0 rows, WARNING),all_null_column(a column NULL in every row of a table with rows, WARNING),duplicate_key(idor<singular table>_idwith fewer distinct values than non-NULL rows, WARNING; a foreign key such asuser_idinordersis left alone) andconstant_column(one value in 100+ non-NULL rows, INFO). Once a table has a baseline they never run again.scherlok verify --tableand MCPverify(tables=...)treat the tables they name as just built, so their WARNING findings count as CRITICAL and fail the default gate;investigatestill profiles without reporting anything. (#107) - Every run is recorded, including runs that pass — a run that found nothing left no trace, so there was no way to show that a check ran and passed.
watch(each--scheduletick too),ci,check,verifyand the MCP toolswatch,checkandverifynow write one row to a newrunstable:run_id,command(mcp:<tool>for the MCP tools),scope,scherlok_version, the names of the tables checked,passed,fail_on, counts per severity,started_atandfinished_at. Each saved anomaly carries therun_idof the run that found it, also printed byhistory --output json.scherlok history --runslists runs, with--output json.verifystill saves no profiles or anomalies; it records its run in the local store only, since it never uploads a remote store, andverify --record PATHappends the same record as one JSON line, for CI to keep as an artifact. A record holds table names and counts only, never column values or connection strings. The store gains the table through a migration; existing anomalies keep an emptyrun_id. (#115) scherlok verify, a read-only gate — compares the current state with the stored baseline and saves no profiles or anomalies, so retrying a failed check keeps failing until the data is fixed. Until now every path (watch,ci,check, MCPwatchandcheck) saved each run, so the run after a failure passed against the broken state.--table(repeatable) limits scope,--fail-on critical|warningsets the exit code, and--output jsonprints one object on stdout (passed,fail_on,tables_verified,tables_without_baseline,anomaly_count,critical/warning/info,anomalies) with Rich output, including a one-line verdict when the gate fails, on stderr. Tables without a baseline are listed and never counted as verified (see the new-table checks above for when they fail the gate). The connection comes fromSCHERLOK_CONNECTIONor the saved config and is never written to it; a remote store is downloaded but never uploaded, so parallel jobs can't overwrite each other. Agents get the same gate as the read-only MCP toolverify(tables, fail_on). (#106)scherlok watch --schedule "0 6 * * *"— keeps the process alive and runs the regular watch path (alerts,--explain) on every five-field cron tick in the host's local time; the Docker image defaults to UTC. Each run's exit code is logged without stopping the loop,SIGINT/SIGTERMlet the current run finish before exiting, and missed ticks are skipped rather than replayed.croniterships behind the optionalscherlok[schedule]extra (bundled in the Docker image); a missingcroniteror an invalid expression exits 1 with a one-line hint. Thanks to @bferanmi806-sketch. (#90, #105)
Changed¶
- Behaviour change: a reading with a CRITICAL anomaly no longer becomes the baseline — every run of
watch,ci,check,dbt,dbt-run-and-watchand the MCPwatch,checkandinvestigatetools saved its profiles as the new baseline, so after a CRITICAL anomaly the broken state became the reference and the alert stopped although nothing was fixed: after one run on a table with a steady row count, after 15 on a table with natural variation, once broken readings filled half of the 30-run window. Now a run that finds a CRITICAL anomaly on a table saves the anomaly but none of that table's profiles, so the alert repeats on every run until the data recovers, which needs no manual step. WARNING and INFO findings are learned as before, and the other tables of the run are unaffected. To make an intended change the new normal, runscherlok accept(see Added).freshness_criticalis the exception and is still learned: freshness compares a table's age with its age on the previous run, so a weekly table watched daily would otherwise alert on every run.watch,ci,checkanddbtname the tables that kept their baseline, and MCPinvestigatereturnsbaseline_updatedper table.verifyis unchanged. (#116) - First runs can report warnings —
watch,ci,check,dbt,dbt-run-and-watchand the MCPwatchandchecktools used to report nothing for a table they had never seen. They now report the new-table checks above, which are saved to the anomaly history and sent to webhooks and e-mail like any other anomaly. They are WARNING or INFO, so the exit code under the default--fail-on criticalis unchanged, but a gate on--fail-on warningnow fails the first time it sees an empty table, an all-NULL column or a duplicate key. (#107) - Baselines are kept per database — profiles and anomalies were keyed by table name alone, so two databases with an
orderstable shared one baseline in the same store, and a run against staging moved the production baseline. Every profile and anomaly now carries ascopenaming its database: the adapter, host, port and path of the connection string (postgresql://db.example.com/shop,snowflake://my-account/SHOP/PUBLIC,duckdb:///data/warehouse.duckdb), built without the user, password, query string or fragment, withpostgres://read aspostgresql://and the default port dropped. Comparisons,history,status,report,dashboardand the MCP tools use the configured connection's scope. Records saved before the upgrade have no scope and keep counting for every connection, so no history is lost; the store gains the column through a migration the first time it is opened. (#110) - One
--fail-ondecision —ci,check,dbt,dbt-run-and-watch,verifyand the MCPcheckandverifytools now share one helper instead of three copies. An unknown value such as--fail-on warningsexits 1 with a hint before any work, where the CLI used to treat it silently ascritical. MCPwatchandcheckresults also gain aninfocount. (#106)
Fixed¶
- Table and column names are quoted safely on PostgreSQL, MySQL, BigQuery and Snowflake — the row count and column statistics queries put a name between quotes without escaping it, and BigQuery and Snowflake pasted the table name into a string literal to read its columns and last-modified time, so a name holding a quote, a backtick or a backslash, from
list_tables, a dbt manifest or--table, broke the query or could run SQL of its own. Every name now goes through the connector's own quoting, the same asreconcile; Snowflake binds the schema and table names of its metadata lookups as driver parameters and BigQuery escapes them as string literals. Snowflake also upper-cases the connection string's database and schema before quoting them, as it already did for the table, so with a lower-casesnowflake://account/analytics/publicthe row count and column statistics queries address the same objects aslist_tablesinstead of the case-sensitive"analytics"."public". On PostgreSQL and MySQL a plain name gives the same SQL as before; BigQuery now quotes the project, dataset and table separately (`project`.`dataset`.`table`), which BigQuery reads as the same table. ci <url>andcheck <url>profile the database they were given — withSCHERLOK_CONNECTIONset, both connected to the URL argument but then profiled, and gated on, the database in the environment variable.- Connection strings are fully masked on display — MCP
statusand the dashboard header showed a password containing an unencoded/,?or#in clear, because the host was cut short and nothing looked like a password. Credential query parameters such aspasswordormotherduck_tokenwere shown too. Both now come out as***, and the dashboard uses the same masking as the MCP server. - Column values no longer saved in the profile store — every connector ran a top-5
GROUP BYand aMIN/MAXon each column, andprofiles.dbkept the results in plain text (top_values,min,max), for example e-mails, document numbers, salaries or birth dates. No detector read them. Both queries are gone (one fewer warehouse query per column) and the store now holds aggregates only; detection is unchanged. Existing stores are cleaned the first time they are opened: the three fields are deleted from every distribution profile and the file is rewritten withVACUUM, so they do not linger in free pages; a remote store is cleaned on its next sync. Older copies ofprofiles.dbstill hold these values, including backups and older object versions in a versioned bucket: delete them yourself. A runner still on an older Scherlok that writes to the same store adds new values again, so upgrade every runner that shares it. The store now records a schema version (PRAGMA user_version) and applies pending migrations when opened; a store written by a newer release is left untouched. See What Scherlok stores. (#114) config.jsonis owner-only —scherlok connect,config,ci <url>anddbtsave the connection string, password included, to~/.scherlok/config.json, which was written with the process umask and so was usually readable by every user on a shared machine or CI runner. The file is now created as0600rather than tightened after the write, an existing file is tightened on the next save, and a newly created~/.scherlok(or the directory of a custom store path) is0700. An existing directory keeps its mode. POSIX only; Windows is unchanged. (#117)
[1.0.5] — 2026-10-19¶
Added¶
scherlok report --output json— one JSON array on stdout (table,volume,schema,freshness,anomaly_countper table),[]for an empty database, Rich output on stderr; text mode unchanged.reportwas the last read command without a machine-readable mode. Thanks to @bferanmi806-sketch. (#93, #103)- Last anomaly in
scherlok status— a new column shows the most recent alert per table (volume_drop · 2h ago), and--output jsongainslast_anomaly(type,severity,detected_at, ornull) with every existing key unchanged. Fetched in one window-function query for all tables. Thanks to @bferanmi806-sketch. (#91, #101)
[1.0.4] — 2026-09-24¶
Fixed¶
- Cardinality noise on unique columns — deleting rows also drops the distinct count of a unique
idor timestamp column, so a single volume drop fired acardinality_changewarning per unique column on top ofvolume_drop. For columns the stored profile shows as near-unique (95% or more of non-NULL rows distinct), the detector now compares the distinct ratio rather than the absolute count, with thresholds sized for a bounded ratio (10% warning, 50% critical, inclusive). NULLs are excluded from the denominator, so nulling values in a unique column no longer reads as lost uniqueness — the nullability detector already reports that. Low-cardinality columns and callers without volume profiles keep the previous behaviour. Thanks to @bferanmi806-sketch. (#94, #97)
[1.0.3] — 2026-09-22¶
Added¶
- Claude Desktop extension —
mcpb/buildsscherlok-<version>.mcpb, attached to every GitHub release. Opening it installs the MCP server in one click and asks only for the connection string, which is stored as a secret and never passed by the model. The bundle is under 250 KB and vendors nothing: on first launchuvinstalls the release from PyPI (~90 MB of wheels, about four seconds on a cold cache, cached afterwards), so one bundle serves macOS, Windows and Linux. PostgreSQL, MySQL and DuckDB; BigQuery and Snowflake stay CLI-only because their drivers need a build toolchain on some platforms.cryptographyis held below 50 in the bundle because 49.0.0 dropped x86_64 macOS wheels, which would break a one-click install on an Intel Mac. CI validates and packs the bundle on every PR, andtests/test_mcpb.pyfails if the manifest version, thescherlokfloor or the advertised tool list drift from the package.
[1.0.2] — 2026-09-18¶
Fixed¶
- MCP server reported an empty version —
serverInfo.versionwas blank in everyinitializereply because the package version was never passed to the server class, so clients and registries listed the server without a version. It now reports__version__(mcp 2.x; mcp 1.xFastMCPtakes no version parameter and is unaffected). - Misleading error when an unsupported
mcpis installed — a release exposing neitherMCPServer(mcp 2.x) norFastMCP(mcp 1.x) produced "requires the 'mcp' package … re-install scherlok", which sends the operator down the wrong path. The message now names the installed version and the supported range. Themcpdependency is pinned to>=1.2,<3to match what is actually tested.
Added¶
- stdio handshake test —
tests/test_mcp_server.pynow spawns the installedscherlok-mcpentry point and speaks JSON-RPC over stdio (initialize + tools/list), the way an MCP client or a registry health check does. This is the layer where the two fixes above failed while every in-process test passed. glama.json— declares repository maintainers for the Glama MCP listing.- Documentation site at https://rbmuller.github.io/scherlok/ (MkDocs Material, deployed from
mainby.github/workflows/docs.yml, strict build checked in CI). Includes a sourced comparison with Elementary, Soda, Great Expectations and Monte Carlo; the README table now links to it and no longer quotes unpublished competitor prices. scherlok demo— a self-contained walkthrough that needs no database: seeds a sample DuckDB warehouse (3 tables, 22,500 rows), learns a baseline, ships a "bad deploy" (60% of orders gone, e-mails nulled, free-text plans, a dropped column) and catches it with the real detectors, in about a second. Runs entirely inside one temporary directory and never touches~/.scherlok;--keep/--dirpreserve the files,--output jsonemits the result for scripts. Requires theduckdbextra:uvx --from "scherlok[duckdb]" scherlok demo.DuckDBConnector.close()— releases the file handle so another writer can open the database.- MCP Registry publishing workflow —
.github/workflows/publish-mcp.ymlpublishesserver.jsonto the official MCP Registry after each successful Release run (GitHub OIDC, no stored token), so theio.github.rbmuller/scherloklisting no longer lags behind PyPI.
[1.0.1] — 2026-09-17¶
Fixed¶
- PyPI release — v1.0.0 was tagged and released on GitHub but never reached PyPI: the tag still carried
version = "0.9.0"inpyproject.toml, so the publish job tried to re-upload 0.9.0 and PyPI rejected it with HTTP 400. 1.0.1 ships the 1.0.0 changes below with the version metadata (pyproject.toml,dbt_project.yml,server.json,__version__) corrected and aligned.
[1.0.0] — 2026-09-03¶
Added¶
- Adaptive historical baselines — after enough valid history, volume and column metric detectors learn per-metric variability with robust median/MAD bands; cold-start and unusable histories retain the existing fixed behavior. (#62)
scherlok status --output json— emits a JSON array of table health objects (table,rows,columns,status,last_profiled) for CI parsers. Rich output is routed to stderr in JSON mode so stdout stays machine-readable. (#14)scherlok history --output json— emits a JSON array of anomaly records (detected_at,severity,table,type,message). Empty history returns[]instead of a text message. (#15)scherlok dbt-run-and-watch --output json— the wrapper now accepts the same--output jsonflag asscherlok dbt, so CI users get the wrapper's convenience and a machine-readable stdout payload in one step.dbt run's own stdout is rerouted to stderr in JSON mode so it never mixes with the payload; ifdbt runfails, stdout gets a small JSON error document (project_dir,error,returncode) instead of plain text. (#47)- Targeted
dbt-run-and-watchprofiling — after a successfuldbt run, the wrapper readstarget/run_results.jsonand profiles only successful model nodes from that invocation. Missing or malformed artifacts fail clearly rather than falling back to the full manifest. (#69) dbt buildsupport indbt-run-and-watch— pass--buildto rundbt build; on dbt's handled failure path (exit 1), successful model nodes fromrun_results.jsonare profiled even when tests fail and downstream models are skipped, while unhandled failures fail fast and preserve dbt's exit code. (#76)- Exposure-aware dbt lineage in anomaly alerts — downstream models remain identified separately from dbt exposures, whose labels and owner information are surfaced in the existing alert message without changing notification routing. (#70)
- Selective column profiling in dbt package — the
scherlok_column_metricsmodel now scansgraph.nodesfornull_anomalytest nodes and profiles only the columns that have tests attached, reducing warehouse queries on wide tables. (#71)
Fixed¶
- Clean JSON stdout on dbt early exits —
console.printcalls that leaked Rich markup to stdout on connection failures and missing manifests now useout_errorso they route to stderr in--output jsonmode. (#67)
[0.9.0] — 2026-08-10¶
Added¶
- dbt package — Scherlok is now installable as a native dbt package (
git:install from this repo; the dbt Package Hub listing is pending in dbt-labs/hubcap#456). Includes 4 instant tests (not_null_proportion,row_count_between,recency,unique_proportion) and 2 auto-learning anomaly detection tests (volume_anomaly,null_anomaly) backed by Shewhart control limits. Incrementalscherlok_metricsandscherlok_column_metricsmodels auto-discover and profile all materialized models. One repo, two distribution channels (PyPI + dbt Hub). (#64)
Fixed¶
- mcp 2.0 compatibility —
FastMCPwas renamed toMCPServerin mcp 2.0. Import now tries v2 first, falls back to v1. (#64) - MySQL/DuckDB view discovery —
list_tables()now includes views alongside base tables, matching the behavior of Postgres, BigQuery, and Snowflake connectors. (#63)
[0.8.0] — 2026-07-10¶
Added¶
--explain— Claude-augmented alerts onwatch,ci,check,dbt, anddbt-run-and-watch. When anomalies fire, one Anthropic API call per run (Claude Haiku 4.5 by default,SCHERLOK_EXPLAIN_MODELto override) turns the batch into a root-cause hypothesis — summary, likely cause, up to three diagnostic steps — injected into the Slack (attachment), Discord/Teams (text), generic-JSON (explanationfield), email, and console alerts, plus anexplanationkey ondbt --output json. dbt runs feed upstream lineage frommanifest.jsoninto the prompt so cascading failures trace to the source model. Aggregate data only (anomaly strings, model names, timestamps — never warehouse rows); fail-open by contract: any API failure delivers the unaugmented alert with a one-line(--explain unavailable: …)note, injected text is escaped and truncated per platform so an oversized hypothesis can never get an alert payload rejected, and the alert path is capped at ~30s worst case. Opt-in viapip install scherlok[explain]+ANTHROPIC_API_KEY. Seesrc/scherlok/explainer/README.md. (#58, #59)--explaindemo recording —examples/demo-explain.svg(asciinema-style cast of a real run), embedded in the README section.
0.7.0 — 2026-05-28¶
Added¶
- MCP server —
pip install scherloknow ships ascherlok-mcpstdio server that exposes Scherlok to AI coding agents (Claude Code, Claude Desktop, …) as MCP tools:list_tables,investigate,watch,status,history,check. The connection is resolved server-side (SCHERLOK_CONNECTION/scherlok config) and never passed by the model; every operation is read-only on the warehouse with no arbitrary-SQL tool, and output is bounded. Seesrc/scherlok/mcp/README.md. (#54, #55) server.json+ PyPI ownership marker — repo-rootserver.jsonand anmcp-name: io.github.rbmuller/scherlokmarker in the README so Scherlok can be published to the official MCP Registry (registry.modelcontextprotocol.io). (#56)
Changed¶
mcpis now a core dependency (moved out of the[mcp]extra) so the registry'spip install scherlokinstall path produces a workingscherlok-mcpwithout an extra step. The[mcp]extra is kept as a back-compat alias and still resolves cleanly. This adds ~15 transitive packages (anyio, pydantic, httpx, starlette, uvicorn family) to the base install; the trade was deliberate — first-class agent integration over a lean install.- Extracted the per-table profile-and-detect orchestration into
scherlok.serviceso the CLI and the MCP server share one transport-agnostic core (no behavior change).
0.6.0 — 2026-05-20¶
Added¶
Connectors¶
- MySQL connector —
pip install scherlok[mysql],scherlok connect mysql://user:pass@host:3306/db. Works with MariaDB and other compatible forks. (#45, closes #18) - DuckDB connector —
pip install scherlok[duckdb],scherlok connect duckdb:///path/to/file.db(orduckdb:///:memory:). For local-first analytics workflows. (#50, closes #19)
dbt integration¶
scherlok dbt-run-and-watch— wraps the typical CI sequence (dbt run->scherlok dbt) into one invocation. Streamsdbt runoutput live; ifdbt runfails, exits with the same code WITHOUT running scherlok against a stale or partial manifest. Passes--target,--profiles-dir, and--selectthrough todbt run. Requires thedbtbinary on PATH (dbt-core remains an opt-in dependency). (#40, closes #34)scherlok dbt --output json— emits a single JSON document on stdout (status + per-model anomalies) for CI parsers, with Rich chatter rerouted to stderr so stdout stays machine-readable. (#41, closes #33)scherlok dbt --include-snapshots— opt-in flag to also profile dbt snapshot nodes. Snapshots are SCD Type 2 tables that physically exist in the warehouse and are profilable like any other materialized model. When the flag is unset, behavior is unchanged. (#26, closes #22)- dbt lineage from
manifest.json— newscherlok.dbt.lineagemodule reads the manifest'sparent_map. Two surfaces consume it:scherlok dbt --show-lineage(also ondbt-run-and-watch) prints an ASCII upstream/downstream tree (├──/└──style) under each profiled model; and every anomaly message is suffixed with· Affects N downstream models: a, b, cwhen descendants exist, so webhook and email payloads tell on-call who's about to be paged downstream. Leaf marts get no suffix. (#49, closes #36)
Dashboard¶
- 14-day anomaly trend barchart — per-day severity bars in the HTML report. (#28, closes #20)
- Stale tables panel — surfaces tables whose last profile is older than the freshness threshold, to catch silent ETL failures. (#30, closes #21)
CLI¶
scherlok check— alias forscherlok ci --fail-on critical, the one-liner CI gate. (#44, closes #32)scherlok connectwith no args prints example connection strings for every supported adapter instead of erroring. (#24, closes #23)
Distribution¶
Changed¶
- Release workflow fails if the
linux/amd64image exceeds 100 MiB, so the published image can't silently bloat. (#43)
0.5.0 — 2026-04-30¶
Added¶
- dbt integration v0 — new
scherlok dbtcommand. Readstarget/manifest.json, discovers materialized models (table/incremental/view/materialized_view), auto-resolves the connection fromprofiles.yml(postgres / bigquery / snowflake), and runs investigate + watch per model with dbt-style ✓/✗ output. - Optional dependency:
pip install scherlok[dbt](adds PyYAML) - Flags:
--project-dir,--profiles-dir,--target,--connection-string,--select,--include-sources,--fail-on,--webhook,--email - Supports
{{ env_var('NAME', 'default') }}rendering inprofiles.yml - Requires dbt 1.6+ (manifest schema v10+)
- HTML dashboard — new
scherlok dashboardcommand. Generates a self-contained HTML report (~28 KB) from the local profile store: KPIs, per-table incidents grouped with summary/threshold/first-seen, schema-drift+/−/~diff, sparklines, and history. Auto dark/light theme viaprefers-color-scheme(--theme dark|lightto override). Addsjinja2>=3.0to core dependencies. examples/dbt_smoke/— runnable mini dbt project (3 models on top of the existing seeded Postgres inexamples/docker-compose.yml) for contributors to reproduce the dbt + dashboard flow end-to-end.
Changed¶
--verbose/-vand--quiet/-qglobal flags added to all commands.--quietsilences progress chatter while keeping anomaly results and errors (CI-friendly).--verboseadds per-table profiling timings and column counts.- Connector error messages now include actionable hints.
Failed to connect.is followed by a one-line explanation of what went wrong, plus a hint when applicable. - Postgres: classifies "connection refused" / "auth failed" / "database does not exist" / "SSL required" / timeout / unknown host
- Snowflake: detects missing
SNOWFLAKE_USER/SNOWFLAKE_PASSWORD, missing python connector, auth failures, account/warehouse/database not found - BigQuery: detects missing google-cloud-bigquery, missing Application Default Credentials, permission denied, project/dataset not found, billing not enabled
Fixed¶
- Views were silently skipped by
scherlok investigate/watch/dbton Postgres and Snowflake (list_tablesfiltered toBASE TABLEonly). Materialized dbt views (materialized: view, the default forstaging/models in layered dbt projects) are now discovered. (#7) - Release notes extraction in CI — the awk in
release.ymlwas using regex match against## [version], where[...]is interpreted as a character class. Switched to literal-substring match so v0.5.0 release notes are properly extracted fromCHANGELOG.mdinstead of falling back to the generic placeholder.
0.4.0 — 2026-04-27¶
Added¶
- Snowflake connector (
pip install scherlok[snowflake]) scherlok cicommand — all-in-one CI/CD (connect + watch + exit code) in one line- Email alerter —
scherlok watch --email user@company.com(configure viaSCHERLOK_SMTP_*env vars) - Multi-recipient support:
--emailis repeatable - Issue templates (bug, feature, new connector)
- CHANGELOG.md
- Automatic GitHub Release notes on tag push
0.3.0 — 2026-04-22¶
Added¶
- Generic webhook alerter — auto-detects Slack, Discord, Microsoft Teams, or generic JSON endpoints
--webhook/-wflag in thewatchcommand
Removed¶
- Removed legacy Slack-only path; replaced by the generic webhook
0.2.2 — 2026-04-22¶
Fixed¶
- Sync
__version__betweenpyproject.tomlandsrc/scherlok/__init__.py
0.2.0 — 2026-04-22¶
Added¶
- BigQuery connector (
pip install scherlok[bigquery]) - Detective logo (AI-generated illustration)
- Animated SVG demo in README
- Examples folder with Docker + PostgreSQL quick start
- 34 new tests across store, alerter, config, BigQuery (93 total)
- PyPI release workflow with trusted publishing
- Redesigned README
0.1.0 — 2026-04-15¶
Added¶
- Initial release
- PostgreSQL connector
- 6 anomaly detectors: volume, schema drift, freshness, nullability, distribution, cardinality
- CLI with Typer (
connect,investigate,watch,report,status,history,version) - Local SQLite storage + remote storage (S3, GCS, Azure Blob)
- Slack webhook integration
- 59 unit tests