This is the flagship, hands-on tutorial. By the end you will have:
- Built a tiny sample repository from scratch.
- Configured Driftlock with
.driftlock.toml. - Made a real signature change, watched the commit get blocked, reviewed the auto-fix, and committed cleanly.
- Used the
driftlock:ignoreannotation to keep an internal helper out of the check. - Run Driftlock the way CI runs it, with
driftlock check --base.
You only need Git, a shell, and the driftlock binary on your PATH (see Getting started to install), plus an LLM API key.
Throughout, terminal transcripts show $ for commands you type. Output is realistic but your LLM's exact wording will vary.
$ mkdir drift-demo && cd drift-demo
$ git init
Initialized empty Git repository in /home/you/drift-demo/.git/Create a source directory and a tiny "calculator" module:
$ mkdir srcsrc/calc.go:
package calc
// Add returns the sum of two integers.
func Add(a int, b int) int {
return a + b
}And a README.md that documents it:
# calc
A tiny calculator library.
## API
### Add
```go
func Add(a int, b int) int
```
Returns the sum of `a` and `b`.Commit the starting point so we have a clean baseline:
$ git add src/calc.go README.md
$ git commit -m "initial calc module"
[main (root-commit) 9f1c2ab] initial calc module
2 files changed, 12 insertions(+)Run the interactive setup. For this tutorial we map src/** to README.md and use an OpenAI-compatible provider (OpenRouter here — see Providers for others):
$ driftlock init
Driftlock interactive setup
Press Enter to accept the default value shown in [brackets].
── Documentation mapping ──
Source file patterns (space-separated) [src/**]: src/**
Documentation files or directories (space-separated) [README.md docs/]: README.md
── LLM provider ──
Driver (openai-compatible / ollama) [ollama]: openai-compatible
Full endpoint URL [http://localhost:11434]: https://openrouter.ai/api/v1/chat/completions
Model name [codestral:22b]: deepseek/deepseek-chat
API key (or ${ENV_VAR}) []: ${DRIFTLOCK_API_KEY}
── LLM options ──
Temperature [0]: 0
Max tokens [2048]: 2048
── Behavior ──
Auto-fix documentation on drift (y/n) [y]: y
Block commit when docs are outdated (y/n) [y]: y
Block commit when LLM is unreachable (y/n) [n]: n
Max LLM retries [2]: 2
Send full diff to LLM (uses more tokens) (y/n) [n]: n
── Solana audit (optional) ──
Enable Solana audit logging (y/n) [n]: n
Driftlock initialized successfully.
A .driftlock.toml has been created, the pre-commit hook is active,
and .driftlock.toml and .driftlock/ have been added to .gitignore.The resulting .driftlock.toml:
[[doc_mapping]]
sources = ["src/**"]
docs = ["README.md"]
[llm]
driver = "openai-compatible"
endpoint = "https://openrouter.ai/api/v1/chat/completions"
model = "deepseek/deepseek-chat"
api_key = "${DRIFTLOCK_API_KEY}"
[llm.options]
temperature = 0.0
max_tokens = 2048
[behavior]
auto_fix = true
block_on_false = true
max_retries = 2
include_full_diff = false
block_on_llm_error = false
[audit]
solana = falseExport your key:
$ export DRIFTLOCK_API_KEY="sk-or-v1-your-real-key"Edit src/calc.go to add a third parameter — a real change to the public API surface:
package calc
// Add returns the sum of the given integers.
func Add(a int, b int, c int) int {
return a + b + c
}Notice that README.md still documents the two-argument form. That is drift.
$ git add src/calc.go
$ git commit -m "calc: Add now takes three integers"
driftlock: checking staged changes...
src/calc.go → README.md
modified: func Add(a int, b int, c int) int
verdict: FALSE — README documents Add with two parameters, but the
signature now takes three (a, b, c).
auto_fix is on: Driftlock rewrote README.md to match the new signature.
Review the changes, stage README.md, and commit again.
Commit blocked.Nothing was committed. Driftlock parsed the old and new content of src/calc.go, diffed the signatures by name, found Add modified, mapped src/calc.go to README.md, extracted just the section mentioning Add ("smart chunking"), and asked the LLM whether the docs still match. The verdict was FALSE, so — because auto_fix = true — it rewrote that section and blocked the commit for your review.
$ git diff README.md ### Add
```go
-func Add(a int, b int) int
+func Add(a int, b int, c int) int-Returns the sum of a and b.
+Returns the sum of a, b, and c.
Only the `Add` section changed — the rewritten chunk was stitched back into the full document by exact heading match, so the rest of `README.md` is untouched. See [Architecture](./architecture.md) for how chunk-in / stitch-out works.
### Step 1.6 — Stage the fix and commit cleanly
```bash
$ git add README.md
$ git commit -m "calc: Add now takes three integers"
driftlock: checking staged changes...
src/calc.go → README.md
modified: func Add(a int, b int, c int) int
verdict: TRUE — documentation matches the new signature.
All documentation is in sync.
[main 4b7d10e] calc: Add now takes three integers
2 files changed, 4 insertions(+), 3 deletions(-)
The second check hits the LLM again only if the (model, diff, doc) tuple changed — which it did, because you edited the doc. See Caching.
What did NOT trigger? If you had only edited the body of
Add(say,return a + b + c→return c + b + a), changed a comment, or reformatted whitespace, Driftlock would have found no signature change and let the commit through untouched. Only the public API surface triggers a check.
Suppose you add an unexported helper that you deliberately do not want to document, plus an exported function you do. Driftlock only triggers on exported symbols, but sometimes you want to suppress a specific declaration explicitly — for example, an exported-but-internal function, or to silence a symbol you have decided not to document. That is what driftlock:ignore is for.
Edit src/calc.go:
package calc
// Add returns the sum of the given integers.
func Add(a int, b int, c int) int {
return sum(a, b, c)
}
// Sum is an exported helper we do NOT want tracked in the docs.
// driftlock:ignore
func Sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
func Debug(v int) int { return v * -1 } // driftlock:ignoreTwo forms of the annotation are shown, and both work in any language's comment syntax:
- Standalone — a line whose only content is the comment
// driftlock:ignore, placed directly above a declaration. It ignores the declaration on the next line (Sumabove). - Inline — the marker in a trailing comment on the same line as the declaration (
Debugabove). It ignores only that declaration.
Commit it:
$ git add src/calc.go
$ git commit -m "calc: add ignored helpers"
driftlock: checking staged changes...
src/calc.go → README.md
added: func Add(...) (unchanged)
Sum and Debug suppressed by driftlock:ignore.
All documentation is in sync.
[main a19f77c] calc: add ignored helpers
1 file changed, 15 insertions(+)Sum and Debug never entered the signature set, so no drift was reported and no doc rewrite happened. See Ignoring symbols for more examples across languages and for scoping doc_mapping so unrelated files never trigger a check in the first place.
CI does not have a staging index — it compares two commits. driftlock check in range mode does exactly that, and it never writes files (no auto-fix in CI; the check either passes or fails).
Let's simulate a pull request locally. Create a feature branch and drift the docs again:
$ git checkout -b feature/multiply
Switched to a new branch 'feature/multiply'Add a new exported function to src/calc.go without documenting it:
// Multiply returns the product of two integers.
func Multiply(a int, b int) int {
return a * b
}Because auto-fix runs in the pre-commit hook, commit with a skip so we get a drifted commit onto the branch to test CI behavior (in real life a teammate might commit past the hook, or edit code on the web UI):
$ git add src/calc.go
$ DRIFTLOCK_SKIP=true git commit -m "calc: add Multiply (docs pending)"
[feature/multiply 7c2a5e1] calc: add Multiply (docs pending)
1 file changed, 5 insertions(+)Now run the check exactly as CI would — comparing the branch tip against main:
$ driftlock check --base main --head HEAD
driftlock: comparing main..HEAD
src/calc.go → README.md
added: func Multiply(a int, b int) int
verdict: FALSE — README does not document Multiply.
Documentation is out of sync.
$ echo $?
1driftlock check exited non-zero, which is what fails the CI job. Note it did not modify README.md — check is read-only by design.
While you are still bringing existing docs into sync, you may not want CI to be red on day one. Add --report to always exit 0 while still printing the findings:
$ driftlock check --base main --head HEAD --report
src/calc.go → README.md
added: func Multiply(a int, b int) int
verdict: FALSE — README does not document Multiply.
Documentation is out of sync (report-only; not failing).
$ echo $?
0Add --json to emit a report to stdout for other tools to consume:
$ driftlock check --base main --head HEAD --json{
"in_sync": false,
"results": [
{
"source": "src/calc.go",
"doc": "README.md",
"changes": [
{ "kind": "added", "signature": "func Multiply(a int, b int) int" }
],
"verdict": false,
"reason": "README does not document Multiply."
}
]
}The exact JSON shape is produced by the tool; treat the fields above as illustrative and inspect real output in your pipeline.
Document Multiply in README.md, or let the local hook do it for you by running driftlock fix (which force-regenerates all mapped docs for staged files) and committing:
$ git add src/calc.go
$ driftlock fix # regenerate mapped docs for staged files
$ git add README.md
$ git commit -m "calc: document Multiply"Re-run the check:
$ driftlock check --base main --head HEAD
driftlock: comparing main..HEAD
All documentation is in sync.
$ echo $?
0Green. To wire this into an actual GitHub pull request, see CI/CD — including the crucial fetch-depth: 0 checkout setting so both refs exist in CI.
- Driftlock triggers only on structural (exported signature) changes, never on bodies, comments, or formatting.
- The pre-commit hook can auto-fix and blocks the commit for your review;
driftlock checkin CI is read-only and just passes or fails. driftlock:ignore(inline or standalone) removes a declaration from analysis.--reportturns a failing check into a passing, informational one for gradual adoption;--jsonmakes it machine-readable.- Repeated identical checks are cached and cost nothing extra (Caching).
Continue with Configuration to fine-tune behavior, or Providers to pick a cheaper/faster model.