Skip to main content
llmwiki rm undoes an ingest: it deletes a source file, deletes the concept pages that source exclusively owns, and leaves any page another live source also contributed to untouched. There is no confirmation flag - a bare rm applies immediately - so --dry-run is the only pre-flight check available.

Basic usage

Run it from your project root, the same as any other llmwiki command.
No LLM provider credentials are required. rm never calls the chat model, and it does not need a working embeddings backend either - see Embeddings after a removal.

Flags

What gets deleted

A kept page’s body is a merge of contributions from every source that produced it, including the one just removed - the file itself carries no record of that. So rm records each kept slug in state.json’s frozenSlugs, the same marker compile already sets when it notices a deleted source. The mark tells the next compile to rebuild that page cleanly from the sources that survive, dropping the removed source’s contribution and its citations rather than leaving them in place. A page no live source owns any more is orphaned instead. The mark is cleared only once the replacement page is actually written. If extraction or page validation fails during that rebuild, the mark and the surviving sources’ ownership are both preserved, so a later compile tries again rather than leaving the page stale with nothing to say so.

Limitations

rm only ever acts on the concepts namespace. On a project using a Configurable Lifecycle Profile (for example newsroom or autosci), rm removes the source file and any wiki/concepts/ pages derived exclusively from it - exactly as described above - but it does not remove, count, or list typed entity pages elsewhere in wiki/ (for example wiki/articles/*.md).Typed entity pages are never recorded against the source they came from, so rm has no way to find or clean them up - and it does not refuse to run on a profile project, since a profile project can still legitimately have its own concept pages to delete. Every rm on a profile project prints this warning instead, regardless of whether the removed source actually produced any typed entity pages:
If the removed source produced any typed entity pages, find and delete them yourself.

Naming the source

Pass the filename under sources/, with or without the .md suffix:
To find the exact filename, look in the sources/ directory of your project. rm matches on filename only. You cannot pass the original URL or local path you ingested. If <source> matches neither a file under sources/ nor an interrupted removal, rm changes nothing and exits 1.

Example

Drop the flag to apply:
The embeddings refresh reports its own outcome separately - see Embeddings after a removal - rather than being folded into that summary line, so a failure there can never be masked by a blanket success message.

What rm reports but doesn’t fix

A removal can damage three things elsewhere in the project. rm reports all three without editing anything for you:
  • A broken wikilink. Deleting a page can leave a surviving page’s [[wikilink]] pointing at something that no longer exists. Reported as the surviving page’s path and the doomed target. Run llmwiki lint for the full detail.
  • A pending review candidate that references the removed source. Reported by count. Run llmwiki review list to see it.
  • A stale citation. A kept page’s body is a merge of every contributing source, so it can still carry a ^[<source>.md] marker pointing at the file just deleted. The page is correctly preserved, but llmwiki lint will raise broken-citation (severity error) on it. Whenever any page is kept, rm says so:
rm doesn’t scan the citations itself - llmwiki lint already reports exactly which markers broke, and re-implementing that parsing on a destructive path would be the wrong place for it.

Safety boundaries

A symlinked entry in sources/ is never treated as a source - rm resolves it the same as a missing file. A concept slug that would escape wiki/concepts/ fails a filename-safety floor: rather than deleting through it, rm leaves the page on disk, warns Not deleted: <slug> (floor:unsafe-slug), and exits 1.
That warning is unreachable by construction, and is kept only as defense in depth. Every slug rm can reach comes from .llmwiki/state.json, and reading that file already rejects an unsafe slug - the whole state is classified corrupt, and rm refuses before a plan exists. The floor exists so the deletion primitive is safe for any caller, not because this one can trip it.
Page deletes apply as one journalled batch. If the process is killed partway through, the next rm, the next compile, or llmwiki recover restores any pages from that batch that had already been removed - see resuming an interrupted removal. rm runs under the project lock (.llmwiki/lock). If another llmwiki process already holds it, rm refuses cleanly and exits 1 rather than waiting or forcing. --dry-run never takes the lock at all. Within a single rm, the plan is computed before the project lock is acquired - deliberately, so --dry-run never has to take it. That leaves a window in which a concurrent compile, watch or review approve can change which pages this source owns. (llmwiki watch recompiles automatically on any change under sources/, so this is an ordinary workflow, not a rare schedule.) rm recomputes that ownership from fresh state under the lock. If it differs from the plan in any way - a page became shared, became exclusive, moved to another source, or was added - the removal refuses outright, before deleting anything:
Nothing is removed: the source file, every page, and state.json are exactly as they were, and re-running plans against current state. rm refuses rather than silently re-planning because the plan you were shown is the only set the command is authorized to delete - quietly deleting a different one is the failure a command with no confirmation prompt cannot afford.

Resuming an interrupted removal

rm deletes the source file first, then the pages. If the page batch fails - a page survives its unlink, the process is killed - the source file is already gone while its .llmwiki/state.json entry remains. Re-running rm with the same name finishes the job. That pairing of “file gone, state entry present” is what identifies an interrupted removal, and it’s how rm tells the case apart from a name that was never there:
The pending journal batch is replayed first, so any pages the failed run had already deleted are restored before being deleted again as one clean batch. A name occupied by something that isn’t a valid source - a symlink, a directory - is not treated as an interrupted removal; that stays a plain “no source matches”.

Embeddings after a removal

Removal uses the same storage selection as compile. An existing binary index stays authoritative; an older JSON backup is not updated or used as a fallback. The removal itself has no new text to embed, but the refresh that runs after it is the same full refresh every other command uses, not a pruning-only variant. It always prunes the deleted pages’ vectors, and it also embeds any other eligible page that doesn’t already have a stored vector - so rm can call your embeddings provider even for pages it didn’t touch. This step is attempted even when no embeddings backend is configured - it is not skipped. A missing or invalid backend surfaces as a warning instead:
By default, a failed embeddings refresh only prints that warning and rm still exits 0. If you’ve set LLMWIKI_EMBED_STRICT, the same failure makes rm exit 1 too, matching every other llmwiki command.
Either way, the delete has already happened by the time embeddings run: the source file, the deleted pages, and .llmwiki/state.json are all written before the embeddings step starts. A non-zero exit here means your embedding index is stale, not that the removal was rolled back or partially applied.

Exit codes