<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: jjopensoftworks-blip</title>
    <description>The latest articles on DEV Community by jjopensoftworks-blip (@jjopensoftworksblip).</description>
    <link>https://dev.to/jjopensoftworksblip</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4035596%2F0cc9e973-313b-418a-a298-d34d9aaa6422.png</url>
      <title>DEV Community: jjopensoftworks-blip</title>
      <link>https://dev.to/jjopensoftworksblip</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jjopensoftworksblip"/>
    <language>en</language>
    <item>
      <title>Scrubkit: point it at a folder, get clean text + metadata back — 100% offline</title>
      <dc:creator>jjopensoftworks-blip</dc:creator>
      <pubDate>Sun, 19 Jul 2026 08:42:41 +0000</pubDate>
      <link>https://dev.to/jjopensoftworksblip/scrubkit-point-it-at-a-folder-get-clean-text-metadata-back-100-offline-13ai</link>
      <guid>https://dev.to/jjopensoftworksblip/scrubkit-point-it-at-a-folder-get-clean-text-metadata-back-100-offline-13ai</guid>
      <description>&lt;p&gt;If you've ever built RAG ingestion, a search index, or any "read a pile of documents" feature&lt;br&gt;
in .NET, you know the annoying part isn't the embeddings or the vector store. It's step zero:&lt;br&gt;
getting clean text out of a messy folder of PDFs, Word docs, spreadsheets, emails,&lt;br&gt;
presentations, and whatever else landed there — without shipping the files off to some cloud&lt;br&gt;
API.&lt;/p&gt;

&lt;p&gt;Scrubkit does exactly that, and nothing leaves your machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Scrubkit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;scrubber&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FolderScrubber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ReadOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Recursion&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Recursion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AllNested&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;scrubber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadStreamAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;@"C:\Docs"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// skip metadata-only rows&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UpsertAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Metadata&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One call, one flat table: per file you get &lt;code&gt;Text&lt;/code&gt;, &lt;code&gt;Metadata&lt;/code&gt;, &lt;code&gt;TypeBucket&lt;/code&gt;, &lt;code&gt;SizeBytes&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;Modified&lt;/code&gt;, and any &lt;code&gt;Warnings&lt;/code&gt;. No network calls, no telemetry — usable in air-gapped and&lt;br&gt;
regulated environments.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it reads out of the box
&lt;/h2&gt;

&lt;p&gt;PDF (PdfPig), Office docx/pptx/xlsx, clean text from HTML/RTF (markup stripped, not dumped),&lt;br&gt;
the plain-text family (txt/md/csv/json/xml/…), and image EXIF. Unknown types come back as a&lt;br&gt;
metadata-only row. A single unreadable file never crashes the batch — the problem shows up as&lt;br&gt;
a &lt;code&gt;Warning&lt;/code&gt; on that row.&lt;/p&gt;
&lt;h2&gt;
  
  
  No code? There's a CLI too
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--global&lt;/span&gt; Scrubkit.Tool

scrubkit scan ./docs                                          &lt;span class="c"&gt;# extract → CSV on stdout&lt;/span&gt;
scrubkit scan ./repo &lt;span class="nt"&gt;--redact&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; jsonl &lt;span class="nt"&gt;--out&lt;/span&gt; docs.jsonl  &lt;span class="c"&gt;# scrub PII + secrets → JSON Lines&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Same engine, zero code — handy for CI (fail a build if secrets would leak) or a quick look at&lt;br&gt;
a folder from any shell.&lt;/p&gt;
&lt;h2&gt;
  
  
  Built like a real library, not a gist
&lt;/h2&gt;

&lt;p&gt;Multi-targeted net8.0 + netstandard2.0. 470+ tests, ~99% line coverage, a 99% gate in CI&lt;br&gt;
(Linux + Windows). Package validation guards the public API against accidental breaking&lt;br&gt;
changes. Deterministic, SourceLinked builds; tag-driven versioning (MinVer); symbol packages.&lt;br&gt;
Fast: a BenchmarkDotNet run extracts on the order of ~12,000 files/sec (4-way parallel) over a&lt;br&gt;
mixed text-file corpus.&lt;/p&gt;
&lt;h2&gt;
  
  
  Extensible without forking
&lt;/h2&gt;

&lt;p&gt;Adding a format is one interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyExtractor&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IFileExtractor&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;CanHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ext&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;".xyz"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ExtractedContent&lt;/span&gt; &lt;span class="nf"&gt;Extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Extractors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MyExtractor&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;   &lt;span class="c1"&gt;// tried before the built-ins&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add-ons reference only the tiny Scrubkit.Abstractions contracts package — no PDF or image&lt;br&gt;
dependencies pulled in. The family's grown since I last posted this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scrubkit.Email&lt;/strong&gt; — .eml (MIME) &lt;em&gt;and&lt;/em&gt; Outlook .msg (OLE2): headers → metadata, body → text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scrubkit.OpenDocument&lt;/strong&gt; — .odt / .ods / .odp from LibreOffice / OpenOffice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scrubkit.Epub&lt;/strong&gt; — .epub e-books.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scrubkit.LegacyOffice&lt;/strong&gt; — pre-2007 binary Office: .doc / .xls / .ppt, read straight off the
OLE2 compound file with the BCL. No NPOI, no interop, no extra dependency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scrubkit.Extensions.DependencyInjection&lt;/strong&gt; — &lt;code&gt;services.AddScrubkit(…)&lt;/code&gt; for ASP.NET Core and
worker hosts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scrubkit.Parquet&lt;/strong&gt; — write the table to Apache Parquet for data-lake ingestion
(net8.0-only).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scrubkit.All&lt;/strong&gt; — one meta-package that pulls in the whole family, if you'd rather not
assemble packages by hand.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Redaction, if you want it
&lt;/h2&gt;

&lt;p&gt;Text is returned exactly as read. Opt into scrubbing by supplying an &lt;code&gt;IRedactor&lt;/code&gt; — entirely&lt;br&gt;
your call, and explicitly best-effort, not a compliance tool. Beyond the usual PII (emails,&lt;br&gt;
phones, Luhn-checked cards, SSNs, IPs, IBANs), it also catches secrets in recognisable formats&lt;br&gt;
— PEM private keys, JWTs, credentialed connection strings, AWS/Google/GitHub/Slack keys — and&lt;br&gt;
can de-identify while keeping data joinable: stable per-value tokens&lt;br&gt;
(&lt;code&gt;jane@example.com&lt;/code&gt; → &lt;code&gt;[EMAIL_3f9a1c8e]&lt;/code&gt;) or a format-preserving mask that keeps the last few&lt;br&gt;
characters (&lt;code&gt;4111 1111 1111 1111&lt;/code&gt; → &lt;code&gt;**** **** **** 1111&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Big folders? Re-scan only what changed
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;FolderScrubber.ReadChangesAsync(root, baseline)&lt;/code&gt; extracts only files added or modified since&lt;br&gt;
a lightweight &lt;code&gt;Manifest&lt;/code&gt; (size + last-write time) — a real win on repeated runs over large&lt;br&gt;
trees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in 30 seconds
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Scrubkit
&lt;span class="c"&gt;# or, without installing:&lt;/span&gt;
dotnet run &lt;span class="nt"&gt;--project&lt;/span&gt; samples/Scrubkit.Playground
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;NuGet: &lt;a href="https://www.nuget.org/packages/Scrubkit" rel="noopener noreferrer"&gt;https://www.nuget.org/packages/Scrubkit&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Site + docs: &lt;a href="https://jjopensoftworks-blip.github.io/Scrubkit/" rel="noopener noreferrer"&gt;https://jjopensoftworks-blip.github.io/Scrubkit/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/jjopensoftworks-blip/Scrubkit" rel="noopener noreferrer"&gt;https://github.com/jjopensoftworks-blip/Scrubkit&lt;/a&gt; — a ⭐ helps a lot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feedback and format requests welcome. What would you point it at?&lt;/p&gt;




</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>showdev</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
