backdraft

Documentation

Backdraft is three things that ship together: a Python CLI (pip install backdraft) that ingests documents and checks citations, a one-page agent skill that makes your agent write through it, and a self-contained file format for the output. There is no server and no SDK to integrate, the CLI is the whole system. This page covers install, the concepts, every command, and what ends up on disk. The normative format specs live in the repo, and agents should start from llms.txt.

Install

uv tool install backdraft

Python 3.13+. The vision-model extractor ships by default, the recommended path for real PDFs (glossy layouts, info boxes, scans) and the only path for scanned images; it activates only when BACKDRAFT_VLM_API_KEY is set. Without a key, PDFs fall back to the keyless embedded-text layer, and ingest says so. Either way PDFs want poppler on your PATH (brew install poppler) — it renders the pages, which is what puts the cited page into the artifact; without it ingest still works and says what is missing. Spreadsheets (xlsx, xlsm), CSV, Word (docx), PowerPoint (pptx), HTML, text and Markdown are keyless and built in, and sheet evidence carries the workbook's own styling: bold, fills, number formats, column widths. Legacy xls workbooks are read, values only, through the [xls] extra. Slide decks extract text only; a visual-heavy deck is better exported to PDF and ingested through the vision extractor, which captures charts and images.

Web pages

An ingest source can be an http(s) URL, not only a path. Diligence folders contain links, and a link should be as citable as a file:

backdraft ingest https://example.com/reports/q4-2025
q4-2025  https://example.com/reports/q4-2025  html  1 page  18402 chars

The page is fetched once and snapshotted like any other source. Its identity is the sha256 of the bytes fetched at that moment — the URL is provenance riding alongside, not identity — so a page that has since changed comes back as a new generation of the same document and citations into the old one report drifted, exactly as an edited PDF does.

Every surface names a fetched source by its page, which is why the line above carries a URL where a file's carries a filename. The fetch does invent a filename to stage the bytes in — q4-2025.html — but no such file is on anyone's disk, so ingest, ls and backdraft read print the URL in its place rather than beside it: two names for one thing would let the invented one look authoritative.

The origin reaches the artifact as well: a receipt on a fetched page carries the URL as a link and the date the bytes were taken, and the source list shows the URL under the slug you gave the document rather than the name the fetch invented for it. The receipt says what the page said; the link is how a reader asks whether it still says it.

Ingest a stable address where the site offers one. A page that is edited will report drifted on the next bind — correct behavior, and useless as a citation, because the sentence quoted is no longer the sentence there. Wikipedia's permanent link (?oldid=), a DOI, a dated press release, an archived snapshot: each serves one revision's bytes for good. Pair it with --slug. A URL ending /index.php, /view or a bare id has no segment worth naming a document after, so the slug falls back to one built from the host (en-wikipedia-org-index) — which names the site and still not the page, and a slug is permanent once tokens carry it. The demo cites a Wikipedia article this way.

What this does not do, said plainly rather than worked around: JavaScript-rendered pages give you whatever the server returns to a plain GET, pages behind a login are out of reach, and the extractor is a parse rather than a readability guess — navigation and footers are part of the page, because a heuristic that changed its mind between two versions of a site would move anchors. Responses are capped at 32 MiB.

Bring a model provider

The vision extractor needs a key you provide, and it runs only on explicit, backdraft-scoped consent: set BACKDRAFT_VLM_API_KEY in .backdraft/env (written by backdraft init) or the environment. Ambient OPENAI_API_KEY-style variables are deliberately never read.

Under the hood the client is the OpenAI SDK with an injectable base_url, so any OpenAI-compatible provider works. The default is OpenRouter (https://openrouter.ai/api/v1) running google/gemini-3.1-flash-lite-preview, so the simplest setup is an OpenRouter key. To point elsewhere, set BACKDRAFT_VLM_BASE_URL and BACKDRAFT_VLM_MODEL, for example directly at OpenAI or at a local server. [entail] adds the optional model-judge verifier, keyed separately by BACKDRAFT_ENTAIL_API_KEY.

Concepts

The gate

Source documents reach the writer only through read, search and cell, which stamp a token on every span they show and record the showing in a session ledger. The set of citable things is exactly the set of things shown, so a citation to something the writer never saw is a distinguishable failure (not_shown), not an invisible one.

Tokens and receipts

bd:<slug>:<locator>:<hash>: document, place, and a content-hash of the exact text cited. Locators are media-native: p8 (a page), p8.c3 (a paragraph-scale chunk), rent-roll!B10 (a cell). An anchor is not a pointer: it carries the verbatim snippet and its sha256, so the finished artifact is defensible with the registry deleted and the sources gone.

Drift

Re-ingesting identical bytes yields identical tokens. When a source changes, citations into it resolve against the superseded snapshot and report drifted. The artifact shows what was cited and what stands now, as a word-diff.

The workflow

# once per project
backdraft init
backdraft ingest report.pdf model.xlsx notes.md
backdraft session start --id s-deal
export BACKDRAFT_SESSION=s-deal

# read through the gate; every chunk arrives with a token
backdraft read                     # list documents
backdraft read report              # table of contents
backdraft read report p4-6         # pages, ranges, sheet names
backdraft search "24850000"        # hits are citable directly
backdraft cell model "rent-roll!B10"
backdraft show bd:report:p4.c2:7f11 # what does this token say?

# write claims as links, then bind and render
backdraft bind memo.md --check value-trace,overlap
backdraft render memo.md --to html # -> memo.backdraft.html
In practice the reading and writing steps are an agent's: you ask for a cited memo, the backdraft skill imposes the gate, and you receive the artifact.

CLI reference

CommandDoes
initCreate .backdraft/ (registry, credentials template) in the current directory.
ingest <sources>Snapshot sources into the registry, minting every anchor. A source is a path or an http(s) URL. Formats: PDF, XLSX/XLSM, XLS, CSV/TSV, DOCX, PPTX, HTML, images (png, jpeg, tiff), text and Markdown. --extractor auto|vlm|image|pdf-text|xlsx|xls|csv|docx|pptx|html|text, --slug, --config k=v (repeatable). Config keys are declared per extractor and checked against the one that was chosen, so an unknown key fails and names the ones that apply rather than being ignored: PDFs take dpi, snapshot_quality, snapshot_max_height; the vision paths (vlm, image) also take api_key, base_url, model, timeout, retries, and vlm alone takes concurrency (image takes no dpi — there is nothing to rasterize). Every other format reads none. A source that cannot be read does not end the run: the rest of the list is ingested anyway and the command exits 1 printing N of M sources ingested and one line per failure with its reason, so re-running the same list after a fix costs nothing. Each source's line closes with how much text came out and which of three things happened — a document created, a new generation of one whose bytes moved (the moment citations into the previous snapshot begin reporting drifted), or unchanged, a no-op. Under a couple of hundred characters a note names the likely cause and what to do, at exit 0.
snapshot-pages <slug>Backfill page images for an already-ingested PDF, locally, no model calls (needs poppler for rendering). Ingest stores them already — this is for a registry built before it did, or on a machine that had no poppler then.
lsList ingested documents: slug, name, media type, page count. The name is the filename, or — for a source fetched from the web — the URL it came from, standing in the staging filename's place rather than beside it.
read [slug] [selector]The gate: document list, table of contents, or a token-marked page/range/sheet read. Mints what it shows.
search <query>Full-text search over every anchor; hits are citable. --in slug, --limit.
cell <slug> <sheet!REF>…Mint specific cells' tokens directly: token plus verbatim value.
show <token>…The inverse of minting: what a token says. Per token, its bind status, its locator and the verbatim snippet, in argument order. drifted prints the cited snippet and what stands there now; unresolved says whether the slug or the locator is wrong; malformed names the grammar. This is the gate, so what it shows is minted and citable. Exit 1 if any token was unresolved or malformed.
session start|showLedger sessions; export BACKDRAFT_SESSION to enable not_shown detection.
bind <doc.md>Resolve every citation, run --check verifiers, assemble evidence, write the record. --session, --mode frontwalk|backfill, --lean (skip page images), --bound (also write the markdown projection).
render <doc.md>The artifact. --to html|footnotes|json, -o, --theme (see Theming).
theme list|showList the bundled themes and which one is in effect; print one (validated) to stdout. See Theming.
clean [dir]Tidy a working directory: relocate stray records, remove leftover projections. Never touches authored files or artifacts.
exportThe whole registry as JSON.

Bind exit codes

CodeMeans
0Every citation resolved.
1Usage or environment error.
2Bind completed and something did not resolve, the code a CI job or Stop-hook gates on. Verification verdicts never produce it.

Writing rules

Claims are markdown links whose href is the token, copied exactly from gate output; multiple citations are ;-separated in one href:

[net operating income of $1,429,600](bd:t12-summary:p1.c3:f10b)
[EGI of $2,684,400](bd:t12:p1.c2:7f11;bd:model:rent-roll!B11:4b79)
  • Bind the span, not the sentence. The link text is the words the evidence supports.
  • Cite only what you were shown. Never construct or edit a token by hand.
  • Uncited prose is fine for recommendations and framing, cite facts, not opinions.
  • Never fix a failure by deleting its token. A kept failure is the honest outcome; the artifact will show it plainly.
  • An italic line directly under the # title becomes the artifact's subtitle.

Verification

Independent switches, off by default, recorded as graded evidence, never gates. value-trace finds every figure in a claim in its cited source, reading through thousands separators, currency, scale suffixes ($1.4M ≡ 1,400,000), percent-vs-decimal (7.7% ≡ 0.077), accounting negatives and dates; a match that only works after rounding is partial, and a miss names the figure. overlap measures how much of a claim's wording appears in its source (and skips single-cell sources, where the question is meaningless). entail (extra) asks a model whether the source supports the claim. Verdicts appear in the artifact's Record layer in plain language.

Files & the artifact

FileIs
memo.mdWhat the writer wrote, prose plus tokens. Yours; re-bindable.
memo.backdraft.htmlThe deliverable: document, receipts and evidence in one self-contained file. No network (CSP-enforced), nothing to install, degrades to readable footnotes if scripts are stripped.
.backdraft/The machinery: registry, credentials, and the bind record (records/memo.backdraft.json, the machine-readable run, also embedded in the artifact as a JSON island with a self-describing legend).

The artifact embeds only cited evidence, a memo citing ten pages of a gigabyte corpus is a ~2 MB file. Recipients need the one .html file and nothing else.

Theming

The artifact's look is a theme, and a theme is a small TOML file. Three ship — default, press (cream stock, small-caps serif heads) and slate (a sans body, tracked uppercase heads):

backdraft render memo.md --theme slate
backdraft render memo.md --theme ./house-style.toml

To make it stick, put a file where every render will find it. Precedence, first match wins:

WhereApplies to
--theme <name|file>this render
.backdraft/theme.tomlthis project
~/.config/backdraft/theme.tomlevery project, no flag needed (honors XDG_CONFIG_HOME)
built-ineverything else

A sample file — set only the keys you want, the rest stay the built-in default:

# ~/.config/backdraft/theme.toml
name = "house"

[colors]
paper = "#FFFDF8"        # cards, panes, sheet cells
ink = "#241F1A"          # body text
sel = "#1F6F5C"          # the cited cell
alarm = "#9B3524"        # a citation that did not resolve

[fonts]
serif = "Charter, Georgia, serif"     # body text
sans = "Inter, system-ui, sans-serif" # UI text
mono = "'SF Mono', Menlo, monospace"  # code

[headings]
family = "sans"          # serif|sans|mono, or a stack of its own
case = "small-caps"      # none|uppercase|lowercase|small-caps
weight = 600             # 100–900
tracking = ".04em"

You do not have to hunt for a starting file — the CLI hands you one, fully commented, with every key and what it paints:

backdraft theme list                 # the bundled ones, and which is in effect
backdraft theme show default > ~/.config/backdraft/theme.toml
backdraft theme show ./mine.toml     # validates it; prints only what render accepts

That same list of color keys is themes/default.toml, which writes out the built-in look and is the file to copy. Two things worth knowing: serif, sans and mono name roles — body text, UI text, code — not classifications, so a sans-bodied theme sets serif; and [headings] styles the document's own title and section heads, not the small labels around them.

A theme is display only. It cannot change layout, cannot touch a token, a receipt or the record, and cannot make the artifact fetch anything — url() is refused with that reason, since the file's CSP would block the request anyway. Unknown keys and unusable values fail the render with a message naming both, so a typo costs you an error and never a half-styled artifact.

Agents and harnesses

The skill's core instruction is a substitution: for source documents, use backdraft read/search, never raw file reads. The token-efficient reference for any agent is llms.txt (~800 tokens). How the skill reaches your agent depends on the harness.

Claude Code

The repo is its own plugin marketplace, so the plugin route tracks releases:

/plugin marketplace add spencerbraun/backdraft
/plugin install backdraft@backdraft

Or have the CLI copy the skills into your skills directory:

backdraft skill install          # the writing skill, into ~/.claude/skills/
backdraft skill install --all    # plus backfill and artifact-reading
backdraft skill install --project  # into this repo's .claude/skills/

Claude Cowork

Once the plugin is listed in Anthropic's community directory, it installs from Cowork's built-in skills directory; until then, zip a skill folder from the repo's skills/ and upload it under Customize > Skills. Inside a session, run the CLI per-command as uvx backdraft ..., since installs do not persist between sessions.

Sandboxes and credentials: a sandbox usually cannot reach model providers, so ingest inside one falls back to the keyless text layer and says so. For full fidelity, ingest once on your own machine with the vision extractor. The registry lives in .backdraft/ inside the project folder, so it travels with the folder into any session, and binding and rendering never need a key. Put BACKDRAFT_VLM_API_KEY in .backdraft/env yourself; never paste keys into a chat.

Codex, Cursor, Copilot

Agents in this family read skills from ~/.agents/skills:

backdraft skill install --agent codex   # into ~/.agents/skills/

Or commit the skill folders to your repo under .agents/skills/ so every checkout carries them. For Codex cloud environments, add pip install backdraft to the setup script, which runs while the network is still on.

Standing context

For any harness that reads AGENTS.md, paste this into your repo's file:

## backdraft (cited writing)
When a document must cite its sources, write it through the backdraft CLI:
it shows source text with a citation token over every span, and only shown
spans are citable. In a sandboxed session run every command as
`uvx backdraft ...` (no install, no PATH edits). Start from
`uvx backdraft --help`; ground truth is https://backdraft.dev/llms.txt.

Security & privacy

  • Ambient keys are never read. Credentials reach backdraft only via BACKDRAFT_* variables, .backdraft/env, or --config. A generic OPENAI_API_KEY in your shell is not consent to send documents anywhere.
  • Artifacts make no network requests, enforced by a Content-Security-Policy the browser applies, no fonts, no analytics, no phone-home.
  • Everything is local. The only network calls in the system are the optional VLM/entail model calls you explicitly key.
  • Gitignore .backdraft/ for confidential corpora, the registry contains the full text of everything ingested.

FAQ

Ingest says it fell back to the text layer, why?

The note names the condition, usually that BACKDRAFT_VLM_API_KEY isn't set. Fix the named one. Text-layer receipts are fine for clean digital PDFs; glossy or scanned ones deserve the vision model.

Ingest printed a page count but almost no characters.

That is the note doing its job: the source is a shell. A scanned PDF has no text layer for pdf-text to read — ingest it through the vision extractor instead. A web page rendered by JavaScript or sitting behind a login returns a shell to an unauthenticated fetch — save the page from a signed-in browser and ingest the file. Either way the exit code stays 0, because a thin snapshot is a real snapshot; check it with backdraft read <slug> and tell whoever asked, rather than citing what came back.

My artifact has no page images.

Ingest stores them for every PDF, through both the vision and the text-layer path — the latter renders the pages locally, which needs poppler on the machine. If ingest printed a note that it could not capture them, install poppler; then, for that registry or any built before ingest did this, run backdraft snapshot-pages <slug> (local, free) and re-bind. Also check you did not bind with --lean, which skips them deliberately.

What does not_shown actually catch?

A real token, valid in the registry, that the writing session was never shown, i.e. the writer cited something it didn't read. Only a session ledger makes this class of failure visible; that is why the skill starts one.

Can a reader trust an artifact without installing backdraft?

That's the design goal: the receipts, the evidence, and a machine-readable record with its own decoding legend are inside the file. The artifact spec lists the checks a skeptic can run with nothing but the file.

Is this fact-checking?

No, provenance. Backdraft proves where a claim came from and shows you the source; the optional verifiers add deterministic evidence like "this figure appears in that cell." Judgment stays with the reader.