BundledResearchVersion 1.2.0

Grounded Citations: Verifiable 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 documentation

Grounded Citations is a bundled Hermes Agent skill that attaches numbered, verifiable citations to anything the agent writes from fetched material. A ledger script owns the mapping from URL to citation number, so the model only ever emits small integers it was handed rather than URLs it might misremember. Reach for it whenever an answer or a file on disk rests on outside information the reader may want to check.

What it does

The skill produces Perplexity-style attribution: every claim drawn from an outside source gets an inline bracketed number, and the document ends with a Sources: list resolving those numbers to URLs. The important part is where the numbers come from. A Python script, scripts/sources.py, keeps a ledger of every URL registered during a task and hands back the id for each one. The model writes [1] or [2], never a URL, so a hallucinated link cannot survive into the deliverable.

That same ledger scales up into a fact-checking chain for work where the reader must be able to audit the reasoning. You can attach verbatim quotes to individual sources, and a quote is rejected unless it literally appears in the fetched page text. Claims that come from model knowledge rather than retrieval get flagged [unverified]. A verification pass with --evidence fails any draft whose cited sources carry no supporting quote.

The skill covers chat answers, written documents in markdown, PDF, docx and slides, and research reports. It does not cover academic BibTeX pipelines. For conference papers you want the arxiv skill, which this skill feeds; the details live in references/citation-formats.md.

Before you start

There is nothing to install beyond the standard toolset. scripts/sources.py is stdlib-only Python 3, so it runs on the platforms the skill declares: linux, macos and windows.

Retrieval itself comes from whatever is configured in your Hermes Agent setup: web_search, web_extract, browser_navigate, or terminal with curl and other CLIs. The skill does not supply a search backend; it supplies the bookkeeping layer on top of one.

The ledger lives at $HERMES_HOME/cache/citations/ledger.json and is profile-aware. You can override the location per task with --ledger or globally with HERMES_CITATION_LEDGER. That override matters more than it looks: it is how you keep separate tasks from contaminating each other's numbering, and how you point parallel subagents at a shared ledger.

How to run

The entry point is a single script. Set a shell variable for it once and the rest of the commands stay short.

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

reset clears the ledger so a new task starts from [1]. add registers a URL and returns its id. list shows the current table. render emits the Sources block. verify checks a draft against the ledger.

One property of add is worth internalising: it is idempotent and URL-normalized. The same page always returns the same id within a ledger, so ids stay stable across many rounds of searching and extracting. You can register a URL you have already seen without worrying about duplicate entries or shifting numbers.

Quick reference

ActionCommand
Fresh ledger for a new tasksources.py reset
Register a source, get its idsources.py add [--title T]
Register several at oncesources.py add ...
Register from JSON tool outputsources.py ingest results.json
Attach verbatim evidence to a sourcesources.py quote --text "exact wording" --from page.txt
Show ledgersources.py list [--json]
Render the Sources blocksources.py render [--style markdown|plain|footnotes|bibtex|evidence] [--only 1,3]
Render only what a draft citessources.py render --cited-in draft.md
Rewrite a draft's Sources block in placesources.py render --replace-in draft.md
Check a draft's citationssources.py verify draft.md [--strict] [--min-coverage 0.6] [--evidence]

Procedure

The workflow has six steps, and the order is the point. Registering sources after writing prose is the failure mode the skill exists to prevent.

1. Reset the ledger

At the start of a task that will produce a grounded answer or document, run reset. Skip the reset when you are continuing work whose ids already appear in a draft; 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, or pipe the raw JSON through sources.py ingest. Do this before writing prose. Registering later, from memory, is exactly the reconstruction-from-recall that the ledger removes.

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]

The formatting rules are strict and small. No space before the bracket. Each id in its own brackets. A maximum of three ids per sentence. Cite per sentence rather than dumping everything at the end. Use only ids the ledger returned; never invent an id or a URL. Claims from your own knowledge get no citation at all. When sources conflict, present both readings, each with its own id. Quote exact figures, dates and names as the source states them, and flag gaps explicitly, for example "no source found for X", instead of smoothing them over.

4. Append the Sources block

Use sources.py render --cited-in so the id-to-URL mapping is generated mechanically from the ledger rather than retyped. For non-markdown targets, pick the matching --style and follow references/citation-formats.md for placement: footnotes in docx, endnotes in PDF and LaTeX, a Sources slide in decks, per-page source lists in wiki output.

5. Verify before delivering

Run sources.py verify . 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 and re-run.

6. Chat answers

Chat answers follow the same steps with the draft in your reply: register sources, cite inline, 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.

Multi-platform sweeps

A request like "what are people saying about X" or "research X across the web" is not one web_search. It is a fan-out across source types, collected in parallel, then synthesised with every claim attributed to the platform it came from.

Source typeRouteWhat it adds
Open webweb_search → web_extractofficial docs, articles, announcements
Community discussionreddit-reading (search, thread)real user experience, complaints, workarounds
Blogs / releases / changelogsrss-feeds (read, discover)dated primary posts, version history
Videoyoutube-contentwalkthroughs, demos, talks
Codeterminal with gh search repos / gh search issuesimplementations, open bugs
X/Twitterxurl (needs API access)announcements, developer chatter

The reddit-reading and rss-feeds skills are optional. If they are absent, install them with hermes skills install official/social-media/reddit-reading or hermes skills install official/research/rss-feeds before using them.

Register every URL from every route in the ledger as it arrives, which is step 2 again. Keep opinion and measurement apart: a Reddit thread is evidence that users report something, not that it is true, so pair it with a primary source or label it as sentiment. Report per-platform coverage gaps, for instance "Reddit search returned nothing newer than March", rather than silently narrowing the report to whatever worked.

Fact-checking mode

For work where the reader must be able to check the chain, including medical, legal, financial and safety topics, disputed claims, or any request that explicitly asks for fact-checking, upgrade from citations to evidence. Four steps.

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 match is insensitive to whitespace, case and markdown markup, so inline links such as _[ERAP1](https://…)_ in extracted text match the plain prose a reader sees. That means a paraphrase or a misremembered figure cannot masquerade as evidence. Copy and 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 in your quote.

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, and a fact-check deliverable dominated by [unverified] markers should say so in its summary.

3. Cross-check disputed facts against a second independent source

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, then source, then exact supporting text, with nothing taken on faith. Use --replace-in to rewrite an existing Sources block in place; it is idempotent and safe to re-run after attaching more quotes. --cited-in prints to stdout instead. Both emit the heading ## Sources, while --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, headings (#), table rows (|) and fenced code are dropped, and 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: a quick syntax or version lookup in the middle of coding, casual conversation, creative writing. Mention a URL only if the user would plausibly want the link.

For academic BibTeX pipelines, use the arxiv skill instead. This skill feeds it rather than replacing it, and the citation format details are in references/citation-formats.md.

Limits and gotchas

The pitfalls below are the ones that actually bite in practice.

  • Registering after writing. The ledger must be populated from tool output, not reconstructed from the draft. Doing it the other way reintroduces exactly 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. Run reset only between tasks.
  • Retyping URLs into the Sources block. Always render. A hand-typed URL is an unverified claim.
  • Citing a search snippet as if you read the page. A web_search description supports only what it literally says. Cite the extracted page when the claim needs the body, so web_extract it 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 --ledger or HERMES_CITATION_LEDGER if 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_extract first, save the text, then quote --from that 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 that verify then flags.

Verification

python "$S" verify report.md --strict --min-coverage 0.5

A green result means three things: 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 somewhere during editing.

What pairs with it

The skill is designed to sit inside a research workflow rather than stand alone. The related skills are arxiv, pdf, reddit-reading, rss-feeds and youtube-content. arxiv handles the academic paper pipeline this skill feeds. pdf covers document extraction. reddit-reading and rss-feeds supply the community and dated-primary-source routes in a multi-platform sweep, and both are optional installs. youtube-content covers walkthroughs, demos and talks, which are often the only place a technique is demonstrated rather than described.

Skills the docs pair this with

More Research skills