drft
A structural integrity checker for markdown directories. drft treats a directory of markdown files as a dependency graph -- files are nodes, links are edges -- and validates the graph against configurable rules.
Install
The binary is called drft.
Quick start
# Check a directory for issues (no setup required)
# Initialize a config file
# Snapshot the current state
# Check for staleness (files changed since last lock)
# Verify lockfile is current (for CI)
What it does
drft discovers markdown files, extracts links between them (inline, reference, image, autolink, frontmatter, wikilink), and builds a dependency graph. It then validates that graph against a set of rules:
| Rule | Default | Description |
|---|---|---|
broken-link |
warn | Link target does not exist |
cycle |
warn | Circular dependency detected |
directory-link |
warn | Link points to a directory, not a file |
stale |
warn | Dependency changed since last lock |
containment |
warn | Link escapes the scope boundary |
encapsulation |
warn | Link reaches into a sealed scope's non-manifest files |
orphan |
off | File has no inbound links |
indirect-link |
off | Link target is a symlink |
Commands
drft check
Validate the graph against all enabled rules.
drft lock
Snapshot file hashes and the dependency graph to drft.lock. This enables staleness detection -- when a file changes, its dependents are flagged.
drft graph
Export the dependency graph.
drft impact
Show what depends on the given files (transitively).
drft init
Create a default drft.toml config file.
Configuration
drft.toml in the scope root:
# Glob patterns for files to exclude from discovery
= ["drafts/*", "archive/*"]
# Rule severities: "error", "warn", or "off"
[]
= "error"
= "error"
= "warn"
= "warn"
# Per-rule path ignores
[]
= ["README.md", "CLAUDE.md"]
# Custom rules (scripts that receive graph JSON on stdin)
[]
= "./scripts/max-fan-out.sh"
= "warn"
drft automatically respects .gitignore.
Scopes
A directory with a drft.lock becomes a scope. Child directories with their own drft.lock are child scopes -- they're checked independently with their own config.
project/
drft.lock # root scope
drft.toml
index.md
docs/
overview.md
research/
drft.lock # child scope (frontier node in parent)
drft.toml
overview.md
internal.md
Use --recursive to check or lock all scopes in one command. Use --manifest to seal a scope and control which files are visible to the parent.
Output
Text format (default):
error[broken-link]: index.md -> gone.md (file not found)
warn[stale]: index.md (stale via setup.md)
warn[cycle]: cycle detected: a.md -> b.md -> c.md -> a.md
JSON format (--format json):
Every JSON diagnostic includes a fix field with actionable instructions.
DOT format (--format dot) for graph export:
digraph
Graph JSON follows the JSON Graph Format specification.
Exit codes
| Code | Meaning |
|---|---|
| 0 | Clean (warnings may be present) |
| 1 | Rule violations at error severity, or lock --check found lockfile out of date |
| 2 | Usage or configuration error |
Custom rules
Custom rules are scripts that receive the dependency graph as JSON on stdin and emit diagnostics as newline-delimited JSON on stdout:
[]
= "./scripts/max-fan-out.sh"
= "warn"
#!/bin/sh
# Flag nodes with more than 5 outbound links
See examples/custom-rules/ for complete examples.
Security note: Custom rules execute arbitrary shell commands defined in drft.toml. Review the [custom-rules] section before running drft check in untrusted repositories, the same as you would review npm scripts or Makefiles.
LLM integration
drft is designed to work with LLMs as both a validation tool during editing sessions and a CI gate.
JSON output
All commands support --format json. The check command returns a summary envelope:
Every diagnostic includes a fix field with actionable instructions an LLM can follow directly.
Impact analysis
Before editing a file, check what depends on it:
Claude Code hooks
Add to .claude/settings.json to run drft automatically after markdown edits:
CI
Both exit with code 1 on failure.
License
MIT