DriftLock turns local engineering intent into agent context, ESLint feedback, and CI checks so AI-assisted TypeScript changes cannot silently drift away from critical sources of truth.
AI coding agents are fast, but they do not naturally know which local rules are critical. A small refactor can hardcode a price, bypass an input schema, weaken a checkout flow, or keep an import around while the returned value no longer comes from the intended source.
DriftLock makes those rules explicit and checkable:
@driftcontracts describe local intent and sources of truth.drift-lock context --taskgives agents relevant constraints before they plan.drift-lock checkvalidates supported invariants in CI.drift-lock explainturns violations into actionable diagnostics.@drift-lock/eslint-pluginbrings the same feedback into the developer loop.
DriftLock does not replace tests or code review. It adds a deterministic layer for the local product and engineering intent that agents often miss.
DriftLock is developed under the same constraints it creates for other AI-assisted TypeScript projects. This repository uses its own contracts, committed index, executable invariants, ESLint plugin, CI checks, and agent-facing workflows so product friction is discovered here before it reaches users.
See docs/dogfood.md for the public dogfooding philosophy and the target state for making this repository DriftLock's most complete practical demo.
DriftLock requires Node.js 22 or newer.
Run the interactive installer from the package manager you use in the project:
# npm
npx --yes @drift-lock/cli@latest install
# pnpm
pnpm dlx @drift-lock/cli@latest install
# bun
bunx @drift-lock/cli@latest install
# yarn
yarn dlx -p @drift-lock/cli@latest drift-lock installThe installer detects your package manager, creates .drift/config.json,
generates the .drift/contracts.generated.index store, adds DriftLock scripts, installs
@drift-lock/cli and @drift-lock/eslint-plugin as dev dependencies, and can
configure ESLint, GitHub Actions, and agent skills.
If you prefer to install the package first, add the CLI as a dev dependency and then run the local command:
# npm
npm install -D @drift-lock/cli
npm exec drift-lock -- install
# pnpm
pnpm add -D @drift-lock/cli
pnpm exec drift-lock install
# bun
bun add -d @drift-lock/cli
bunx drift-lock install
# yarn
yarn add -D @drift-lock/cli
yarn drift-lock installUseful installer options:
# Scan a specific source directory
npx --yes @drift-lock/cli@latest install --source src
# Install bundled agent skills for a provider
npx --yes @drift-lock/cli@latest install --agent openai
npx --yes @drift-lock/cli@latest install --agent claude
npx --yes @drift-lock/cli@latest install --agent cursor
# Add GitHub Actions CI
npx --yes @drift-lock/cli@latest install --ci github
# Add the bundled Next billing example
npx --yes @drift-lock/cli@latest install --example next-billing
# Skip ESLint or CI when you do not want them
npx --yes @drift-lock/cli@latest install --no-eslint --no-ci
# Preview changes without writing files or installing packages
npx --yes @drift-lock/cli@latest install --dry-runAfter install, run DriftLock through your package manager's local binary runner:
npm exec drift-lock -- check
pnpm exec drift-lock check
bunx drift-lock check
yarn drift-lock checkThe installer also adds package scripts such as drift-lock:check, so npm run drift-lock:check and pnpm drift-lock:check work too.
Add a @drift contract next to code that must preserve a local source of truth:
import { isCheckoutInput } from '@/features/billing/billing.schema';
import { BILLING_PRICES } from '@/features/billing/pricing';
/* @drift
version: 1
id: billing.create-checkout-session
scope: declaration
stability: locked
intent: >
Create a Checkout session for the Pro subscription while respecting the billing sources of truth.
ssot:
pricing: "@/features/billing/pricing.ts"
schema: "@/features/billing/billing.schema.ts"
invariants:
- id: checkout-price-from-pricing
enforce: drift/ssot-flow
ssot: pricing
sinks:
- return.priceId
- return.amount
- return.currency
- id: validates-input
enforce: drift/ssot-usage
ssot: schema
llm:
must_not_change:
- pricing source
- accepted input shape
- checkout flow
*/
export async function createCheckoutSession(input: unknown) {
if (!isCheckoutInput(input)) {
throw new Error('Invalid checkout input');
}
const payload = input;
const price = BILLING_PRICES[payload.plan];
return {
priceId: price.priceId,
amount: price.monthlyAmount * payload.seats,
currency: price.currency,
};
}Then extract the committed baseline:
drift-lock extractCommit the .drift/contracts.generated.index directory so locked contract changes can be
detected in CI.
drift-lock install
drift-lock context <file>
drift-lock context --task "<user prompt>"
drift-lock extract
drift-lock check
drift-lock check --changed
drift-lock check --changed --git-base origin/main
drift-lock check --adoption-mode warn
drift-lock coverage
drift-lock coverage --json
drift-lock diff --summary
drift-lock diff --summary --json
drift-lock proof --git-base origin/main
drift-lock proof --git-base origin/main --format json
drift-lock explain [contract-id]
drift-lock accept <contract-id> --reason "<reason>"
drift-lock skills list
drift-lock skills install --provider openaiinstall sets up DriftLock in the current project. It creates .drift, adds
package scripts, installs the CLI and ESLint plugin, and can configure CI or
agent skills.
context <file> renders the contracts that matter for one file. Use it before
editing risky code so an agent sees the local intent, sources of truth, and
invariants.
context --task "<prompt>" prepares contract-aware context for a planned AI
change. It is the best starting point before asking an agent to implement a
feature.
extract scans the source directory and writes the .drift/contracts.generated.index store.
Commit this directory so locked contract changes can be detected later.
check validates contract syntax, locked baselines, required-contract coverage,
and supported invariants. Run it locally and in CI.
check --changed focuses validation on contracts changed since the Drift index.
Add --git-base <ref> in PR workflows to check files changed since a branch.
coverage reports how many contracts exist and which required files are still
missing contracts. Use --json for agent-readable output.
diff --summary summarizes contract changes for review. It is useful before a
PR or before accepting an intentional contract change.
proof --git-base <ref> composes changed checks, contract diffs, acceptances,
and coverage into a non-blocking pull request proof report. Use --format json
for CI or app integrations.
explain [contract-id] turns current failures into actionable diagnostics. Use
--json when CI or an agent needs stable fields.
accept <contract-id> --reason "..." records an intentional locked contract
change. The reason must be explicit and should describe the product decision.
skills list shows bundled DriftLock agent skills. skills install --provider openai|claude|cursor installs them for your agent environment. Use
--drift-command "<command>" when skills should call a repo-specific wrapper.
For the AI-assisted workflow, run context before planning:
drift-lock context --task "add yearly billing plan"Then plan and implement against the listed constraints, and finish with
drift-lock diff --summary plus drift-lock check.
For a PR workflow, run:
drift-lock diff --summary
drift-lock check --changed
drift-lock diff --summary --git-base origin/main
drift-lock check --changed --git-base origin/main
drift-lock proof --git-base origin/main
drift-lock accept billing.create-checkout-session --reason "Intentional billing contract change"Use accept only for intentional locked contract changes with a clear product
reason. Add --git-base <ref> in PR workflows when you want Drift to focus on
files changed since a Git base instead of the whole extracted index.
When a check fails, run:
drift-lock explain
drift-lock explain billing.create-checkout-session
drift-lock explain --jsonUse --json when an agent or CI step needs stable diagnostic fields such as the
contract id, invariant, sink, reason, found expression, and suggested fix.
DriftLock reads .drift/config.json:
{
"version": 1,
"source": "src",
"index": ".drift/contracts.generated.index",
"requireContracts": [
"src/features/**/actions.ts",
"src/services/**/*.ts"
],
"adoption": {
"mode": "warn"
}
}source may also be an array for monorepos that need to scan multiple package
source roots without including tests or fixtures.
Source discovery honors a project-root .driftignore when present; otherwise it
falls back to .gitignore. The two files are not merged. DriftLock always
ignores internal state, build, and dependency directories such as .git,
.drift, .next, dist, and node_modules.
requireContracts is optional and defaults to []. When set, drift-lock check
reports DRIFT015_REQUIRED_CONTRACT_MISSING for matching source files that do
not contain any valid @drift contract. adoption.mode controls whether those
required-contract gaps are reported as audit, warn, or blocking enforce
diagnostics; other Drift violations remain blocking. You can also override it for
a single check with drift-lock check --adoption-mode warn. Use drift-lock coverage
to review required files that are still uncovered before turning patterns into
blocking CI policy. See docs/config.md for the config reference.
coverage also reports local disable directives so adoption exceptions stay
visible without changing check behavior yet:
// drift-lock-disable-next-line drift/ssot-flow -- reason: migration billing-v2, expires: 2026-07-01
// drift-lock-disable-file drift/import-boundary -- reason: generated adapterDisable directives are reporting-only in this release; malformed or expired
directives are listed by drift-lock coverage but do not affect exit codes.
DriftLock ships an ESLint 9 flat config plugin:
import driftLock from '@drift-lock/eslint-plugin';
export default [
{
plugins: { 'drift-lock': driftLock },
rules: {
...driftLock.configs.recommended.rules,
},
},
];The recommended config enables:
drift-lock/valid-contract
drift-lock/no-locked-contract-change
drift-lock/ssot-usage
drift-lock/ssot-flowGenerate a GitHub Actions workflow during install:
npx --yes @drift-lock/cli@latest install --ci githubOr add the checks manually:
drift-lock check
eslint .DriftLock V1 catches supported forms of:
- invalid
@driftcontract syntax or schema - locked contract changes without explicit acceptance
- missing usage of declared sources of truth
- return fields, nested return paths, and supported collection sinks that no longer derive from a declared source of truth
The practical failure mode is simple: if an agent replaces a declared pricing
source with a hardcoded local object, drift-lock check can fail before that
change merges.
V1 is intentionally narrow:
- TypeScript and TSX files
- contracts written as
/* @drift */block comments scope: filefor context, locked baselines, anddrift/ssot-usagescope: declarationfor function-level flow checksstability: draftandstability: lockeddrift/ssot-usagedrift/ssot-flowfor declaration-scoped function return-object flows with explicitreturn.<path>sinks
drift/ssot-flow does not try to prove arbitrary program correctness. The
supported local subset includes nested return paths, const aliases, simple
branches, simple destructuring, resolvable spreads, direct array.map
collection sinks with one non-terminal [] segment, import aliases, namespace
imports, and verified helper summaries.
Unsupported or ambiguous patterns still fail clearly rather than create a false
sense of safety: mutations and reassignments, unverified helper calls, raw or
dynamic spreads, complex destructuring, callback-heavy collection chains,
default imports, computed namespace access, and any drift/ssot-flow invariant
declared on a scope: file contract.
@drift-lock/cli CLI and installer
@drift-lock/core Parser, extractor, context, and checks
@drift-lock/eslint-plugin ESLint 9 flat config pluginThis repo includes a small Next.js demo that shows the main V1 proof:
pnpm --filter next-v1 drift-lock:context
pnpm --filter next-v1 drift-lock:check
pnpm --filter next-v1 lintSee apps/next-v1/README.md for the full scenario.
DriftLock is complementary to rule files such as AGENTS.md, CLAUDE.md,
Cursor rules, or provider-specific instructions. Those files describe global
team preferences and workflows. DriftLock targets a different layer: local
product and engineering intent attached to code that carries risk.
The difference is enforcement. A rule file can tell an agent what to do. A DriftLock contract can be extracted into context and then checked deterministically, so the codebase can fail when a critical invariant is silently bypassed.
Install dependencies:
pnpm installBuild and verify the workspace:
pnpm build
pnpm check
pnpm testRun the demo checks:
pnpm --filter next-v1 drift-lock:context
pnpm --filter next-v1 drift-lock:check
pnpm --filter next-v1 lint