Grounded Citations: Cite and Verify Sources in Hermes Agent
Ground answers and documents in cited, verifiable sources.
Written by Neura Market from the official Hermes Agent documentation for Grounded Citations. Commands, paths, and version numbers are reproduced from the source unchanged.
Read the official documentationGrounded Citations is a bundled Hermes Agent skill that turns every fetched fact into a numbered, verifiable citation. You register sources as you retrieve them, cite them inline while drafting, and let a script render the final Sources block. It is for anyone who writes reports, briefs, or chat answers that rest on outside information and wants the reader to be able to check every claim.
What it does
The skill runs on a small Python script, sources.py, that keeps a ledger mapping each URL to a stable integer id. When you add a source, the script returns the next number, and you drop that number into your text as [1], [2], and so on. The model never guesses a URL or an id; it only uses what the ledger handed it. At the end, the script renders the Sources: block mechanically from the ledger, so the mapping is always accurate.
Beyond simple citations, the same ledger supports a fact-checking mode. You attach a verbatim quote to each source, and the script rejects any quote that does not literally appear in the fetched page text. Claims that come from model knowledge rather than a source get flagged [unverified]. A verification command then fails the draft if any cited source lacks evidence or if the prose is too thinly cited.
The skill covers chat answers, markdown documents, PDFs, docx files, slides, and research reports. It does not handle academic BibTeX pipelines; for conference papers you should use the research-paper-writing skill, which this skill feeds.
Before you start
There are no special prerequisites beyond the standard Hermes Agent toolset. The script scripts/sources.py is stdlib-only Python 3, so it runs anywhere Python runs. Retrieval comes from whatever is already configured: web_search, web_extract, browser_navigate, or terminal with curl or other CLIs.
The ledger lives at $HERMES_HOME/cache/citations/ledger.json and is profile-aware. You can override the location per task with --ledger or the HERMES_CITATION_LEDGER environment variable.
How to run
The script is the single entry point. Set a shell variable to its path, then call it with subcommands:
S=~/.hermes/skills/research/grounded-citations/scripts/sources.py
python "$S" reset # start a clean ledger
python "$S" add https://example.com/a --title "A" # prints: [1]
python "$S" add https://example.com/b --title "B" # prints: [2]
python "$S" list # ledger table
python "$S" render # Sources: block
python "$S" verify draft.md # catch bad citations
The add command is idempotent and URL-normalized: the same page always returns the same id within a ledger, so ids stay stable across many search and extract rounds. That stability is what lets you register sources incrementally without worrying about collisions.
Quick reference
The table below summarizes the main actions. You will use reset once per task, add after every retrieval, and render plus verify before delivering.
| Action | Command |
|---|---|
| Fresh ledger for a new task | sources.py reset |
| Register a source, get its id | sources.py add [--title T] |
| Register several at once | sources.py add ... |
| Register from JSON tool output | sources.py ingest results.json |
| Attach verbatim evidence to a source | sources.py quote --text "exact wording" --from page.txt |
| Show ledger | sources.py list [--json] |
| Render the Sources block | sources.py render [--style markdown|plain|footnotes|bibtex|evidence] [--only 1,3] |
| Render only what a draft cites | sources.py render --cited-in draft.md |
| Rewrite a draft's Sources block in place | sources.py render --replace-in draft.md |
| Check a draft's citations | sources.py verify draft.md [--strict] [--min-coverage 0.6] [--evidence] |
Procedure
1. Reset the ledger
Start a fresh ledger at the beginning of any task that will produce a grounded answer or document. If you are continuing work whose ids are already in a draft, skip the reset; reusing the ledger keeps the numbering stable.
2. Register every source at retrieval time
After each web_search, web_extract, browser_navigate, or fetch, pass the URLs to sources.py add. If the tool returns raw JSON, pipe it through sources.py ingest. Do this before writing any prose. Registering later from memory is the failure mode this skill exists to prevent.
3. Write cite-while-drafting
Place the bracketed id or ids immediately after each sentence the source supports:
Ice floats because it is less dense than liquid water.[1][2]
Follow these rules:
- No space before the bracket; each id goes in its own brackets.
- Maximum 3 ids per sentence. Cite per sentence, not one dump at the end.
- Only use ids the ledger returned. Never invent an id or a URL.
- Claims from your own knowledge get no citation.
- Conflicting sources: present both readings, each with its own id.
- Quote exact figures, dates, and names as the source states them. Flag gaps explicitly, for example "no source found for X", instead of smoothing them over.
4. Append the Sources block
Run sources.py render --cited-in so the id-to-URL mapping is generated mechanically from the ledger, not retyped. For non-markdown targets, pick the matching --style and follow references/citation-formats.md for placement: footnotes in docx, endnotes in PDF or LaTeX, a Sources slide in decks, per-page source lists in wiki output.
5. Verify before delivering
Run sources.py verify on the draft. It exits non-zero on unknown ids, on a Sources block that disagrees with the ledger, or, with --min-coverage, on prose that is too thinly cited. Fix any issues and re-run until it passes.
6. Chat answers
Chat answers follow the same steps with the draft in your reply: register sources, cite inline, and end with the rendered Sources: list. For a short answer you can render the block from sources.py render --only instead of writing to a file.
Fact-checking mode
For work where the reader must be able to check the chain, such as medical, legal, financial, safety, disputed claims, or when the user asks for fact-checking, upgrade from citations to evidence.
1. Attach a verbatim quote per source
After extracting a page, save its text to a file and attach the sentence or sentences that carry each claim:
python "$S" quote 1 --text "Ice is about 9% less dense than liquid water." --from page1.txt
The quote is rejected unless it appears verbatim in the evidence text. The matcher is insensitive to whitespace, case, and markdown markup, so inline links like _[ERAP1](https://…)_ in extracted text match the plain prose a reader sees. A paraphrase or misremembered figure cannot masquerade as evidence. Copy-paste from the fetched text; never retype. Quote the sentence as the reader sees it, because the matcher sees through the extractor's markup for you, so you do not have to reproduce link syntax or escaped asterisks.
2. Flag model-knowledge claims with [unverified]
A load-bearing claim you could not source gets an explicit marker instead of a citation:
The refactor likely predates the 2.0 release.[unverified]
verify --min-coverage counts [unverified] sentences as covered. The goal is declared provenance for every claim, not a citation on every sentence. If a key claim can be checked, check it. [unverified] is for what genuinely cannot be sourced. A fact-check deliverable dominated by [unverified] markers should say so in its summary.
3. Cross-check disputed facts
When two sources disagree, cite both readings with their own ids and quotes, and say which you weight and why. One source is reporting; two independent sources are corroboration.
4. Verify with the evidence gate and render the evidence block
python "$S" verify report.md --evidence --min-coverage 0.5
python "$S" render --style evidence --replace-in report.md
--evidence fails the draft if any cited source has no attached quote. The evidence render style prints each source's quotes beneath its URL, so the deliverable shows claim, source, and exact supporting text with nothing taken on faith. Use --replace-in to rewrite an existing Sources block in place; it is idempotent, so it is safe to re-run after attaching more quotes. --cited-in prints to stdout instead. Both emit the heading ## Sources; --style plain emits Sources:.
What --min-coverage counts
Coverage is sentences with declared provenance / prose sentences. A prose sentence is a non-empty line fragment of 4+ words after the Sources block, with headings (#), table rows (|), and fenced code dropped; blockquote markers are stripped. Provenance is declared by either a [n] citation or an [unverified] marker, so a sentence carrying both counts once. Run verify without a threshold first and read the info: stats: line to see the counts before picking a number.
When not to use it
Skip inline citations when the retrieval is incidental to another task, such as a quick syntax or version lookup mid-coding, casual conversation, or creative writing. Mention a URL only if the user would plausibly want the link. Also skip this skill for academic BibTeX pipelines; use research-paper-writing instead.
Limits and gotchas
The source lists several pitfalls that trip up new users:
- Registering after writing. The ledger must be populated from tool output, not reconstructed from the draft, because that reintroduces the hallucinated-URL risk the numbering removes.
- Renumbering mid-task. Never hand-edit ids in a draft. Ids are ledger identities; if a draft cites
[4],[4]must stay that source. Runresetonly between tasks. - Retyping URLs into the Sources block. Always use
render. A hand-typed URL is an unverified claim. - Citing a search snippet as if you read the page. A
web_searchdescription supports only what it literally says. Cite the extracted page when the claim needs the body;web_extractit first. - Over-citing. Three ids on a sentence is the ceiling. A citation on every clause makes text unreadable and hides which source carries the load.
- Citing the ledger in code or config artifacts. Source comments belong in prose deliverables and doc headers, not inside generated code.
- Parallel subagents. Each subagent has its own working directory. Point them all at one ledger with
--ledgerorHERMES_CITATION_LEDGERif their outputs get merged, otherwise their ids will collide. - Quoting from a snippet instead of the page. Evidence quotes must come from the extracted page text, not a search-result description.
web_extractfirst, save the text, thenquote --fromthat file. - Paraphrasing into
quote --text. The verbatim check will reject it. The fix is to find the actual sentence, not to reword until something matches. - Using
[unverified]as an escape hatch. It marks the rare claim that genuinely cannot be sourced. If most sentences carry it, the task needed more retrieval, not more markers. - Hand-editing the Sources block. Use
render --replace-in. Slicing the file yourself risks a stale or duplicated block thatverifythen flags.
Verification
A full verification run looks like this:
python "$S" verify report.md --strict --min-coverage 0.5
Green means every [n] in the draft exists in the ledger, the Sources block lists exactly the cited ids with the ledger's URLs, and the cited share of source-bearing sentences meets the threshold. Read the warnings even when the exit code is 0; uncited registered sources usually mean a claim lost its attribution during editing.
What pairs with this
The skill feeds directly into research-paper-writing, which handles the academic BibTeX pipeline. For literature discovery, pair it with arxiv. For pulling text out of PDFs and scanned documents before you quote it, use ocr-and-documents. Together these cover the full research workflow from retrieval to a cited, verifiable deliverable.