Bind specs to code. Lint for drift.
Any markdown file in your repo can declare anchors to code — specific files or AST symbols. When bound code changes, drift lint flags the spec as stale. Agents that change code must update the specs they affect.
zig build -Doptimize=ReleaseSafe --prefix ~/.localInstall the CLI and the agent skill:
zig build -Doptimize=ReleaseSafe --prefix ~/.local
npx skills add fiberplane/driftThe skill teaches Claude Code how to maintain drift anchors — when to link, when to update specs, and how to handle stale anchors. Once installed, you can tell Claude to add drift to your existing docs:
"Add drift anchors to docs/auth.md for src/auth/login.ts and src/auth/provider.ts#AuthConfig"
"Run drift lint and fix any stale specs"
"Write a spec for the payment module and bind it to the relevant source files"
Claude will run drift link to stamp provenance and keep specs in sync as it makes code changes. When drift lint is in CI, stale specs block merges — so the agent can't silently break documentation.
Write a markdown spec, then bind it to code:
drift link docs/auth.md src/auth/login.ts
drift link docs/auth.md src/auth/provider.ts#AuthConfig
drift link adds the anchor to the spec's YAML frontmatter and auto-appends provenance (current git HEAD or jj change ID). You can also reference code inline — @./src/auth/provider.ts#AuthConfig in the spec body — and drift link will stamp those with provenance too.
Check if specs are fresh:
drift lint
Refresh all anchors in a spec after updating it:
drift link docs/auth.md
After linking, your spec has frontmatter anchors and (optionally) inline references:
---
drift:
files:
- src/auth/login.ts@qpvuntsm
- src/auth/provider.ts#AuthConfig@qpvuntsm
---
# Auth Architecture
Users authenticate via OAuth2. The validation flow uses @./src/auth/provider.ts#AuthConfig@qpvuntsm ...Each anchor carries provenance via an @change suffix — a snapshot of which VCS change you last reviewed that file at. Provenance is per-anchor, so different files track independently.
If you don't want frontmatter visible in render frontends like GitHub, wrap it in an HTML comment instead — drift picks it up the same way:
<!-- drift:
files:
- src/auth/login.ts@qpvuntsm
- src/auth/provider.ts#AuthConfig@qpvuntsm
-->drift lint Check all specs for staleness (exits 1 if stale)
drift status Show all specs and their anchors
drift link Add an anchor to a spec (auto-appends provenance)
drift unlink Remove an anchor from a spec
For each anchor with provenance, drift compares the file's content at the provenance revision against its current content. If they differ, the spec is stale. This is content-based — git rebases that don't change content won't trigger false positives, and jj rewrites that do change content won't slip through.
For symbol-level anchors (#AuthConfig), drift parses the file with tree-sitter and compares just the symbol's AST node. Changes elsewhere in the file don't trigger staleness.
Staleness is reported with a reason:
$ drift lint
docs/auth.md
STALE src/auth/provider.ts#AuthConfig
changed after spec
STALE src/core/old-module.ts
file not found
docs/payments.md
ok
1 spec stale, 1 ok
Reasons include:
- changed after spec — the bound file (or symbol) was modified after the spec
- file not found — the bound file no longer exists
- symbol not found — the bound symbol no longer exists in the file
drift lint exits 1 when any spec is stale, so it works as a CI gate:
# .github/workflows/drift.yml
name: Drift
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: fiberplane/drift@main
- run: drift lintfetch-depth: 0 is required — drift needs VCS history to compare content at provenance revisions. The setup action auto-detects platform and downloads the right binary from GitHub releases.
git and jj. Auto-detected from .jj or .git directory. In jj, the @change provenance suffix stores stable change IDs that survive rewrites.
Requires Zig 0.15.2. The repo includes a .tool-versions file for mise (or asdf). If you haven't already, activate mise in your shell, then:
mise install # installs zig 0.15.2
zig build test # run tests
zig build -Doptimize=ReleaseSafe # build release binaryEnable the pre-push hook to run build, lint, and tests before every push:
git config core.hooksPath hooks