CLI Reference
Overview
piicrawler is a single binary that ships in three modes:
- An interactive terminal UI (no arguments)
- A web UI server (
piicrawler serve) - A set of command line subcommands for one-off scans, real-time monitoring, DSAR lookups, and HTML report generation
Run piicrawler help (or -h / --help) to print a usage summary at any time. Each subcommand also accepts --help (e.g. piicrawler scan --help).
Synopsis
piicrawler Launch interactive TUI
piicrawler --tui Launch interactive TUI (the same thing, said explicitly)
piicrawler demo Scan throwaway synthetic data (no setup, no registration)
piicrawler [scan] <path>... [--workers <n>] [--out <file>] [--format jsonl|csv] [--progress tui|plain|none] [--quiet] [--no-ocr] Scan files or directories
piicrawler smb <server> <share> [-u <user>] [-p <pass>] [--subfolder <path>] [options] Scan an SMB network share (saves to the database)
piicrawler serve [port] Start web UI (default port 3001)
piicrawler watch <path>... [options] Monitor directories for PII in real-time
piicrawler dsar <name> [options] Search for a person's PII across all scans
piicrawler scans [--json] List recorded scans and their IDs
piicrawler report <scan_id> [--out <file>] Generate an HTML report for a scan
(--max-findings <n> caps the findings list)
piicrawler export <scan_id> [--out <file>] [--exclude-fp] Export a scan's findings as CSV
piicrawler textextract <path> [--no-ocr] [--out <file>] Print extracted text from a file, directory, or container
piicrawler findings list --scan <id> [filters] [--json] List findings for a scan (LLM triage)
piicrawler findings mark --scan <id> --verdict <fp|tp> <selector> Mark findings as fp or tp
piicrawler findings unmark --scan <id> --verdict <fp|tp> <selector> Clear a verdict (back to unreviewed)
piicrawler findings stats --scan <id> [--json] Show verdict tallies for a scan
piicrawler register <email> [--timeout <secs>] [--force] Register this installation by email
piicrawler update [--yes] [--force] Download and install the latest build in place
piicrawler version Print the PII Crawler version
piicrawler completions <shell> Print a shell completion script (bash, zsh, fish, ...)
piicrawler help Show the built-in help message
Naming a scan, a list, or a run by ID
Commands that act on something saved in the database take its numeric ID, and they take it either way round: as a bare number or as a flag.
piicrawler export 42 piicrawler export --scan 42
piicrawler findings list --scan 42 piicrawler findings list 42
piicrawler lists show 3 piicrawler lists show --list 3
piicrawler lists run --list 3 --action delete --yes piicrawler lists run 3 --action delete --yes
piicrawler lists run-show 12 piicrawler lists run-show --run 12
Each pair above is the same command. The flag is --scan for a scan, --list for an action list, and --run for a past action run. This reference writes each command in one spelling for consistency, but the other one always works, so a habit picked up from one command carries to the next.
Two rules:
- Giving both at once (
piicrawler export 42 --scan 43) is an error, not a silent preference for one of them. piicrawler lists addnames two things at once, a list and a scan, so it keeps--listand--scanand takes no bare numbers:piicrawler lists add --list 1 --scan 2 --file 10 11. Two unlabelled numbers in a row have no order anyone could remember.
Global options
These flags work with any command (and on their own, e.g. before launching the TUI).
-
--log-level <level>: Minimum log level to record:error,warn,info,debug,trace, oroff. Defaults toinfo. Logs appear in the TUI's Logs view and in the log file, which every run writes. This flag overrides thePIICRAWLER_LOG_FILTERenvironment variable when both are set. Usedebugortraceto surface SMB protocol traces during a network-share scan, andoffto record nothing at all: a scan's log names every file it read, sooffis the setting for a machine that should not keep that list.piicrawler --log-level debug # launch the TUI with debug logging piicrawler scan ~/share --log-level trace # trace a one-shot scan -
--tui: Launch the interactive terminal UI. Runningpiicrawlerwith no arguments does the same thing, so most of the time you will not need this. Reach for it where "pass no arguments" is awkward to write down: a wrapper script, a desktop entry, or ansshcommand that would otherwise end in a bare binary name.ssh admin@fileserver piicrawler --tui # review a remote host's scans--tuinames what to run, so it cannot be combined with a command that names something else:piicrawler --tui scan ~/datais refused rather than quietly ignoring one of the two. -
--color <when>: Control color in human-facing output (the scan report card,doctor, the registration notice):auto(the default),always, ornever. Underauto, color is on when the output is an interactive terminal and off when it is piped or redirected. The conventionalNO_COLORenvironment variable disables color, andCLICOLOR_FORCEenables it; an explicit--color alwaysoverridesNO_COLOR. Color never touches machine output (stdout JSON, CSV, or--outfiles), so it is always safe to leave on.
Commands
(no arguments): Interactive TUI
piicrawler
Launches the interactive terminal UI for browsing scans, viewing findings, and managing the local database. This is the default mode when you run the binary with no arguments.
demo: See it work on sample data
piicrawler demo [--keep]
The fastest way to see what PII Crawler does. It generates a set of synthetic files (a fake HR export, a customer list, a payroll memo) seeded with obviously made-up but realistically formatted PII, alongside a few hundred ordinary PII-free files. It scans them all with the live progress bar on, so you watch it work for a beat, then prints the same report card a real scan produces, followed by a spot-check section that shows a sample of the actual matches highlighted inside the lines they were found on. By default the sample files are written to a temporary directory and deleted as soon as the command finishes, so nothing on your disk is read or left behind.
demo needs no setup: it does not require registration and never opens your database. It is the recommended first command for a new install.
PII Crawler demo
Generating a sample data breach — all synthetic, nothing on your disk is touched.
synthetic sample data
✖ CRITICAL RISK 79 findings · 3 of 304 files 0.4s
email 15 ██████████████████
name 15 ██████████████████
phone 15 ██████████████████
credit-card 14 █████████████████
ssn 13 ████████████████
address 3 ████
city_state_zip 3 ████
dob 1 █
Hottest files
61 hr/employee-records.csv
12 exports/customer-export.txt
6 notes/payroll-memo.txt
Spot check — each match shown on the line it was found on:
hr/employee-records.csv
ssn …mple.com,(212) 867-5309,«123-45-6789»,01/15/1985,4111-1111-111…
credit-card …om,(212) 867-5309,123-45-6789,01/15/1985,«4111-1111-1111-1111»
dob …om,(212) 867-5309,123-45-6789,«01/15/1985»,4111-1111-1111-1111
phone …[email protected],«(212) 867-5309»,123-45-6789,01/15/1985…
exports/customer-export.txt
credit-card …99 Card on file: «6011-0009-9013-9424» Home: 123 Main Str…
phone …[email protected] Phone: «(202) 555-0199» Card on file: 6011-…
email …e) Jane Doe «[email protected]» Phone: (202) 555-…
city_state_zip …: 123 Main Street, «Springfield, IL 62704» Robert Brown ro…
notes/payroll-memo.txt
ssn …mple.com). SSN on file: «457-55-5462». Cell: (206) 555-0117. M…
phone …le: 457-55-5462. Cell: «(206) 555-0117». Mail the corrected W-…
email …ontractor Alex Kim («[email protected]»). SSN on file: 457-…
city_state_zip … the corrected W-2 to 789 Pine Road, «Springfield, IL 62704».
That came from throwaway files we generated and just deleted.
Keep them next time → piicrawler demo --keep
Not registered yet? It's free and takes 30 seconds → piicrawler register [email protected]
Then point it at something real → piicrawler ~/Documents
The spot check shows at most a few findings per file, one per PII type, each highlighted with «» inside a window of its own line. The full counts are on the report card above it.
The sample files are named the way they sit in the sample, so the live progress, the Hottest files list and the spot check all point at the same names. Scanning your own files needs a registration, which is free and takes about 30 seconds, so the demo closes by offering register before the commands that need it.
Pass --keep to write the sample files to ./piicrawler-demo instead of a temporary directory and leave them there when the command finishes. You can then open the files to check the matches yourself, or rescan the folder with a real command (piicrawler piicrawler-demo) and compare the results. That rescan is an ordinary scan, so it needs this copy to be registered first. Delete the folder whenever you like; rerunning piicrawler demo --keep regenerates it.
A demo that is stopped before it finishes leaves its sample behind. A process killed by a signal does not get to run its own cleanup, so Ctrl-C, or quitting a pager partway through, or piicrawler demo 2>&1 | head, all leave the temporary sample directory on disk. It is named piicrawler-demo-<random> in your system temp directory and holds nothing but the synthetic files. The next piicrawler demo sweeps up any such leftover that is over an hour old and says how many it took. It only ever removes directories that carry the marker file demo itself writes, so nothing else in your temp directory is at risk.
When you are ready to scan your own files, point scan at a real folder. Color follows the --color setting.
[scan] <path>...: One-shot scan
piicrawler [scan] <path>... [--workers <n>] [-j <n>] [--out <file>] [-o <file>] [--format jsonl|csv] [--progress tui|plain|none] [--quiet] [--no-ocr] [--exclude <regex>]... [--ext <list>] [--max-size <MB>] [--only <types>] [--exclude-type <types>] [--all] [--regex <label=pattern>]... [--terms-file <path>] [--detect-forms] [--summary] [--json] [--fail-on-findings] [--fail-on-risk low|medium|high|critical] [--save] [--name <label>]
Scans one or more files, archive containers (e.g. .zip, .tar.gz), or directory trees. The leading scan keyword is optional, so piicrawler ~/share and piicrawler scan ~/share are equivalent.
What it prints depends on where the output goes. On an interactive terminal it shows a colored report card on stderr: a risk verdict, a per-type breakdown, the files with the most findings, and the suggested next command (see Interactive report card below). When stdout is piped or redirected it writes the full results to stdout as JSON, so scripts keep working unchanged. Pass --json to force JSON on a terminal too. Progress is always written to stderr, so piping stdout is always safe:
piicrawler ~/Documents > findings.json # stdout not a terminal, so JSON is written
piicrawler ~/Documents # terminal, so the report card is shown
Multiple paths and stdin. scan accepts several targets in one invocation, scanned in the order given. A literal - reads newline-separated paths from stdin (blank lines ignored), so it composes with tools like find:
piicrawler scan ~/share ~/Downloads # several targets at once
find ~/share -name '*.pdf' | piicrawler scan - # paths from stdin
- may be mixed with literal paths (the stdin paths are appended after them). The JSON shape adapts to the input: a single plain file prints one ScanResult object (unchanged from before), while multiple paths, a directory, or a container print a JSON array of results.
Requires a registered installation. If no license is present, the command exits with an error. Register from the command line with piicrawler register <email> (documented below), or interactively via piicrawler serve (web UI) or the TUI.
Options:
--workers <n>,-j <n>: Number of worker threads to use when scanning a directory. Defaults to4. Capped at the number of files found.--out <file>,-o <file>: Stream results to<file>instead of stdout (truncates if it exists). The format is auto-detected from the file extension:.csv→ CSV (one row per finding, see below), anything else → JSONL (oneScanResultobject per line). Each result is written and flushed as soon as its file finishes scanning (and, for a very large text file, while it is still being scanned), so memory does not grow with the size of the tree or of any one file. Recommended for large directory scans. Stdout JSON output is suppressed when this flag is used; stderr progress is unaffected. The destination may not be one of the paths being scanned: it is emptied before the scan starts, so the scan would read the file it just destroyed and report it as clean, and that combination is refused before anything is opened. A destination inside a folder being scanned is fine —piicrawler scan . --out findings.jsonlworks — and the results file itself is not scanned.--format jsonl|csv: Force the output format, overriding extension detection. Requires--out. Useful when piping CSV to a file without a.csvextension, or forcing JSONL output to a.csvfilename.--progress tui|plain|none: Control the progress display on stderr. Default: auto, meaningtuiwhen stderr is an interactive terminal andplainotherwise (e.g. CI logs, piped stderr).tui: In-place status block: a spinner, counts, a progress bar, live throughput (files per second), an estimated time remaining, and the files currently scanning. Updates in place using ANSI cursor controls and never scrolls. The throughput and ETA appear once a couple of files are done and disappear at completion. File counts are comma-grouped (e.g.155,693) for readability on large scans. Before the bar can appear, the whole tree is walked to count what will be scanned. During that phase a liveDiscovering files under … Nline counts up, so a large directory (a whole home folder can hold hundreds of thousands of files) shows activity immediately instead of looking hung.plain: Scrolling per-file[start N/T]/[done N/T]/[running K]lines, preceded by aDiscovering files under …line and occasional…N files so farmilestones while the tree is enumerated. Use this for CI logs or anywhere the ANSI-aware mode would cause garbled output.none: Suppress all progress output (equivalent to--quiet).
--quiet: Alias for--progress=none.--no-ocr: Skip OCR on images and scanned PDFs. Speeds up scans of mostly-text trees. Images encountered under--no-ocrare counted as skipped, not as errors, so the report card does not report them as "unreadable" (see Interactive report card).--exclude <regex>: Skip files and directories whose full path matches the regex. Repeatable; a path matching any one pattern is skipped. A matched directory is pruned, so the walk never descends into it (e.g.--exclude '(^|/)node_modules/' --exclude '(^|/)\.git/'skips dependency and VCS trees entirely). An invalid regex fails the scan before any work begins, on every scan type, and so does one that would exclude every path (an empty pattern from an unset shell variable, a trailing|, or.*; see Patterns that match everything). The report card lists each pattern with the number of paths it excluded, so an over-broad pattern (build/also matchesrebuild/) or one that matched nothing is visible; see Exclusion Patterns. (PII Crawler's own data directory,~/.piicrawler, is pruned automatically, so you do not need to exclude it by hand. See File filters below.)--ext <list>: Only scan files whose extension is in this comma-separated list (case-insensitive, leading dot optional, e.g.--ext csv,pdfor--ext .csv,.pdf). Files with no extension are skipped once the list is set. A list with no usable entries (e.g.--ext ,) disables the filter rather than skipping everything. The report card says how many files the list skipped.--max-size <MB>: Skip files larger than this many megabytes. The limit is inclusive, so--max-size 50keeps a file of exactly 50 MB. Without this flag there is no size limit at all: a multi-gigabyte log or database dump is read in bounded windows rather than skipped.--max-size 0fails the scan, because a ceiling of zero admits no file that has anything in it: the scan would read nothing and report the same clean result as an empty folder. Omit the flag for no limit. The report card says how many files the limit skipped, so a scan that quietly passed over the biggest export in the tree is visible.--only <types>: Run only the comma-separated PII detectors listed (e.g.--only ssn,credit-card). Conflicts with--alland--exclude-type. An unknown slug fails the scan and prints the full list of valid names. A value that names no detector at all, which is what an unset shell variable expands to (--only "$TYPES"), fails the scan too, unless--regex,--terms-fileor--detect-formsgives it something else to look for: a scan with nothing to detect would read every file and report no findings, which is indistinguishable from a clean tree.--exclude-type <types>: Skip the comma-separated detectors listed. Applied on top of the default-enabled set, or on top of--allwhen combined with it (e.g.--all --exclude-type nameruns every detector except full-name matching). A list long enough to leave no detector running fails the scan, on the same terms as--onlyabove.--all: Enable every detector, including the region-specific ones that are off by default: NZ IRD, AU Tax File Number, DE Tax ID (Steuer-ID), and the Australian, German, and New Zealand passport and driver-licence detectors. This is the only way to reach those region detectors from the CLI; the default scan runs the broadly-applicable set only.--regex <label=pattern>: Add a custom regex detector. Repeatable, so you can supply several. The text before the first=is a label and the rest is the pattern, so the pattern may itself contain=. Matches surface under aregex-<label>PII type, where the label is slugified (lowercased, with anything that is not a letter dropped, soemp-idbecomesempid). An invalid pattern fails the scan before any work begins. Example:--regex 'empid=EMP-\d{6}'flags strings likeEMP-123456asregex-empid.--terms-file <path>: Match a list of keywords loaded from a text file, one term per line. Blank lines and surrounding whitespace are ignored. Matches surface under aterms-list-<name>PII type, where<name>is the slugified file stem (somedical-terms.txtbecomesterms-list-medicalterms). An empty or unreadable file fails the scan, and a path that is not there is reported like a mistyped scan target, with a suggestion and exit status2. This is the file-based equivalent of the terms lists managed in the TUI and web UI.--detect-forms: Also run document-similarity matching against the bundled US tax-form templates (1040, W-2, W-4, W-9, and others) and emit aus-tax-formsfinding per detected form. Off by default because the extra pass adds overhead even when no forms are present.--summary: Print a compact per-PII-type count table instead of the full JSON finding dump, for quick triage and CI logs. Each line is a detector slug and its finding count, ordered by descending count (ties broken alphabetically), closed by aTOTALline; a clean scan printsNo PII findings.. The table is results, not progress, so it is written to stdout and is not suppressed by--quiet. Combine it with--outto still stream the full results to a file while seeing the summary on stdout. Under--save(where stdout is reserved for the scan ID) the same table is written to stderr alongside the saved-scan summary.--json: Emit the full results to stdout as JSON even when stdout is a terminal, and suppress the report card. Without this flag, an interactive scan shows the report card and JSON is written only when stdout is piped or redirected. Use--jsonwhen you want machine-readable output while still watching the terminal. Conflicts with--outand--summary, which own stdout themselves.--fail-on-findings: Exit with status1if the scan turns up any PII, and0when it is clean. Output (stdout JSON and any--outfile) is unchanged; only the exit code differs. Designed for CI pipelines that should break the build when PII leaks into a repository or artifact.--fail-on-risk <level>: Exit with status1when the scan's overall risk level reaches the given threshold:low,medium,high, orcritical. The level is computed from the same composite risk score the HTML report headlines (the sum of every finding's PII-type risk weight), sopiicrawler reportand this gate agree on the same files. Use it to tolerate a few low-risk hits while still failing on, say, a directory full of Social Security numbers. AFAIL:line naming the level and score is written to stderr when the threshold is met.--save: Persist the scan to the local database instead of only streaming results. This is what makesreport,findings, anddsarwork on a CLI scan: a saved scan gets a record (with an ID), per-file rows, and stored matches, exactly like a scan started from the TUI or web UI. The scan runs through the same engine those interfaces use, and looks for exactly what the command asked for: the detectors you chose, any--regexpatterns, and any--terms-fileterms. Saved terms lists and proximity groups are picked per scan on the TUI and web UI's new-scan form, so a command-line scan does not run them; start the scan there if you want one. The new scan's numeric ID is printed to stdout (so you can capture it in a script); a human-readable summary and the live progress display go to stderr. Conflicts with--out/--format: a saved scan keeps its results in the database, so retrieve them afterwards withpiicrawler report <id>orpiicrawler findings list --scan <id> --json.--name <label>: Label for the saved scan, shown bypiicrawler scans. Implies--save. Defaults to the scan path when omitted.
--save records a single scan against one root, so it accepts exactly one path; passing several paths (or - resolving to more than one) is an error. Save each separately, or drop --save to stream all of them.
Interactive report card
When stdout is a terminal, a scan ends with a report card on stderr instead of dumping JSON. It summarizes the whole invocation: a risk verdict (the same level the HTML report uses), a per-type breakdown with proportional bars, the files holding the most findings, and the next command to run.
~/Documents
⚠ HIGH RISK 47 findings · 12 of 1,240 files 18.3s
ssn 18 ████████████████
credit-card 12 ███████████
email 9 ████████
phone 8 ███████
Hottest files
19 budget/2024-payroll.xlsx
11 hr/onboarding.pdf
Next → piicrawler scan ~/Documents --save
A clean scan collapses to a single line, for example ✓ All clear no PII found across 1,240 files (18.3s). The reassuring headline rotates between runs (All clear, Squeaky clean, Nothing to see here, and so on), so a clean result is a small fresh reward rather than the same line every time. The card is suppressed by --quiet, --summary, and --json, and color follows the --color setting (so a piped or NO_COLOR terminal gets plain text). It never writes to stdout, so it does not interfere with --out files or piped JSON.
Skipped vs. unreadable files. Two counts can follow the file total, and they mean different things:
- skipped: files PII Crawler deliberately did not read: binary formats it does not extract text from, and images when
--no-ocris set. These are expected and not a problem. - unreadable: files it genuinely could not read. When any are present, the card lists them broken down by cause, so a large count is explained rather than mysterious:
✓ All clear no PII found across 595,000 files, 74,000 skipped, 1,132 unreadable (2m05s)
permission denied 1,015
timed out 97
too large 20
permission denied covers files the operating system refused (run with the right permissions, or --exclude those paths); timed out covers files whose text extraction exceeded its time budget; too large covers files over the size limit; and other collects everything else (corrupt files, decode failures). The same breakdown appears under the verdict line on a scan that also has findings.
Folders it could not open. A folder the operating system refuses is not the same loss as a file it refuses. A file costs one file; a folder costs everything inside it, at any depth, and none of it is ever counted, because nothing in it is reached in the first place. So the card names the folders instead of only counting them:
✓ All clear no PII found across 1 file, 1 unreadable (0.4s)
permission denied 1
1 folder could not be read, so nothing inside was scanned
/srv/payroll (Permission denied (os error 13))
The scan carries on through the rest of the tree, and each such folder is written to --out, to the CSV, and to the JSON dump as a record with its error, the same way an unreadable file is, so a script can see it too. The first five are named and the count is exact, so a tree of protected folders reads as and 12 more rather than a screen of paths. piicrawler textextract on a directory reports them the same way, as an [error: ...] block under the folder's name.
If the folders are ones you do not need, --exclude them and the scan is clean for the right reason. If you do need them, run the scan as an account that can read them.
Files read only in part. A few kinds of file hold more than a scan reads all of: a database table with more rows than the 1,000 sampled, a column of stored documents, a document carrying dozens of pictures that each need an OCR pass. These are read as far as is worth reading and the card says which ones, so a clean result over them is not mistaken for a clean result over the whole file:
✓ All clear no PII found across 1,240 files (18.3s)
2 files were read only in part
Sampled 5 of the 812 stored files in this database; 807 more were not read, in tickets.attachment
Spent 80s reading this document's pictures, so its 59 later ones were not read
These files are not errors: they were read, their findings are real, and they count as scanned. What the line tells you is where there is more to look at. A --save scan records the same thing per file, so it is still there when you come back to the scan later.
The card is a summary: it caps the breakdown at the top 8 PII types and the 5 files with the most findings. An interactive scan that only shows the card does not keep the individual findings (they are tallied as they are scanned and released, so a home-folder-sized tree does not balloon memory). To get every finding, capture the full results with --out <file>, force JSON with --json, pipe stdout (piicrawler scan … | less), or persist with --save and then use piicrawler findings list --scan <id> and piicrawler report <id>.
Very large text files are scanned in pieces. A plain text file of 32 MB or more (a big log, CSV, or SQL dump) is read in bounded windows rather than loaded whole, and its findings are written out as each window completes. A customer export with millions of findings would otherwise have to hold every one of them until the file finished, which is enough to run a machine out of memory on a large enough file. Peak memory therefore stays roughly flat no matter how big the file is. Two things follow:
- Results are written to
--out(and to the JSON dump on stdout) while the file is still being scanned, so a partial record is on disk before the file completes. - Findings inside such a file are grouped by the window they were found in, so they are not ordered strictly by position in the file. The set of findings is the same either way.
- A table's column names travel with every window, so a labelled column reads the same at the end of a 10 GB export as at the start. That covers a heading above the header, which is what a report export, a saved spreadsheet and a dumped database table all write, however many rows of it there are.
With --out in JSONL format, or when the JSON dump goes to stdout, each file needs its record written in one piece, so files this large are scanned one at a time instead of in parallel. A directory holding several of them finishes faster with --out results.csv or with the report card alone, where results can be written as they arrive from every worker at once.
File filters. --exclude, --ext, and --max-size apply only while walking a directory tree. A single file or container archive named directly on the command line is always scanned, on the assumption that an explicitly named target is intentional. That holds for a saved scan too: pointing a scan at one file scans that file whatever its size or extension. The same filters are available in the TUI and web UI via scan configuration, where Max File Size is blank by default and blank means no limit. In the TUI the field is in megabytes, and a ceiling that is not a whole number of them — the web UI and the API take bytes — can be written with its own unit, as 500KB or 1500000B, which is also how the field shows one it was filled in from.
A filter says what it removed. A skipped file is not scanned, not stored, and not counted as an error, so a smaller file count is the only other trace it leaves. The report card, the --save summary, and the scan page in the web UI each report how many files the size limit and the extension list kept out of the run, alongside the per-pattern exclusion counts:
✓ All clear no PII found across 41 files (2.1s)
3 files skipped, larger than the 50 MB limit
8 files skipped, extension not in the --ext list
Archive entries are bounded independently. --max-size measures a file's size on disk, but a compressed archive entry can be small on disk and enormous once decompressed (a highly compressible file, or a crafted "zip bomb"). To keep one such entry from exhausting memory, each entry inside a .zip, .7z, or .tar.gz is read with a fixed 100 MB decompressed cap. An entry that expands past that cap is reported as too large and skipped rather than read into memory whole, so the rest of the archive and the rest of the scan continue normally.
The app's own data is skipped by default. PII Crawler keeps its database, logs, and downloaded models in ~/.piicrawler (%USERPROFILE%\.piicrawler on Windows). A directory walk prunes that folder automatically, so piicrawler scan ~ does not scan PII Crawler's own database. The exclusion is lifted only when you target the data directory directly, so an explicit request is always honored: piicrawler scan ~/.piicrawler still works. This applies to plain scans, --save scans, and the TUI/web UI; it does not apply to smb shares (which are remote and never contain the local data directory).
Did you mean? A mistyped path that does not exist stops the scan before any work begins, with a suggestion drawn from the parent directory when a close match is found, for example:
error: no such path to scan:
testdata/files-to-scan-for-testng
did you mean testdata/files-to-scan-for-testing?
Only paths named directly on the command line are checked this way; paths read from stdin (piicrawler scan -) stay lenient, so a find … | piicrawler scan - pipeline keeps going if a file disappears mid-run. A mistyped subcommand is caught the same way (piicrawler scna … suggests scan) rather than being treated as a file to scan. Both cases exit with status 2.
An entry inside an archive is a special case of the same thing. Results name entries as <archive>!/<inner-path>, and pasting one back is not a path at all — the archive is what the command can be handed — so the suggestion points there:
error: no such path to scan:
reports/bundle.zip!/q3/customers.csv
did you mean reports/bundle.zip?
A typo in the archive half is picked up the same way, so one suggestion covers a slip on either side of the !/. textextract gives the identical answer.
The files an option names are checked the same way. --terms-file on scan, smb and imap, and --policy on watch, are paths you type like any other, so a missing one is reported with a suggestion and exits with status 2 as well, before the scan connects to anything or opens the database:
error: no such path to read terms from:
medical-termz.txt
did you mean medical-terms.txt?
A file that is there but cannot be read, such as a directory or one your account has no permission for, is a different failure: it keeps its own message and exits with status 1.
Subcommands that live under a group. delete, prune, ls, show, list, stats and the rest belong to scans, lists or findings. Typing one on its own names the full command instead of looking for a file by that name:
error: unrecognized subcommand 'prune'
Did you mean 'piicrawler scans prune'?
Where two groups have a command by the same name, both are offered:
error: unrecognized subcommand 'delete'
Did you mean one of these?
piicrawler scans delete
piicrawler lists delete
A file or directory that really exists always wins, so piicrawler run scans a folder named run rather than suggesting lists run.
Detector slugs. --only and --exclude-type accept these names: address, au-drivers-license, au-passport, au-tfn, aws-credential, city_state_zip, credit-card, de-drivers-license, de-passport, de-steuer-id, dob, drivers-license, ein, email, name, nz-drivers-license, nz-ird, nz-passport, passport, phone, ssn. Region detectors (nz-ird, au-tfn, de-steuer-id, and the AU/DE/NZ passport and driver-licence detectors) are off in a default scan and run only under --all or when named in --only.
Custom detection. --regex, --terms-file, and --detect-forms add your own detectors on top of the built-in ones; they compose with the selection flags above, so a scan can run, say, --only ssn --regex 'empid=EMP-\d{6}' to look for SSNs plus your employee-ID format. These are the CLI equivalents of the custom patterns, terms lists, and tax-form detection configured in the TUI and web UI. They apply to every scan target (single files, directories, and container archives) and work with --save too, in which case the resulting regex-*, terms-list-*, and us-tax-forms findings are stored in the database like any other. All three are validated up front, so a bad pattern or missing terms file fails before the scan starts.
Exit codes. A successful scan exits 0. The two --fail-on-* flags above opt into a 1 exit on findings or risk; without them scan always exits 0 even when PII is present. An exit code of 1 from an unflagged scan indicates a runtime error (e.g. missing registration), not findings. Both --fail-on-* flags work the same way under --save. The risk score is read back from the saved scan.
Saving to the database (--save). By default scan is stateless: it streams results to stdout (or --out) and forgets them. Pass --save to persist the scan instead, which unlocks the natural pipeline:
SCAN_ID=$(piicrawler scan ~/share --save --name "Q3 audit" --quiet)
piicrawler findings list --scan "$SCAN_ID" --json # triage
piicrawler report "$SCAN_ID" # HTML risk report
piicrawler dsar "Jane Doe" # search across all saved scans
The bare scan ID is the only thing written to stdout, so $( … ) captures it cleanly. A saved scan is identical to one created from the TUI or web UI, so it shows up in piicrawler scans and can be reported on, triaged, exported, or searched by dsar. It looks for what the command asked for and nothing else, so the terms lists and proximity groups saved in your library are not applied: those are chosen on the new-scan form, and a scan that needs one has to be started from the TUI or web UI. --save does not stream per-finding output, so it is mutually exclusive with --out/--format.
A saved scan records the settings it ran with: the detectors chosen by --only, --exclude-type and --all, --detect-forms, the patterns passed with --regex, the terms loaded by --terms-file, the --ext and --max-size filters, --exclude patterns, and the worker count and throttle. Resuming or re-running the scan from any of the three interfaces uses those same settings, so the two halves of one scan look for the same things. A --regex pattern is stored on the scan itself, label and all, and appears on the scan's page alongside any saved custom regex rules; it is not added to your saved rules, so later scans are unaffected. A --terms-file works the same way: the terms are copied onto the scan when it starts, so a resume keeps looking for them even if the file has since moved or changed, and the scan's page names the list with the number of terms it carried. The list is not added to your saved terms lists, so later scans are unaffected.
Compatibility note. Progress is always written to stderr, never stdout, so piicrawler scan ~/share > findings.json and piicrawler scan ~/share --out report.csv keep working unchanged regardless of the progress mode. The --out file content is identical across modes.
CSV format. When CSV is selected, the file starts with a header row and then carries one row per finding:
file_path,pii_type,term,start,end,error
/srv/share/contracts/2026-q1.pdf,ssn,123-45-6789,1024,1035,
/srv/share/contracts/2026-q1.pdf,email,[email protected],2110,2126,
/srv/share/contracts/clean.txt,,,,,
/srv/share/contracts/locked.pdf,,,,,decryption failed
Files with no findings still produce one row (with empty PII columns) so that scanned-but-clean and unreadable files are visible in the report. Values containing commas, quotes, or newlines are quoted per RFC 4180.
Behaviour:
- File: extracts text, runs PII detection, prints a single
ScanResultJSON object (or one JSONL line with--out). - Container: extracts each entry and scans it, prints an array of
ScanResultobjects (or one JSONL line per entry with--out). Containers include.zip,.7z,.tar.gz/.tgz, and.mbox. For.mboxfiles each message becomes its own entry; the message ordinal (andMessage-ID:when present) is appended tofile_path, e.g.mail.mbox::message-000042::<[email protected]>. Output streams as each entry completes, sotail -fworks on the JSONL file mid-scan. - Directory: recursively walks the tree (skipping symlinks) and scans every supported file type or container in parallel, prints an array of
ScanResultobjects (or one JSONL line per file as it completes with--out).
smb <server> <share>: Scan an SMB network share
piicrawler smb <server> <share> [--subfolder <path>] [-u <user>] [-p <pass>] [--domain <domain>] [--max-concurrent <n>] [--delay-ms <ms>] [--bandwidth-mbps <mbps>] [--workers <n>] [--no-ocr] [--only <types>] [--exclude-type <types>] [--all] [--regex <label=pattern>]... [--terms-file <path>] [--detect-forms] [--exclude <regex>]... [--ext <list>] [--max-size <MB>] [--name <label>] [--progress tui|plain|none] [--quiet] [--summary] [--fail-on-findings] [--fail-on-risk low|medium|high|critical]
Scans an SMB / CIFS network share (the same network-share scanning the TUI and web UI offer) without a browser, which makes it the right choice for servers, containers, and CI. The share is enumerated, and each file is downloaded and scanned with the configured throttle. The results are always saved to the local database, so a network scan behaves like scan --save: the new scan's numeric ID is printed to stdout, while the live progress display and a human summary go to stderr.
piicrawler smb fileserver Finance -u alice # password from PIICRAWLER_SMB_PASSWORD
piicrawler smb fileserver Finance -u alice -p 'secret' --subfolder HR/2025
Requires a registered installation (same gate as scan). The connection is tested up front, so a bad host, share, or credential fails immediately with a friendly error and no scan record is created.
Credentials. Provide -u/--username (and optionally --domain) for authenticated access, or omit them for an anonymous / guest connection. The password comes from -p/--password. When --username is given without --password, the password is read from the PIICRAWLER_SMB_PASSWORD environment variable instead, so it stays out of your shell history. Unlike the TUI and web UI, the CLI does not persist credentials to the database: the connected client is handed straight to the scan engine, so the password is never written to disk. (The scan record, file rows, and findings are saved as usual.)
The three flags are checked together. A password or a domain with no username has no account to authenticate as, so the scan stops and says which flag to add. A username with no password anywhere is allowed, because that is what a guest share takes: the connection sends a blank password and prints a note saying so, and if the server rejects the login, the error says the password it sent was blank. That is usually a PIICRAWLER_SMB_PASSWORD that never reached the shell you are running in, which sudo and most cron setups drop.
Options:
--subfolder <path>: Scan only this subfolder within the share. Defaults to the share root. Forward or backslashes both work.--username <user>,-u <user>: Username for authentication. Omit for an anonymous / guest connection.--password <pass>,-p <pass>: Password for authentication. If--usernameis given without--password, the password is read fromPIICRAWLER_SMB_PASSWORDinstead, and a blank password is sent when that variable is empty or unset. Requires--username.--domain <domain>: Windows domain for NTLM authentication. Requires--username.--max-concurrent <n>: Maximum number of files to read from the share concurrently. Defaults to2. Raise it for fast, lightly-loaded shares; lower it to be gentle on the server or the network.--delay-ms <ms>: Politeness delay inserted between files, in milliseconds. Defaults to100. Set to0to disable.--bandwidth-mbps <mbps>: Cap download bandwidth at this many megabits per second. No cap when unset (or when set to a non-positive value).--workers <n>,-j <n>: Worker threads for scanning the downloaded files. Defaults to4.
The three throttle settings and the worker count are saved with the scan, so stopping it and resuming it later (from the TUI or the web UI) reads the rest of the share at the rate you asked for.
--no-ocr: Skip OCR on images and scanned PDFs.--only,--exclude-type,--all,--regex,--terms-file,--detect-forms: Detector selection and custom detection, identical to thescancommand flags of the same name.--exclude <regex>,--ext <list>,--max-size <MB>: File filters, identical to thescancommand flags of the same name. They apply while enumerating the share.--name <label>: Label for the saved scan, shown bypiicrawler scans. Defaults to the UNC path (e.g.\\fileserver\Finance).--progress tui|plain|none,--quiet,--summary: Progress and summary output, identical to thescancommand. The--summarytable is written to stderr (stdout is reserved for the scan ID).--fail-on-findings,--fail-on-risk <level>: CI exit gates, identical to thescancommand. The risk score is read back from the saved scan.
Once a network scan completes, the natural follow-ups are the same as any saved scan:
SCAN_ID=$(piicrawler smb fileserver Finance -u alice --quiet)
piicrawler report "$SCAN_ID"
piicrawler findings list --scan "$SCAN_ID" --json
To see SMB protocol traces while diagnosing a connection problem, add --log-level debug (or set PIICRAWLER_LOG_FILTER=debug). For the interactive equivalent and how credentials are protected there, see Scan an SMB Network Share.
imap <host>: Scan an email account over IMAP
piicrawler imap <host> -u <user> [--port <n>] [--starttls] [-p <pass>] [--folders <list>] [--exclude-folder <name>]... [--max-concurrent <n>] [--delay-ms <ms>] [--bandwidth-mbps <mbps>] [--workers <n>] [--no-ocr] [--only <types>] [--exclude-type <types>] [--all] [--regex <label=pattern>]... [--terms-file <path>] [--detect-forms] [--exclude <regex>]... [--max-size <MB>] [--name <label>] [--progress tui|plain|none] [--quiet] [--summary] [--fail-on-findings] [--fail-on-risk low|medium|high|critical]
Scans an email account in place over IMAP: every message in the selected folders is fetched into memory, scanned (body, headers, and attachments), and discarded. Nothing is downloaded to disk. The mailbox is opened read-only with EXAMINE and BODY.PEEK, so messages are never marked as read and no flags change. Results are always saved to the local database, so like scan --save the new scan's numeric ID is printed to stdout while progress goes to stderr.
piicrawler imap imap.fastmail.com -u [email protected] # password from PIICRAWLER_IMAP_PASSWORD
piicrawler imap imap.example.com -u you --folders INBOX,Sent
piicrawler imap mail.corp.local --starttls -u you # STARTTLS on port 143
Requires a registered installation. The connection and login are tested up front, so a bad host or password fails immediately and no scan record is created.
Credentials. The password comes from -p/--password, or, when omitted, from the PIICRAWLER_IMAP_PASSWORD environment variable, so it stays out of your shell history. A mailbox has no guest login, so an empty password is refused rather than sent: if the variable is empty or unset and no --password is given, the scan stops and says so before connecting. Most providers with two-factor authentication (Gmail, Fastmail, iCloud) require an app password for IMAP; see Scan Email Over IMAP for provider specifics. The CLI does not persist the password: it is handed straight to the scan engine in memory.
The connection always uses TLS with certificate validation (implicit TLS on port 993 by default, or --starttls on 143). There is no option to skip certificate checks; self-signed servers work by installing the certificate into the OS trust store.
Options:
--port <n>: Server port. Defaults to 993, or 143 with--starttls.--starttls: Connect in plaintext and upgrade with STARTTLS instead of implicit TLS.--username <user>,-u <user>: Account username, usually the email address. Required.--password <pass>,-p <pass>: Account password or app password. Read fromPIICRAWLER_IMAP_PASSWORDwhen omitted. Cannot be empty.--folders <list>: Scan only these folders (comma-separated). A folder also covers everything nested under it. Defaults to all folders, with virtual duplicate views (Gmail's All Mail, Important, Starred) skipped automatically.--exclude-folder <name>: Skip this folder and everything under it. Repeatable.--max-concurrent <n>: Maximum messages fetched concurrently (one IMAP connection each). Defaults to2, well under every provider's connection cap.--delay-ms <ms>: Politeness delay between messages, in milliseconds. Defaults to100. Set to0to disable.--bandwidth-mbps <mbps>: Cap download bandwidth at this many megabits per second. No cap when unset.--workers <n>,-j <n>: Worker threads for scanning fetched messages. Defaults to4.
As with smb, the throttle settings and the worker count are saved with the scan and reused when it is resumed.
--no-ocr: Skip OCR on image and scanned-PDF attachments.--only,--exclude-type,--all,--regex,--terms-file,--detect-forms: Detector selection, identical to thescancommand flags of the same name.--exclude <regex>: Skip messages whose entry path (folder/NNNNNNN - subject.eml) matches this regex. Repeatable.--max-size <MB>: Skip messages larger than this many megabytes.--name <label>: Label for the saved scan. Defaults toimap://user@host.--progress,--quiet,--summary,--fail-on-findings,--fail-on-risk: Identical to thescancommand flags of the same name.
Each message becomes its own entry in the scan, named by folder, message number, and subject, so findings point at the exact email. Follow-ups work like any saved scan:
SCAN_ID=$(PIICRAWLER_IMAP_PASSWORD='app-pass' piicrawler imap imap.fastmail.com -u [email protected] --quiet)
piicrawler report "$SCAN_ID"
piicrawler findings list --scan "$SCAN_ID" --json
For the interactive equivalents, folder pickers, and provider setup, see Scan Email Over IMAP.
serve: Web UI
piicrawler serve [port] [--bind <address>] [--open]
Starts the web UI on the given port (default 3001) and opens an HTTP server you can reach at http://localhost:<port>. The web UI is where you create and manage scans, register your license, and review findings in a browser.
To start the server and open the web UI in your default browser in one step, add --open (or -o):
piicrawler serve --open # serve on 3001 and open the browser
piicrawler serve 8080 --open # serve on 8080 and open the browser
The Windows installer's Start Menu shortcut and the macOS PIICrawler.app bundle launch PII Crawler this way (with no console or Terminal window), starting the server and opening the UI for you. If no browser can be launched (for example on a headless machine) the server still starts and the failure is noted in the logs.
Only one PII Crawler instance runs at a time, whichever port you ask for. If one is already running, a second serve starts no server of its own and reports that on stderr:
PII Crawler is already running, so nothing was started on port 8080.
Use the web UI the running instance is serving, or stop it and try again.
With --open it opens the browser on the running instance instead, so launching from the Start Menu or the Dock a second time brings the UI back up.
By default the server binds to 127.0.0.1 (loopback only), so the UI is reachable from the local machine. To expose it on the LAN for testing, set a password under Settings → Login password first — a network bind with no password is refused — then use --bind:
piicrawler serve --bind 0.0.0.0 8080 # all interfaces
piicrawler serve --bind 192.168.1.10 # a specific NIC
PIICRAWLER_BIND=0.0.0.0 piicrawler serve # via env var
Options:
--bind <address>,-b <address>: Network address to bind to for this invocation only. Overrides the persistedSettings → Bind addressfrom the web UI. ReadsPIICRAWLER_BINDas a fallback. Resolution order:--bind/PIICRAWLER_BIND> persisted setting >127.0.0.1.--open,-o: Once the server is listening, open the web UI in your default browser.
When the server cannot start, the reason is explained above the error, and the last line is the command to run instead. The three that happen in practice:
- The port is already taken. Another application is listening on it. The message names the address and port, and offers a free port to use instead.
- The port is privileged or reserved. On Unix, ports below 1024 need elevated permissions. On Windows, Hyper-V, WSL and Docker Desktop each reserve blocks of ports at boot, and those blocks routinely cover ordinary four-digit ports.
- The bind address does not exist on this machine. Usually a machine that has moved networks since
Settings → Bind addresswas set. That setting lives in the web UI, which is exactly what will not start, so serve on loopback once to get back to it:
piicrawler serve --bind 127.0.0.1
--bind overrides the persisted setting for that run only, so the setting is still there to change when the UI comes back up.
You do not have to wait for a failed start to find out. piicrawler doctor reports the address the web UI would bind to and whether this machine can listen on it.
- The bind address is one other machines can reach, and no password is set. Refused rather than started. On a network bind the Host-header check that protects a loopback install is off, on the grounds that the password is doing that job instead — so without one the whole database would be readable both from the LAN and, through DNS rebinding, from any web page open in your own browser. Set a password under
Settings → Login password, or serve on this machine only.
Anything reachable at the bound address can hit the UI. The password gates the API routes for unauthenticated callers, but the UI itself, the static assets, and the login endpoint stay reachable. Scope the bind address to the smallest network you need.
watch: Real-time monitoring
piicrawler watch <path>... [--webhook <url>] [--policy <file>] [--scan-existing] [--no-json] [--no-ocr] [--debounce <ms>]
Watches one or more paths for file system changes and scans newly created or modified files for PII as they appear. A path may be a directory, which is walked recursively, or a single file, which is watched on its own. Each file found to hold PII is reported on stderr as it changes. Press Ctrl+C to stop the daemon.
Only changes are scanned. The files already under a watched path when the daemon starts are what it diffs against, so they are never reported on their own. Add --scan-existing to scan them once at startup, before the watching begins. The startup banner says which of the two is happening.
Each <path> is resolved to an absolute path before the daemon starts, so piicrawler watch ./uploads watches, reports and records /home/you/uploads/.... A violation therefore names the same file whatever directory it is read from, and a policy's path_pattern can be written against absolute paths as the examples do.
watch scans files, so it needs a registered installation like scan and smb do. On an unregistered copy it exits with status 1 and points at piicrawler register.
A watched path that does not exist stops the daemon before the startup banner claims to be watching it. Every missing path is listed at once — not just the first — each with a suggestion when a nearby name looks like the one you meant, and the command exits 2. This is the same error scan gives a mistyped target, and it names the resolved absolute path, which is the directory that was actually looked for.
Alerts come from policies. A finding becomes a violation only when it matches a loaded policy, and violations are what get streamed to stdout as JSON, POSTed to the webhook, and recorded in the database. A daemon started without --policy therefore scans normally and sends no alerts at all; it says so on startup. See Watch Mode & Policies for the policy file format.
Options:
--webhook <url>: POST policy violations to the given webhook URL as they are produced. Needs--policy, or there are no violations to send.--policy <file>: Load alert policies from a config file (TOML-style[[policy]]tables). A path that is not there is reported like a mistyped watch path, with a suggestion and exit status2. Each policy can match onpii_typeandpath_pattern, and tags every matching violation with anactionandseveritylabel. Loaded policies are appended to the local database for the daemon to consume. See Watch Mode & Policies for the file format, the webhook payload schema, and how to safely reload policies.--scan-existing: Scan every file that is already under the watched paths before watching for changes. Each one goes through the same extraction, scan and policy evaluation as a changed file, so violations reach stdout, the webhook and the database the same way. The pass closes withChecked N files already in place, M with PII.on stderr. See Watch Mode & Policies.--no-json: Disable the JSON stdout stream (use this when you only want webhook delivery or database persistence).--no-ocr: Skip OCR on images and scanned PDFs.--debounce <ms>: Debounce window for file events in milliseconds. Defaults to500. Useful when editors save in bursts.
dsar: Data Subject Access Request
piicrawler dsar "Person Name" [--assert-clean] [--report <file>] [--json] [--redact]
Searches every recorded scan in the local database for PII associated with the given person and prints a summary to stderr. Use this to fulfil GDPR/CCPA right-to-know requests or to check whether a specific person's data has leaked into a watched location. For an end-to-end walkthrough (scan → identity → DSAR report), see the DSAR Walkthrough.
Options:
--assert-clean: Exit with status1if any findings are returned (and0with aCLEAN:line if not). Designed for use in CI pipelines. TheCLEAN:line names how many scans were searched, so a gate that passed because the scan step never ran is not read as a pass.--report <file>: Write a self-contained HTML report to<file>.--json: Print structured findings as JSON to stdout in addition to the stderr summary. This output is not masked. It is the machine-readable half of the command, the counterpart to the masked--report, so treat the file like the database rather than like the report.--redact: Apply format-preserving redaction to everyssnanddobvalue before it leaves the process, for the case where the results go to an external LLM endpoint. This is the same mechanism, the same per-database secret and the same mapping asfindings list --redact, so a value redacts to the same string in both commands. See what it covers below.
Each JSON finding has these fields:
| Field | What it is |
|---|---|
scan_id, scan_name, scan_date |
The scan the finding came from |
file_path |
The file the value was found in |
match_id |
The finding's ID, which is what findings mark --match takes |
pii_type |
The kind of PII, for example ssn or email |
term |
The matched text, unmasked |
masked_value |
The same value masked, as it appears in the report and on stderr |
verdict |
unreviewed, false_positive or true_positive, from ordinary triage |
confidence |
High, Medium or Low. See Check the Medium and Low rows |
context |
The text surrounding the value, or null |
The result also carries what the search covered:
| Field | What it is |
|---|---|
scans_searched |
How many recorded scans the search read |
scans_with_findings |
How many of those held anything for this person |
scans_not_searched |
Scans in the database the search could not read, each with its scan_id, scan_name and status. A scan is read once it has completed or been stopped; one still running, or stopped on an error, covers an unknown part of its tree |
scans_without_identity_index |
How many of the searched scans have no completed identity run. Findings in those can only come back as Low confidence |
scan_id, file_path, pii_type, term and verdict mean the same thing here as in findings list --json, so a script written against one reads the other.
A DSAR search reports every match whatever its verdict, so verdict tells you what a reviewer already decided without changing what comes back. Filter on it yourself if you want to drop findings you have already dismissed:
piicrawler dsar "Jane Doe" --json 2>/dev/null \
| jq '[.findings[] | select(.verdict != "false_positive")]'
What --redact covers
--redact applies to the whole result, so every output of the command is redacted together:
piicrawler dsar "Jane Doe" --redact --json 2>/dev/null | your-llm-triage-script
termandcontextare rewritten, the wayfindings list --redactrewrites them.masked_valueis recomputed from the redacted value. Ordinary masking keeps an SSN's real last four digits (***-**-6789), so leaving it alone would hand those four back in the same JSON object.- The stderr summary and the
--reportHTML show the redacted values too, because both are built frommasked_value. A singledsar "Jane Doe" --redact --json --report jane.htmlcannot produce a redacted JSON and an unredacted report.
The substitution table is seeded from every scan the findings came from, not just one, so an SSN recorded in one scan is still scrubbed where it appears in another scan's context. As in findings list, each context string is also swept with the SSN and date detection regexes, which catches values that were never recorded as matches.
Not redacted: file_path, scan_name, scan_path, and the person's own name in person_query and in context. The limits described for findings list --redact apply here unchanged: other PII types pass through, and bare 9-digit SSNs are only caught when they were themselves recorded as matches.
scans: List and manage recorded scans
piicrawler scans [--json]
piicrawler scans delete <scan_id> [--yes] [--json]
piicrawler scans prune [--yes] [--json]
Lists every scan recorded in the local database, most recent first. Use this to look up the numeric scan ID required by piicrawler report.
The default output is a text table with the following columns:
ID: numeric scan ID (pass topiicrawler report)STATUS:pending,enumerating,scanning,completed,stopped, etc.FILES:scanned/totalfile countsFINDINGS: total PII matches recordedCREATED: timestamp of scan creationNAME: display name (often the path or a TUI-supplied label)PATH: root path that was scanned
On an interactive terminal the table is colored to be scannable at a glance: the status is green when finished, yellow while in flight, and red on failure, and a nonzero finding count is highlighted. Color follows the --color setting and is based on the table's own stream, so piicrawler scans | cat stays plain text while --color always keeps it colored through a pipe.
Options:
--json: Print the same listing as a JSON array on stdout for scripting (each entry includesid,name,path,status,files_total,files_scanned,findings_total,scan_type,created_at,updated_at).
For example, find the scan ID and generate its report:
piicrawler scans
piicrawler report 42
scans delete <scan_id>
Deletes a saved scan and everything recorded for it: its files, findings, action list entries, and identity results. The scan's HTML reports and CSV exports are ordinary files you wrote earlier, so they are untouched.
piicrawler scans delete 42
piicrawler scans delete 42 --yes
You are asked to confirm first. In a script, a cron job, or any session where stdin is not a terminal, there is nobody to ask, so --yes is required and the command refuses without it.
The disk space comes back. Deleting the rows is not enough on its own: the database keeps the freed pages for reuse and stays at its peak size on disk. So after removing the scan, delete compacts the database and reports what it returned:
Deleting scan 42 (2,132,700 findings)...
Deleted scan 42 (customer-share).
Reclaiming disk space...
1.4 GB of 2.1 GB...
Reclaimed 2.1 GB of disk space.
Both steps take a while on a scan with millions of findings, which is why each is announced before it starts and the compaction shows its progress on a terminal. The space is returned in stages, so stopping the command part way (Ctrl+C) keeps whatever it has already given back. A scan with no findings skips the compaction.
Options:
--yes: Skip the confirmation prompt.--json: Print{"deleted": <id>, "name": ..., "findings_total": ..., "reclaimed_bytes": ...}on stdout.
scans prune
Marks interrupted scans as stopped. A scan whose process was killed (Ctrl+C, a crash, an OOM, a reboot) never gets to write a final status, so it keeps saying scanning in piicrawler scans forever. prune clears those rows. Nothing else about the scan changes: the files and findings it recorded before it died are kept, so its report and export still work.
piicrawler scans prune
The interrupted scans are listed, then you confirm. As with delete, a non-interactive session needs --yes.
Only prune when no scan is running. PII Crawler cannot tell an abandoned scan apart from one another process is still working on, so pruning is your assertion that nothing is in flight. Running it during a scan (a second terminal, a cron entry) marks that live scan stopped in the database while it keeps going.
Options:
--yes: Skip the confirmation prompt.--json: Print{"pruned": <count>, "scans": [<id>, ...]}on stdout.
report: HTML risk report
piicrawler report <scan_id> [--out <file>] [-o <file>] [--max-findings <n>]
Generates a standalone HTML risk report for the scan with the given numeric ID. By default it writes piicrawler-report-<scan_id>.html in the current working directory. When it is done it prints Wrote report to <path> to stderr, so the file it made is named even when you did not name it. Run piicrawler scans to see the available scan IDs (or look them up in the TUI or web UI).
If no scan has the ID you gave, the command says so and stops without touching the destination file, so a mistyped ID cannot overwrite a report you already had.
The report is built to be shared, so every value the scan recorded is masked wherever it appears in it, including inside the snippet of surrounding text printed beside each finding. A snippet is a slice of the file, so it routinely quotes the values of the findings either side of the one it belongs to, and those are masked too. Findings dismissed as false positives are masked on the same footing: their own rows are gone from the report, but the value is still masked where it turns up next to something else. Unmasked values stay in the local database and in piicrawler export, which is the machine-readable half. Treat the export like the database, and the report like something you can attach to an email.
The report counts findings the way the rest of the product does: anything marked as a false positive is left out of the totals, the charts, and the per-file tables.
The report says what the scan could not read. A file whose read was refused, a folder that could not be listed, an archive that would not open: none of them produce findings, so none of them appear anywhere else on the page. A Could Not Be Read section names them, with the reason each one gave and a count per kind of failure, so a report over a share where half the folders were locked does not read like a report over a share that was clean. The list stops after the first 100 paths and says so; the count above it is exact however many there were.
Data Scanned is the size of what was read, not the size of the tree. A file the scan could not open still has a size on disk, and counting it here credited a scan with bytes it never looked at. When the two differ, the card shows the total underneath, so 135 B over of 151 B says both what was read and what was there.
The Detailed Findings list is capped; the numbers above it are not. A report is something you open in a browser and attach to an email, and a scan with hundreds of thousands of findings produced a page tens of megabytes long that no browser opens comfortably. So the per-file list at the bottom stops after the first 1,000 findings by default and says exactly what it left out, while the risk score, the summary cards, the per-type chart and table, the most-exposed-files table and the file-type breakdown are all computed over the whole scan whatever the cap is. Raise it with --max-findings, turn it off with --max-findings 0, or use piicrawler export — which streams — when what you want is every finding as data.
Options:
--out <file>(or-o <file>): Write the report to this path instead of the default. The path is used verbatim, so you choose both the directory and file name (for examplepiicrawler report 42 --out /tmp/q3-audit.html). The destination directory must already exist.--max-findings <n>: List at mostnfindings in the Detailed Findings section (default 1,000).--max-findings 0lists every one. When the cap bites, the section says how many findings it is listing out of how many, how many further files with findings it did not reach, and points atpiicrawler exportfor the complete set; a file listed in part carries a12 of 40 findingsbadge rather than one that reads as the file's total.
export <scan_id>: Export findings as CSV
piicrawler export <scan_id> [--out <file>] [-o <file>] [--exclude-fp]
Exports every finding from a saved scan as CSV, one row per match. Run piicrawler scans to look up the scan ID. If no scan has the ID you gave, the command says so and stops without touching the --out file, so a mistyped ID cannot overwrite an export you already had. By default the CSV streams to stdout, so it can be redirected or piped:
piicrawler export 42 --out findings.csv
piicrawler export 42 > findings.csv
piicrawler export 42 | grep ssn
The columns match the TUI and web UI exports, so files from any of the three can be combined or processed identically:
file_path,pii_type,match,start_pos,end_pos,context,verdict,file_status,file_error
verdict is the finding's review state (unreviewed, true_positive, or false_positive), file_status is what happened to the file during the scan (scanned, error, skipped, pending), and file_error holds the failure message when there is one. A file that failed to read has no findings and therefore no rows here; the TUI's Files list export (e on the Files list) is the per-file CSV that includes those.
Rows stream straight from the database to the output as they are read, so the export stays fast and uses constant memory no matter how large the scan is. This makes it the recommended way to pull findings out of very large scans (hundreds of thousands or millions of files), where downloading the same CSV through a browser can be slow.
Options:
--out <file>(or-o <file>): Write the CSV to this path instead of stdout (truncates if it exists). Prints a confirmation with the number of findings exported.--exclude-fp: Leave out findings that have been marked as false positives (in the TUI, web UI, or withpiicrawler findings mark). By default every finding is included.
For example, pull a clean CSV of triaged findings:
piicrawler scans
piicrawler export 42 --exclude-fp --out q3-findings.csv
textextract <path>: Print extracted text
piicrawler textextract <path> [--no-ocr] [--out <file>] [-o <file>]
Prints the raw text that PII Crawler's extractors would feed into the scanner, without running PII detection. Useful for verifying that a format is parsed correctly, debugging unexpected scan results, or piping cleaned text into another tool.
<path> may be:
- a single file (PDF, Office document, image, plain text, etc.)
- a directory (walked recursively, every text-extractable file is emitted)
- a container archive (
.zip,.7z,.tar.gz,.mbox): each inner entry is emitted with the<archive>!/<inner-path>convention used byscan
Output goes to stdout by default, with one block per file:
===== /path/to/file.txt =====
<extracted text>
===== archive.zip!/inner/notes.docx =====
<extracted text>
===== /path/to/scan.pdf =====
[error: Extraction failed: encrypted PDF]
Files that fail extraction are still listed, with the error written in place of the body so the failure is visible in the output.
A <path> that does not exist is different: nothing is written to stdout or to an --out file, the error goes to stderr with a suggestion when a nearby name looks like the one you meant, and the command exits 2. That is the answer scan gives a mistyped path, so a script can tell a wrong path from a file that holds no text.
Options:
--no-ocr: Skip OCR on images and scanned PDFs. Speeds up extraction when you only care about text-bearing files.--out <file>,-o <file>: Write output to<file>instead of stdout (truncates if it exists). It may not be the path being extracted — that would empty the file before reading it, and is refused — and a destination inside a folder being extracted is left out of the walk rather than extracted into itself.
findings: Triage scan findings (LLM-friendly)
piicrawler findings list --scan <id> [--verdict <unreviewed|fp|tp|all>] [--pii-type <type>...] [--limit <n>] [--offset <n>] [--context <none|surrounding|full>] [--json]
piicrawler findings mark --scan <id> --verdict <fp|tp> <--match <id>|--text <pii_type> <term>|--file <id>>
piicrawler findings mark --scan <id> --from-json <path|->
piicrawler findings unmark --scan <id> --verdict <fp|tp> <--match <id>|--text <pii_type> <term>|--file <id>>
piicrawler findings stats --scan <id> [--json]
Pulls scan findings as JSON and writes triage verdicts back into the local database. Designed for agentic LLM workflows: a model can fetch unreviewed matches with findings list --json, classify each one, and post the verdicts back via findings mark --from-json. The same scan_false_positives table is shared with the TUI's review mode and the web UI, so verdicts written from the CLI show up everywhere. For an end-to-end walkthrough that wires these commands to an LLM, see Agentic Triage.
Get the --scan ID from piicrawler scans.
findings list
Returns matches for a scan. JSON when stdout is not a TTY (or when --json is passed); a compact table otherwise. The default --verdict filter is unreviewed, which is the natural starting point for a triage loop. Pass --verdict all to see everything.
Options:
--scan <id>: Required. Scan ID to read from.--verdict <unreviewed|fp|tp|all>: Filter by verdict status. Defaults tounreviewed.--pii-type <type>: Restrict to one or more PII types (e.g.--pii-type ssn --pii-type email). Repeatable. A name the scanner does not know is refused, with the valid names listed and the types this scan actually recorded, so a typo cannot come back as an empty list that reads like "no PII of that type". Custom detector slugs (regex-<label>,terms-list-<name>,us-tax-forms) are accepted when the scan recorded them.--limit <n>/--offset <n>: Page through large result sets.--offsetdefaults to0. The page is read from the database as a page, so pulling 50 matches costs the same on a scan with ten million findings as on one with ten. Omit--limitto list every match;--limit 0is refused, because a page holding nothing prints the same empty list a clean scan prints and nothing on screen says which one you are looking at. Note that this is the opposite ofreport --max-findings, where0means "every finding" — a report always has a body to cap, a page never has fewer than one row to be worth asking for. An--offsetpast the last match is an ordinary empty last page and is not an error.--context <none|surrounding|full>: Controls which context fields appear in JSON output.surrounding(default) returns the small snippet around the match (best signal-to-noise for an LLM);fulladds the line and paragraph contexts;noneomits thecontextkey entirely for cheap counts.--json: Force JSON output even on a TTY.--redact: Apply format-preserving redaction tossnanddobmatches before emission. Replaces every digit in the term with a hash-derived digit (keeping dashes, slashes, and other separators), and rewrites every occurrence of those plaintext terms incontext.surrounding/line/paragraph. The substitution table covers every distinct ssn/dob term recorded across the whole scan (not just the paginated subset). Each context string is additionally swept with the SSN/DOB detection regexes to catch dashed SSNs and date shapes that were never recorded as matches (placeholders the original scanner filtered, formats outside the anchored detector). The substitution is deterministic per database (a secret is generated on first use and stored locally), so the same input always maps to the same output and an LLM still sees "this value repeats N times" without reading the original digits. Other PII types pass through unchanged;file_pathis not redacted. Intended for the case where you must send context to an external LLM endpoint; if you can run a local or private-cloud model, use that instead. See Agentic Triage → Redacting sensitive terms.
JSON shape (one object per match):
{
"id": 42,
"scan_id": 1,
"file_id": 7,
"file_path": "/data/hr/2025.csv",
"pii_type": "ssn",
"term": "123-45-6789",
"start": 10,
"end": 21,
"context": { "surrounding": "...Employee SSN: 123-45-6789 filed on..." },
"verdict": "unreviewed"
}
verdict is one of unreviewed, false_positive, or true_positive. With --context full the context object also includes line and paragraph. With --context none the context key is omitted.
findings mark and findings unmark
Writes (or removes) a verdict. Three scope selectors mirror the TUI's review-mode keys (f/t for the current match, F/T for the whole file, plus an implicit text scope when the same term repeats across files):
--match <id>: Apply to a single match by its ID (fromfindings list).--text <pii_type> <term>: Apply to every match in the scan with the given(pii_type, term)pair, across all files. Use this when a term is unambiguously a false positive (e.g. an example SSN like000-00-0000) regardless of where it appears. The pair is matched exactly, including type, case and surrounding space, so a term no match in the scan carries is refused rather than stored as a rule that could never fire. If the scan holds that term under another type or another spelling, the error names it.--file <id>: Apply to every match in a file by its file ID. The TUI'sF/Tshortcut writes this scope.
Specificity wins on read: a match-scope verdict overrides text scope, which overrides file scope. So mark --text ssn 000-00-0000 --verdict fp followed by mark --match 42 --verdict tp leaves match 42 as a true positive while every other match of 000-00-0000 stays a false positive.
unmark takes the same selectors and the same --verdict <fp|tp> flag. The FP and TP verdicts are stored as separate rows, so you must say which one you're clearing.
Bulk path (--from-json <path|->):
piicrawler findings mark --scan 1 --from-json verdicts.json
piicrawler findings mark --scan 1 --from-json - # read JSON from stdin
The input is a JSON array of entries; each carries its own verdict and exactly one selector:
[
{ "match_id": 42, "verdict": "fp" },
{ "match_id": 43, "verdict": "tp" },
{ "text": { "pii_type": "ssn", "term": "000-00-0000" }, "verdict": "fp" },
{ "file_id": 7, "verdict": "tp" }
]
The whole batch applies in a single SQLite transaction. If any entry is malformed (contains more than one selector, or names a match, file or term this scan does not hold) the CLI exits non-zero, says which entry, and no verdicts are written. --verdict on the command line is ignored for --from-json; only the per-entry verdict matters.
A selector that names nothing in the scan is refused. A match ID from another scan, a file ID mistyped for a match ID, or a term with no match in this scan cannot record a verdict, so the command says so and exits non-zero rather than reporting a write that did not happen.
A --from-json file that is not there is refused before the batch is opened, with a suggestion when a nearby name looks like the one you meant, and status 2 — the same error a mistyped path gets from scan. - reads stdin and is never treated as a path.
Output (single mark):
{ "marked": 6, "verdict": "fp" }
The count is the number of matches the verdict covers, not the number of rules written: one --file selector can cover hundreds of findings and one --match selector covers exactly one. It is the same number the web UI's "Mark all N as false positives" button shows. unmark reports its count under "unmarked", and a scope that carried no verdict to clear reports 0 without failing.
Output (bulk):
{ "marked": 6, "entries": 4, "by_verdict": { "fp": 5, "tp": 1 } }
marked counts matches and entries counts the lines of the input file, so a batch of four entries covering six matches reports both.
findings stats
Tallies verdicts for a scan. Useful for an LLM to check "how many unreviewed remain" before/after a triage pass.
Options:
--scan <id>: Required. Scan ID.--json: Force JSON output.
JSON shape:
{
"scan_id": 1,
"totals": { "unreviewed": 120, "false_positive": 32, "true_positive": 8 },
"by_pii_type": [
{ "pii_type": "ssn", "unreviewed": 50, "false_positive": 10, "true_positive": 5 },
{ "pii_type": "email", "unreviewed": 70, "false_positive": 22, "true_positive": 3 }
]
}
Example: agentic triage loop
# 1. Pull unreviewed findings as JSON
piicrawler findings list --scan 42 --json --limit 50 > batch.json
# 2. Have your LLM classify each one and emit verdicts.json
# (e.g. [{"match_id": 101, "verdict": "fp"}, ...])
# 3. Apply the verdicts atomically
piicrawler findings mark --scan 42 --from-json verdicts.json
# 4. Check progress
piicrawler findings stats --scan 42
register <email>: Register this installation
piicrawler register <email> [--timeout <secs>] [--force]
Registers this machine so the licensed commands (scan, watch, and the web UI's scans) will run. It performs the same two-step flow as the TUI and web UI, but headlessly, which makes it the right choice for servers, containers, and CI where no browser or interactive terminal is available.
- A verification link is emailed to
<email>. - The command then polls until you click that link, and on success stores the signed license and registered email in the local database (
~/.piicrawler/). Nothing is written until verification completes.
If the server already trusts the address (you verified it before, or you are registering a second machine), it says so and finishes in well under a second instead of waiting for a link that was never sent.
piicrawler register [email protected]
The signed license is tied to this machine's OS and architecture, so run register on each machine you install on (the same email can register multiple machines).
Options:
--timeout <secs>: How long to wait for you to click the verification link before giving up. Default: 600 (10 minutes). The server is always asked at least once, however short the timeout, so an address it has already verified registers immediately rather than waiting. On timeout the command exits with status1and no license is stored; click the link and runregisteragain to resume.--force: Register again even when a valid license is already present. Without it,registerdetects an existing valid license, prints the masked registered email, and exits0without contacting the network.
On success the command prints the address it registered and the day your license stops working:
Registered successfully as [email protected]. You're all set — run a scan to get started.
Your trial ends 2026-09-11 (6 days left).
Registering starts a free 7-day trial, so that second line is the trial's last day. A purchased license is perpetual and reads Your license does not expire. instead. Running register again on a machine that already has a working license repeats the line rather than re-registering:
Already registered (yo*@example.com). Your trial ends 2026-09-11 (6 days left). Pass --force to register again.
piicrawler doctor shows the same date on its Registration line, so you can check how long you have at any time without re-running register.
Progress and prompts are written to stderr; the success lines are written to stdout. An obviously malformed email is rejected immediately, before any network call.
update: In-place binary upgrade
piicrawler update [--yes] [--force]
Checks downloads.eligian.com for the latest build for your platform, compares it to the running binary, and (if newer) downloads the matching archive, verifies its SHA-256, extracts the binary, and atomically swaps it into place. Your database, license, terms lists, and triage verdicts in ~/.piicrawler/ are never touched.
Options:
--yes,-y: Skip the interactive[y/N]confirmation. Required for scripted upgrades: with nothing attached to stdin to answer the prompt,updaterefuses and says so rather than reading end-of-file as "no".--force,-f: Reinstall even when the local build is already at or newer than the published one.
update checks that it can write where the binary lives before it downloads anything, so a system-wide install you do not have permission to replace fails in a second, naming the directory, instead of after sixty-odd megabytes. The installed file keeps the permissions it already had, so a deliberately private install (0700) is not widened by upgrading it, and a failed install leaves nothing behind.
Behaviour by platform:
- Linux and macOS swap the binary atomically. Any already-running
piicrawlerprocesses keep using the old binary until they exit; new invocations pick up the new build. - macOS installs the archive's standalone
piicrawler-clicopy — the one signed on its own, and the one the quickstart puts at/usr/local/bin/piicrawler. Run from insidePIICrawler.app,updatestops instead of swapping the bundle's executable, which would break the signature Gatekeeper checks; replace the whole.appto update it. - Windows cannot overwrite a running
.exe, so the live binary is renamed topiicrawler.exe.oldnext to the original and the new bytes are written at the original path. The.oldfile can be deleted once nopiicrawler.exeprocesses remain.updatealways pulls the Azure-Trusted-Signing-signedpiicrawler-cli-windows-signed.zip, so the binary you end up with after an upgrade is signed by the same publisher as your initial install.
If your platform or architecture is not currently published (e.g. Linux ARM), update exits with a friendly error pointing you to the download page.
version: Print version
piicrawler version
piicrawler -V
piicrawler --version
Prints the running build's version string (e.g. 26.0507.1432) to stdout and exits. Stdout-only output keeps it pipe-friendly for shell scripts that need to read the version.
doctor: Check the installation
piicrawler doctor
Runs a quick health check and prints the result, one line per item. It is the fastest way to answer "is this set up correctly?" before a scan, or to gather facts for a support request. It reports:
- Version and how long ago the binary was built.
- Database location and the number of saved scans.
- Registration status, including the masked email if registered and where the license stands:
trial ends 2026-09-11 (6 days left)on a trial,license does not expireon a purchased perpetual license. An unregistered install shows a red ✗ and the exactpiicrawler registercommand to fix it. On a machine provisioned with a deployedlicense.lic,doctorreads that file the way every other command does, so the line readsregistered as …; if the file was rejected it names the file instead of sending you toregister. - Web UI: the address
piicrawler servewould bind to, whether this machine can listen on it, whether it would be reachable from other machines with no password set, and which setting chose it. - OCR availability (the text-detection and text-recognition models are built into the binary, so this is always present).
PII Crawler doctor
✓ Version 26.0525.0433 (built 2h ago)
✓ Database ~/.piicrawler/piicrawler.db (4 saved scans)
✓ Registration registered as ja**@example.com, trial ends 2026-09-11 (6 days left)
✓ Web UI will listen on 127.0.0.1, this machine only
✓ OCR built in (reads text from images and scanned PDFs)
Everything looks good. Try: piicrawler ~/Documents
The Web UI line names the setting when one was chosen, so you know where to change it: (Settings → Bind address) for a value saved in the web UI, (PIICRAWLER_BIND) for one in the environment, and nothing at all for the 127.0.0.1 default. A wildcard address reads all interfaces — reachable from other machines on your network instead of this machine only, which is worth checking on a shared or public network.
Checking the address is more than reading it back. doctor asks the operating system whether it will hand the address over, which is the one part you cannot tell by looking. A laptop that has moved networks since a LAN address was saved under Settings → Bind address still shows that address in the web UI, and the web UI is exactly what stops starting:
PII Crawler doctor
✓ Version 26.0525.0433 (built 2h ago)
✓ Database ~/.piicrawler/piicrawler.db (4 saved scans)
✓ Registration registered as ja**@example.com, trial ends 2026-09-11 (6 days left)
✗ Web UI cannot listen on 192.168.1.10: Cannot assign requested address (os error 99)
No network interface on this machine has the address 192.168.1.10, so nothing can listen there.
That address came from Settings -> Bind address in the web UI, where it is stored in the database -- not from anything on this command line.
Serve on this machine only. That overrides the setting for one run and puts the web UI back in reach, so you can change it:
piicrawler serve --bind 127.0.0.1
✓ OCR built in (reads text from images and scanned PDFs)
Resolve the items marked ✗ above, then re-run `piicrawler doctor`.
doctor does not check whether the port is free. The commonest reason serve's port is busy is that PII Crawler is already running on it, which is not a problem to report; if the port really is taken by something else, serve says so when you start it.
The closing line is contextual. When everything passes and you have not saved any scans yet, it points you at piicrawler demo; once you have saved scans, it suggests scanning a real folder. If any check fails it tells you to resolve the items marked ✗ and re-run.
doctor is diagnostic, so it always exits 0; problems show as a red ✗ in the output rather than a non-zero status. Color follows the --color setting.
That holds even when the problem is the data folder itself. PII Crawler keeps its database in a .piicrawler folder inside your home directory, and it cannot always create one: a file may already be sitting under that name, the account may not be allowed to write there, or there may be no home directory at all (which is what a cron job or a service account with no HOME set looks like). doctor still prints the whole report, marks the Database check ✗, names the folder and says what to do about it, and leaves the checks it reads out of the database as unknown (database unavailable) rather than guessing:
PII Crawler doctor
✓ Version 26.0525.0433 (built 2h ago)
✗ Database could not use the data directory: File exists (os error 17)
Something that is not a directory is already at:
/home/dana/.piicrawler
Move or remove it, then try again.
• Registration unknown (database unavailable)
• Web UI unknown (database unavailable)
✓ OCR built in (reads text from images and scanned PDFs)
Resolve the items marked ✗ above, then re-run `piicrawler doctor`.
Every other command needs that folder before it can do anything. They stop rather than run, and print the same explanation and remedy first:
Something that is not a directory is already at:
/home/dana/.piicrawler
Move or remove it, then try again.
Error: Could not open the PII Crawler data directory
Caused by:
File exists (os error 17)
That guidance goes to standard error, so --json output is untouched: a command that writes machine-readable results to stdout still writes only the error object there.
completions <shell>: Shell completion script
piicrawler completions bash|zsh|fish|powershell|elvish
Prints a tab-completion script for the named shell to stdout. The script is generated from the live command definition, so it always covers the current subcommands, flags, and value choices. Pipe or redirect it into the location your shell loads completions from. The accepted shells are bash, zsh, fish, powershell, and elvish.
Typical installation:
# Bash (user-local; create the directory once if needed)
piicrawler completions bash > ~/.local/share/bash-completion/completions/piicrawler
# Zsh (anywhere on your $fpath, e.g. a personal completions dir)
piicrawler completions zsh > ~/.zfunc/_piicrawler
# Fish
piicrawler completions fish > ~/.config/fish/completions/piicrawler.fish
# PowerShell (append to your profile)
piicrawler completions powershell >> $PROFILE
After installing, restart the shell (or re-source the file) so completions load. Because the script is regenerated from the binary, re-run the command after upgrading to pick up new flags.
A flag whose value comes from a fixed set completes that set: tab after --color, --log-level or --progress offers the modes, and tab after --only, --exclude-type or --pii-type offers the detector slugs, so you do not have to remember whether it is credit-card or creditcard. Flags that name a file, and the paths you pass to scan, complete as filenames.
help
piicrawler help
piicrawler -h
piicrawler --help
Prints the built-in usage summary to stdout. Pass a subcommand name (e.g. piicrawler help scan) for command-specific help.
Output
On an interactive terminal, piicrawler <path> ends with the report card on stderr and writes nothing to stdout. When stdout is piped or redirected (or you pass --json), it instead prints a JSON document to stdout with one entry per scanned file. Each entry has the shape:
{
"file_path": "/absolute/path/to/file.pdf",
"findings": [ ... ],
"full_names": [ ... ],
"char_count": 12345,
"error": null
}
If extraction fails for a file, error is set to a short message and findings is empty. Container scans return the same shape, one entry per archive member.
See PII Data Types for the structure of individual findings and Results Storage for the database schema used by serve, watch, and the TUI.
Errors under --json
A command that prints JSON keeps stdout for its machine-readable results and puts human-facing text on stderr, which is what makes a pipeline like piicrawler dsar "Jane Doe" --json 2>/dev/null | your-script work. When such a command fails, the failure is written to stdout as well, as a single object:
{
"error": "No scan with ID 999. Run `piicrawler scans` to list scan IDs."
}
The message is the sentence that also goes to stderr, and the {"error": ...} shape is the one the web API returns, so a script written against either surface reads the other. Exit codes are unchanged, and a successful run never prints an error object, so a caller can branch on the exit code or simply look for the error key.
The object follows the results, command by command:
findingsandlistsprint JSON to stdout when given--jsonor when stdout is piped or redirected, so both ways of calling them report a failure there. This is what lets an agent script capture stdout without passing the flag.scanfollows the same rule as its result dump:--json, or a piped stdout with no--out/--summary/--saveto claim it.scansanddsarprint JSON only when asked, so only--jsongets the object.watchwrites a stream of violations rather than one document, so its failures join that stream as one line instead. See The daemon could not start.
Everywhere else, and whenever the flag is absent, stdout stays empty on failure and the message is on stderr alone. Mistakes in the command line itself (an unknown flag, a missing value) are still reported as usage text on stderr with exit code 2, since there is no valid invocation to produce JSON for. A path that is not there is not one of those: the command is valid and names a file that is missing, so it gets the object like any other failure, with the did-you-mean on stderr and exit code 2.
An unregistered or expired copy gets the object too. This is the failure a scheduled job is most likely to meet, because a job running under its own account has no registration of its own and a trial runs out on a date, so it is worth handling by name:
{
"error": "This copy of PII Crawler is not registered. Register for free with: piicrawler register [email protected]"
}
{
"error": "The PII Crawler license on this machine expired on 2026-08-18. Registered as ja**@example.com; registering it again will not renew the trial. To keep scanning, buy a license: https://www.piicrawler.com/?utm_source=piicrawler-app#pricing"
}
Both exit 1. The welcome and the trial-ended notice still print in full on stderr for whoever is watching the terminal.
Piping into head, less, or another pager. Closing the reader early is fine. piicrawler demo 2>&1 | head and quitting a pager partway through the output both stop the scan quietly, the same way any other command-line tool behaves, and the shell reports exit status 141. Earlier versions treated the closed pipe as a crash and printed a panic message. A demo stopped this way leaves its temporary sample directory behind, which the next demo run sweeps up.
Environment variables
PII Crawler reads a small set of environment variables on startup. None of them are required for normal use; they're escape hatches for daemonized, headless, or noisy deployments.
Logging
PIICRAWLER_LOG_FILE: Write the log to this path instead of the per-platform default. Useful when collecting logs from a fleet in one place. Failure to open the file is logged and the binary continues without file logging.PIICRAWLER_LOG_FILTER: Acceptsoff,error,warn,info,debug, ortrace. Defaults toinfo. Set todebugortraceto surface SMB protocol traces during a network-share scan, oroffto record nothing. The--log-levelglobal flag overrides this when both are set, so a machine that has turned logging off can still be asked for a log one run at a time.
Credential store (headless / CI)
PII Crawler keeps SMB credentials encrypted under a credential password. See Scan an SMB Network Share → How credentials are protected. For headless deployments where you can't type the password interactively, set one of:
PIICRAWLER_CRED_PASSWORD: Auto-unlock the credential store with this password the first time anything in the session touches a stored credential. The password is never written back to disk.
If it is not set, the credential store stays locked until the user unlocks it via the TUI, the web UI, or POST /api/cred/unlock.
Credential store HTTP endpoints
A running piicrawler serve instance exposes the same unlock flow over HTTP, for orchestration tools that manage the server rather than its environment. The endpoints sit behind the regular web-login session, so log in first:
GET /api/cred/status: returns{is_set, is_unlocked}.POST /api/cred/unlockwith{password}: unlocks the running server.POST /api/cred/lock: locks the store again without restarting.POST /api/cred/passwordwith{password}(first-time set) or{current_password, password}(change). This is also the way to change an existing credential password; the change is instant however many credentials are stored.
Scan passwords
PIICRAWLER_SMB_PASSWORD: Password forsmbscans when--usernameis given without--password.PIICRAWLER_IMAP_PASSWORD: Password forimapscans when--passwordis omitted.
Crash reporting
SENTRY_DSN: If PII Crawler crashes in the wild we use this to receive crash reports. It's the only way for us to know if we have a crashing bug in the wild. Please leave this set if possible. Reports go to Sentry, not to Eligian's own servers, and carry the error chain, the version, the machine's hostname, the OS and CPU architecture, and the on-disk paths of the binary and the libraries it had loaded — see Security → Error Reporting for the full list. Nothing a scan read is in them: no file contents, no matches, and no path of a file you scanned. Setting it to an empty string (SENTRY_DSN="") disables error reporting entirely; it is the only outbound call a scan could otherwise make, since the update check runs only when you runpiicrawler update.
Examples
Scan a directory and save the findings to a file:
piicrawler ~/Downloads --workers 8 > findings.json
Stream a large directory scan to a JSONL file (results are appended as each file finishes, so memory stays flat):
piicrawler /srv/shared --workers 8 --out findings.jsonl
jq -c 'select(.findings | length > 0)' findings.jsonl
Export findings as CSV for spreadsheet review (auto-detected from the .csv extension):
piicrawler scan ~/share --workers 8 --out report.csv
Force CSV format when the output file has a non-standard extension:
piicrawler scan ~/share --out report.txt --format csv
Scan a single archive without OCR and pipe to jq:
piicrawler backups/2026-04.zip --no-ocr --quiet | jq '.[] | select(.findings | length > 0)'
Scan only for card numbers and SSNs, skipping the noisier detectors:
piicrawler scan ~/share --only ssn,credit-card
Get a quick per-type count for triage or a CI log instead of the full JSON dump, and break the build if anything turns up:
piicrawler scan ~/share --summary --fail-on-findings
Enable the New Zealand and Australian region detectors (off by default) by turning on the full detector set:
piicrawler scan ~/share --all
Scan a source tree for spreadsheets and PDFs only, skipping dependency and VCS directories and any file over 25 MB:
piicrawler scan ~/project \
--exclude node_modules --exclude '\.git/' \
--ext csv,xlsx,pdf \
--max-size 25
Add custom detectors: an employee-ID regex, a keyword list, and US tax-form detection:
piicrawler scan ~/share \
--regex 'empid=EMP-\d{6}' \
--terms-file ./medical-terms.txt \
--detect-forms
Save a scan to the database, then generate a report and triage findings from its ID:
SCAN_ID=$(piicrawler scan ~/share --save --name "Q3 audit" --quiet)
piicrawler report "$SCAN_ID"
piicrawler findings list --scan "$SCAN_ID" --json
Scan an authenticated SMB share for spreadsheets and PDFs only, capping bandwidth, and report on it (the password is read from the environment so it stays out of shell history):
export PIICRAWLER_SMB_PASSWORD='…'
SCAN_ID=$(piicrawler smb fileserver Finance -u alice --domain CORP \
--subfolder HR/2025 --ext csv,xlsx,pdf --bandwidth-mbps 50 --quiet)
piicrawler report "$SCAN_ID"
Watch two directories with a webhook and a policy file:
piicrawler watch /srv/uploads /srv/exports \
--webhook https://alerts.example.com/piicrawler \
--policy ./policies.toml \
--debounce 1000
Check an existing share for PII, then keep watching it:
piicrawler watch /srv/uploads --policy ./policies.toml --scan-existing
Fail a CI job if any PII is found for a given person:
piicrawler dsar "Jane Doe" --assert-clean
List the recorded scans to find one to report on:
piicrawler scans
Generate an HTML report for scan ID 42:
piicrawler report 42