Save it to a file, always. The file is the artifact — it survives the session, it can be diffed against a later run, and it is what gets published.
-
Write the complete markdown to the reports directory, creating it if it doesn't exist:
$BULLSHIT_DETECTOR_REPORTS when that variable is set, otherwise ~/.bullshit-detector/reports/<YYYY>/.
The file name is bs-report-<slug>-<YYYY-MM-DD>.md, where <slug> is a short kebab-case form
of the content's title (bs-report-claude-situation-shitshow-2026-07-30.md).
Not the temp directory. Reports are meant to be re-read, diffed against a later run and
compared across releases, and none of that survives a temp sweep — macOS runs a cleaner nightly
and prunes old files. A report that quietly evaporates after a few days is not an artifact.
Point $BULLSHIT_DETECTOR_REPORTS at a git repo if you want them versioned.
If the home directory isn't writable — a sandboxed environment, a locked-down host — fall back
to the platform temp directory and say so in your reply, because then the file dies with
the session and the user needs to save it themselves.
-
Never overwrite. If the path exists, append -2, -3, … Re-running the same content on the same day produces a second reading, and comparing them is the point — silently clobbering the first destroys the evidence that verdicts move between runs.
-
Always end your reply with the full file path on its own line, whichever output mode you used.
-
If writing fails, say so plainly and print the report inline rather than losing it.
Then check it with the script — do not count the table by hand:
uv run <detector-skill-dir>/scripts/tally.py <the-file-you-just-wrote> \
--source /tmp/bs-source-<slug>-<YYYY-MM-DD>.md
Pass --source — it is the file you saved in step 1, and it lets the script check
that every span you put in quotation marks is words the content actually contains. Omit
it and that check silently does not run, which is the one failure a fact-checking tool
cannot survive: a verdict rendered against words the speaker never said.
<detector-skill-dir> is wherever this skill is installed — ~/.claude/skills/bullshit-detector
under the usual layouts. The bare scripts/tally.py written here previously resolved from
nowhere and cost a real run a failed invocation.
Write the run record first, then let the script write both derived lines. The record is the
raw material: the two timestamps, the query log, the counts only you can know. Everything the
report states about the run is computed from it.
- Write the run record beside the report — same path with
.md swapped for .run.json. The
schema and the fields that are easy to get wrong are in RUN-RECORD.md;
read it when you write the record, not before. Two things you need while still running,
because they shape what you must have kept: log every search query as you issue it (a list
rebuilt from memory at the end is wrong in the direction that flatters the run), and log
every source you could not reach, with the claim it would have supported.
- Run
tally.py --fix. It writes the tally line and the run line, recounts every row, and
verifies the version stamp, the linked source, the origin markers and the claim numbering.
Exit 2 means the report is non-compliant: fix what it names and re-run until it exits 0.
Run it the moment the table and the record exist, and let its output be the first time the
count is checked at all. Do not audit the table yourself first. The script is not confirming a
number you already worked out — it is the number, and a hand recount before the call is work
the call was built to make unnecessary. Instrumented across 35 runs: 16 of them passed the gate
with zero rejections and still spent a median 52 seconds — up to 257 — deliberating before
asking, 1,173 seconds in total across the corpus, all of it spent re-deriving what the script
returns for free.
If the same rejection comes back twice, stop re-running and go read the line it names. Six
runs on record re-ran the gate against rejections that repeated verbatim — one burned 525
seconds, 89% of it deliberating, on three rejections it had already been given once. A repeated
rejection means the edit did not land, or landed somewhere else; the script will keep saying so
as long as you keep asking. Open the file at that line, read what is actually there, and fix
that.
--fix also corrects the record's own derived counts — claims.extracted, claims.checked,
claims.dropped_ambiguous and wall_seconds — from the table and your two timestamps, so those
four are not worth getting exactly right by hand either. See RUN-RECORD.md.
If you wrote a shell and a claims file, compose before you gate:
uv run <detector-skill-dir>/scripts/tally.py <report.md> --compose <report>.shell.md
It renders the tables from the claims file, then counts the rendered rows with the same
parse the gate uses, so the tally line cannot disagree with the table above it. Exit 2
means a claim line is invalid — it names the line; fix the claims file and re-compose,
never the rendered report. Then run the --fix + --source gate on the composed report
exactly as described here.
Do not hand-write either line. Both are pure functions of the claims table and the record —
the tally line's buckets and the footer's searches, tools, coverage, wall clock and
per claim arithmetic. Every one of those has been typed wrong in a shipped run: 35 searches
against 40 logged, 21 against 29, a 40-row table miscounted by 2 and then by 8 while the analysis
in those same runs was sound. Attention goes to the argument and the bookkeeping rots behind it,
so a number that can be computed is never typed. If the script declines to write the footer it
says which field the record is missing — supply the field, don't write the line yourself.
If you cannot write the record, skip it: the footer then has no source, and a footer you invent
is worse than one that is absent.
When anything was unreachable, say so in the report too, as one line under the tally —
tally.py rejects a record that lists unreachable sources against a report that never mentions them:
Unreachable: 4 sources — 3 paywalled, 1 blocked. Named in the rows that needed them.