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 q4-2025.html html 1 page
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. backdraft ls
shows the origin URL for anything fetched.
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 names the page instead of the staged filename. The receipt says what the page said; the link is how a reader asks whether it still says it.
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" # 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
CLI reference
| Command | Does |
|---|---|
init | Create .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. |
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. |
ls | List ingested documents: slug, filename, media type, page count, and the origin URL for anything fetched. |
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. |
session start|show | Ledger 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|show | List 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. |
export | The whole registry as JSON. |
Bind exit codes
| Code | Means |
|---|---|
0 | Every citation resolved. |
1 | Usage or environment error. |
2 | Bind 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
# titlebecomes 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
| File | Is |
|---|---|
memo.md | What the writer wrote, prose plus tokens. Yours; re-bindable. |
memo.backdraft.html | The 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:
| Where | Applies to |
|---|---|
--theme <name|file> | this render |
.backdraft/theme.toml | this project |
~/.config/backdraft/theme.toml | every project, no flag needed (honors XDG_CONFIG_HOME) |
| built-in | everything 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 genericOPENAI_API_KEYin 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.
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.