TLDR: Drift anchors your markdown specs to source code using tree-sitter and git. When the anchored code changes,
drift checkcatches it in CI via AST fingerprinting.curl -fsSL https://drift.fp.dev/install.sh | sh
Spec-driven development is so hot right now. Some people are saying it might be the future of development. Well, we don’t know what the future holds but we’ve been writing some specs and we noticed they go stale quickly. So we built a tool to keep them fresh.
Since starting fp.dev we’ve put a lot of effort into making our codebase agent-optimized and self-driving without rapidly accumulating technical debt. We’ve wholly embraced tools like Effect, for its explicitness, ast-grep for structural linting to enforce our code shape and rules, and many integration and end-to-end tests.
However, one problem remained. We drive most of fp.dev development with fp itself, but some internal docs still need to live in the codebase: key technical decisions and explanations of more complex parts (like our sync or extensions systems). In a fast-moving codebase, these docs quickly go stale.
To avoid spec drift and context rot, we want to make sure these docs stay accurate as code changes underneath them. Existing approaches — doc tests (Rust, Python), snippet embedding tools, a few commercial products — all focus on keeping code examples in sync, not the prose that describes them. You can throw an LLM at it, but burning tokens every commit to answer “did this doc go stale?” is expensive for what should be a trivial check.
So we built drift.
How drift works
Drift works by anchoring your spec files to code. An anchor is simply a small piece of markdown frontmatter you add to your spec file that looks like this
// docs/auth.md
---
drift:
files:
- src/auth/login.ts@a1b2c3d
- src/auth/provider.ts#AuthConfig@a1b2c3d
---
# Auth Architecture
// ...
or this:
// docs/auth.md
Users authenticate via OAuth2. The validation flow uses
@./src/auth/provider.ts#AuthConfig@a1b2c3d
// ...
Both styles are treated identically — you can mix them in the same file.
Anchor anatomy
Every anchor has three parts: path, symbol, and provenance.
src/auth/provider.ts #AuthConfig @a1b2c3d
└── file path ──────┘ └─ symbol ─┘ └ provenance ┘
- Path (required) — the file you’re binding to, relative to the repo root.
- Symbol (optional) — a
#Namesuffix that narrows the anchor to a specific declaration (function, class, type, interface) inside the file. Drift parses the file with tree-sitter, finds the matching AST node, and only tracks changes to that symbol. The rest of the file can change freely without triggering staleness. - Provenance (optional) — an
@<git-sha>suffix recording which commit last addressed this anchor. This is how drift knows when you last reviewed the code. Without it, drift falls back to the last commit that modified the spec file itself.
You don’t need to write provenance by hand. drift link stamps it automatically.
How staleness detection works
Every drift check run reads specs, parses referenced files on demand, queries git, and reports. For each anchor, drift asks: has the bound code changed since the provenance commit?
- Look up the anchor’s baseline — the provenance commit if present, otherwise the last commit that touched the spec file.
- Get the file (or symbol) content at that baseline via
git show <baseline>:<file>. - Compare it against the current version.
- If different → STALE. If the same → ok.
The comparison is syntax-aware for all supported languages (TypeScript, Python, Rust, Go, Zig, Java) — both file-level and symbol-level anchors. Drift parses the code with tree-sitter and hashes a normalized AST fingerprint (node kinds + token text, no whitespace or position data). Reformatting a file won’t trigger a false positive. For unsupported languages, drift falls back to raw content comparison.
What a stale report looks like
$ drift check
docs/auth.md
STALE src/auth/provider.ts#AuthConfig (changed after spec)
changed by mike in e4f8a2c (Mar 15)
"refactor: split auth config into separate concerns"
ok src/auth/login.ts
1 spec stale, 0 ok
Drift tells you which anchors drifted, why, and who changed them — so you know exactly where to look.
Typical workflow
- Write a spec — a markdown file describing some part of your system.
- Bind it to code — run
drift link docs/auth.md src/auth/provider.ts#AuthConfig. This adds the anchor to your spec’s frontmatter and stamps the current commit as provenance. - Code evolves — someone (or an agent) modifies
AuthConfig. - CI catches it —
drift checkexits 1. The spec is stale. - Update the spec — review the code change, update the spec prose, then run
drift linkagain to refresh the provenance. Lint passes.
Importantly, drift helps with detection, not the review itself. Nothing stops you from re-linking without updating the spec prose — drift link just stamps new provenance. The workflow assumes that when you re-link, you’ve actually reviewed the code change and confirmed the spec still matches. The CI gate makes drift hard to ignore, but the review is on you or your agent.
The real value isn’t the tool, it’s the constraint. Agents are prolific code changers but terrible at knowing what else their change affects (I had to delete 3 safeParseJson functions from a PR yesterday). A CI gate that says “you touched AuthConfig, now update the spec that describes it” makes it easier to keep internal documentation up to date.
The lesson: build or use more tools that can yell at your agents.
Drift is open source — github.com/fiberplane/drift. You can get started by running:
curl -fsSL https://drift.fp.dev/install.sh | sh
and installing a coding agent (Claude Code, Codex, Cursor) skill so they know how to use it:
npx skills add fiberplane/drift