A Claude Code skill and slash command that catches when a fact you cite has changed at the source, verifies your local citation still matches the baseline, shows you what is now out of date, and fixes scoped files once you approve.
Most page monitors stop at "this page changed." fact-drift reads the actual value, checks the files where you cite it, and updates approved stale citations after you sign off.
Your content quotes numbers from other people's websites: a government limit, a quota, a price, a deadline. Those numbers change, and nobody tells you. The source page updates, your article does not, and you find out when a reader does. Tools that watch pages can say "this page changed," but they cannot tell you the number went from 60 to 80, or which of your files now needs fixing.
Anywhere your content repeats a fact that lives on a page you do not control:
- Help guides and knowledge bases that quote government rules, fees, or deadlines, like visa, tax, or benefits guides. When a threshold changes, the skill flags the guide and offers to fix it.
- Developer docs that pin versions ("requires Node 20+", "tested on Python 3.12"). Keep install and compatibility notes in step with upstream releases.
- Pricing and comparison pages that cite a competitor's price or plan limits. When they change their pricing, your "X vs Y" page stops being quietly wrong.
- Compliance and policy pages that track regulations: tax rates, minimum wage, filing deadlines, reporting limits.
- Affiliate and review roundups that cite specs, prices, or ratings from product pages, the kind that goes stale the moment a product changes.
- API and integration docs that reference a third party's rate limits, endpoints, or end-of-life dates.
A run that catches a change looks like this:
# fact-drift snapshot, 2026-05-28
| Target | Status | Current | Baseline | Source |
| python-latest-stable | OK | 3.14.5 | 3.14.5 | python.org |
| node-latest-stable | DRIFT | 26.1.0 | 24.9.0 | nodejs.org |
| kubernetes-latest-stable | OK | 1.36.1 | 1.36.1 | kubernetes.io |
DRIFT node-latest-stable 24.9.0 -> 26.1.0
Found "24.9.0" in 2 files:
docs/install.md:12
README.md:40
Update both to "26.1.0"? [y/n]
Install as a plugin (recommended). In Claude Code:
/plugin marketplace add https://github.com/hwajongpark/fact-drift
/plugin install fact-drift@fact-drift
Or install as a standalone skill:
git clone https://github.com/hwajongpark/fact-drift
cp -r fact-drift/skills/fact-drift ~/.claude/skills/fact-drift
mkdir -p ~/.claude/commands
cp fact-drift/commands/fact-drift.md ~/.claude/commands/fact-drift.mdThen point it at your sources and run it:
# Copy the example config from the repo into your project, then add your targets
cp examples/rules.config.example.json ./rules.config.json
# Optional: sanity-check the config before the first run
node validate-config.js rules.config.json
# Capture a baseline (first run)
/fact-drift --update-baseline
# Later, check for drift
/fact-driftYou give it a list. Each item is one fact: the web page it lives on, a plain-English note for what to pull ("find the latest version number"), and the files where you cite it.
When it runs, it opens each page, extracts the value with an exact source quote, compares the value to what it saw last time, and checks that your configured citation files still contain the expected value. If something changed, it shows you the old value, the new value, the source quote, and every scoped file that still has the old one. You check that list, and if it looks right, it updates the approved matches. Nothing changes until you say yes.
Under the hood it checks all the pages at once, so a long list still finishes fast.
One entry per fact. The included examples/rules.config.example.json tracks the latest stable versions of Python, Node.js, and Kubernetes, each read from the project's own release page, so it runs out of the box.
{
"targets": [
{
"id": "python-latest-stable",
"name": "Python latest stable release",
"url": "https://www.python.org/downloads/",
"extract": "Find the latest stable Python release offered on the downloads page (for example 3.14.5). Return only the version string.",
"citation_files": ["docs/requirements.md"],
"notes": "Our setup docs pin a minimum Python version; bump them when a new stable lands."
}
]
}Three optional per-target fields:
"cadence": "monthly" | "quarterly" | "semiannual": how often the target is worth re-checking. Omit it and the target is checked on every run. A not-yet-due target is reported asSKIPPEDand not fetched;/fact-drift --allchecks everything regardless. The last check date lives in the target's baseline file, and a target only goes back to sleep when it comes backOK, so an unresolved drift keeps re-appearing until you deal with it."manual_check": true: for sources automated fetch can never read reliably (bot walls that block datacenter IPs, geo blocks, broken TLS, login-only portals). The target still gets attempted, but a failure reportsMANUAL_CHECKinstead ofERROR, never fails a scheduled run, and is listed in every report as a standing reminder to verify the source in a browser. Without this, one permanently unreachable page keeps your scheduled check red forever and you stop trusting it."listing_url": see "A pinned announcement can be quietly superseded" under Design Decisions.
Validate any config with the bundled dependency-free checker:
node validate-config.js rules.config.jsonWant a harder example? examples/advanced-korea-gov.config.json tracks government pages. Some of those need a full browser, so it also shows manual_check and cadence in action.
Plain words, not code, to find the value. I tell it what to look for in plain English, like "find the latest version number." The other option is to point at an exact spot in the page's code. That is faster, but it breaks the moment someone redesigns the page, even when the number is still sitting right there. Plain words keep working through a redesign. Worth the small extra cost.
Point at the owner of the fact. Track the page that owns the fact, not an aggregator that repeats it. A wiki, a news article, or a roundup post is someone else's citation of the number, and it can lag or misquote; the issuing project's release page, the agency's own announcement, the vendor's own pricing page is where the number changes first and cannot be wrong about itself. The demo configs all point at owning sources for exactly this reason.
A pinned announcement can be quietly superseded. Some facts live in dated announcements: this year's quota, this cycle's fee schedule. The trap is that the old announcement page never changes, so a plain check reports OK forever while a newer announcement has replaced the number. For targets like that, set listing_url to the board or news index the announcements are posted on; the checker makes one extra same-site fetch to confirm your pinned page is still the newest, and reports DRIFT when it has been superseded.
It remembers the last value, so it can spot a change. To know a number changed, you have to know what it was before. The first run writes down each value, where it found it, and the exact source quote. Every run after that compares now against last time. No memory, no way to catch a change. In CI, commit fact-drift-baseline/ or restore it before running the check.
It checks your content even when the source has not changed. A source can still match the baseline while your docs drifted by hand. OK means the upstream value matches the baseline and your configured citation scope still contains that value.
It does the fixing, you do the deciding. The point is not to hand you a to-do list, it is to do the boring work. When a value changes, it finds stale matches in the citation scope, prepares the edits, and applies them once you approve. It does the finding and the typing; you make the call.
It always shows the plan before it touches anything. Nothing changes until you have seen exactly what will change. A number like "60" can mean five different things across your files, and you are the one who knows which should actually update. So it searches the configured citation scope, shows you every match first, and lets you drop the ones that should stay.
It runs free on Claude Code. It uses the Claude Code plan you already have, so every check costs nothing and anyone with Claude Code can run it. The catch: it reads pages the simple way, so pages that need a full browser (heavy JavaScript, logins) will not work, and it says so plainly. Most pages are fine.
The trick is not the check, it is everything around it. Honestly, underneath this is just "go read a page," something you could do by hand. What makes it useful is the rest: a fixed list so you never forget a number, a saved history so it can spot changes, scoped edits after approval, and a check you can put on a schedule so drift gets caught without you remembering. It turns "remember to check this and fix it where cited" into a single yes.
fact-drift is a command, so you schedule it the way you schedule any command: point a timer at it. A weekly GitHub Action that runs the check and fails the job if any target is NEW, DRIFT, ERROR, or CONTENT_MISMATCH (MANUAL_CHECK and SKIPPED never fail it):
# .github/workflows/fact-drift.yml
name: fact-drift
on:
schedule:
- cron: "0 8 * * 1" # 08:00 UTC every Monday
workflow_dispatch: {}
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# The runner starts with no skills installed, so install fact-drift first.
- name: Install the fact-drift skill and command
run: |
git clone --depth 1 https://github.com/hwajongpark/fact-drift /tmp/fact-drift
mkdir -p ~/.claude/skills ~/.claude/commands
cp -r /tmp/fact-drift/skills/fact-drift ~/.claude/skills/fact-drift
cp /tmp/fact-drift/commands/fact-drift.md ~/.claude/commands/fact-drift.md
# Requires committed fact-drift-baseline/ files, or a previous step that restores them.
# Auth: set an API key or OAuth token per Claude Code's headless/CI docs.
# The permissions flag is fine here because the runner is disposable.
- name: Run the check
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: npx @anthropic-ai/claude-code --dangerously-skip-permissions -p "/fact-drift --ci"
# `claude -p` exits 0 whenever the session completes, whether or not the check
# found drift, so the verdict cannot ride on the agent's exit code. Instead,
# --ci ends by writing a machine-readable status file, and this plain shell
# step fails the job from that data. If the run died before writing the file,
# `test -f` fails the job too, so the check fails closed.
- name: Fail on drift
run: |
test -f fact-drift-snapshots/ci-status.json
node -e "const s=require('./fact-drift-snapshots/ci-status.json'); if(s.failing.length){console.error('Not OK: '+s.failing.join(', '));process.exit(1)}; console.log('All targets OK')"Three things to know before you rely on it:
- Schedule the check, approve the fixes. fact-drift never edits a file without showing you the plan first, so an unattended run detects drift and fails/reports it; you run
--applyto approve the edits. Fixing a target hands-off is a choice you make per target, not the default. - Keep the baseline available to CI. If CI starts without
fact-drift-baseline/, targets areNEW, not verified. Commit the baseline directory or restore it before the check. - Only pages that load as plain text are good schedule targets. A page behind heavy JavaScript or a login returns ERROR on the cheap path. For a page that will never fetch cleanly from a runner (bot wall, geo block, broken certificate), mark it
manual_check: true: it stops failing the schedule but stays in every report as a reminder to re-check it by hand, or run a real browser for it (a separate path).
- It does not change anything without your approval. You see every edit before it touches a file, and you can drop any of them.
- It does not handle pages behind a login, or pages that need heavy JavaScript to load. If a page will not load as plain text, that target reports ERROR (or MANUAL_CHECK if you opted the target in) and needs a real browser, which is a separate path.
- It does not read PDFs. A fact that lives inside a PDF needs a different source page that carries the same number, or a
manual_check: truetarget as a standing reminder to check the PDF by hand. - It does not commit to git. It edits the files and updates the baseline; committing is yours.
- It does not treat an unreadable page as a valid baseline. Unreadable and not-found results fail closed.
Contributions are welcome. Bug reports (a page it misreads) and useful real-world target configs help most. If you have a config for a public source worth tracking, a pull request adding it alongside examples/advanced-korea-gov.config.json gives the next person a head start. Please run node validate-config.js over any config you add; CI runs it over the examples on every push.
