Skip to main content

PII Checker User Guide

PII Checker detects personal data and credentials in Agent inputs and outputs. It returns a structured verdict, produces safe evidence and optional redacted text, and records sanitized Security Events for audit and Observability correlation.

V2 phase 1​

The V2 RPM provides Rust agent-sec-cli and agent-sec-daemon. Use the RPM built by ./scripts/rpm-build.sh agent-sec-core-v2 in a dedicated Linux validation environment; this migration does not switch existing Agent hosts or their services to V2. After installing that RPM, start its system service, which creates the protected runtime directory:

sudo systemctl start agent-sec-core

Scan through the daemon's default socket:

agent-sec-cli scan-pii --text 'contact alice@company.cn' --source manual

The default endpoint is /run/agent-sec-core/daemon.sock. A nonempty AGENT_SEC_DAEMON_SOCKET overrides it, and agent-sec-cli --socket /absolute/path/to/daemon.sock scan-pii ... takes precedence. The CLI does not start a daemon or fall back to Python. Files and stdin are read by the CLI; the daemon receives text, never a caller-selected input file or rules path. Empty text is valid. --text-stdin aliases --stdin. Completed pass, warn, and deny scans exit 0; scan/connection failures exit 1, and CLI usage errors exit 2. deny is a finding classification, not a PDP authorization decision.

Without --max-bytes, V2 does not truncate input by default. An explicit positive byte limit keeps a valid UTF-8 prefix; malformed UTF-8 fails. Request business data must fit the V2 4 MiB budget, including JSON escaping and envelope overhead. Requests have a separate 32 KiB propagation allowance; responses remain limited to 4 MiB. Oversize input fails explicitly. Spans count Unicode characters, matching Python positions, rather than UTF-8 bytes or UTF-16 units.

V2 keeps the existing top-level result fields and adds evidence metadata under summary:

FieldMeaning
execution_statuscompleted or failed
coverage.statuscomplete, partial, or unavailable
coverage.reasonsSafe codes for truncation, invalid rules, matching limits, or scan failure
input_sha256SHA-256 of text received by the detector; never a claim about omitted input
scanned_input_sha256, scanned_bytesDigest and byte length of the prefix actually scanned
scanner_versionDetection semantics version; V2 currently reports 2.0.1
ruleset_idIdentity of the scanner version and immutable built-in/custom rule configuration

bytes_scanned retains the V1 prefix counter and can include an incomplete UTF-8 tail excluded from scanned_bytes. A partial result may still be pass: verdict only aggregates observed findings. Check coverage before treating evidence as complete.

V2 bounds retained finding details while matching and builds redacted text incrementally. The returned report fits within 512 KiB of formatted JSON. Oversize reports retain the first finding of each type/severity and omit raw evidence. summary.findings_truncated=true records reduced findings or evidence. Verdict, summary.total, category/severity counts, digests and scan coverage still describe all findings detected in the scanned input. Hook notices use these totals and indicate omitted details. The returned findings are then representative evidence, not an exhaustive list of locations.

If the full redacted_text still exceeds that budget, V2 returns [REDACTED: output size limit] for the entire text and sets summary.redacted_text_omitted=true; it never returns a partly redacted remainder. Both markers are absent when false. Output reduction alone does not make scan coverage partial, and completed scans still exit 0. The transport-free PiiScanner applies the same bound; it continues counting and classifying all matches after detail or redaction output is reduced.

The daemon admits at most two concurrent PII scans without queuing; another request receives scan_busy. A worker holds its slot until execution actually exits, including after a transport timeout. The executor checks existing cancellation at entry and the inherited deadline between matching operations and during long scanning loops. These failures return scan_cancelled or scan_deadline_exceeded, with ok=false, verdict error, coverage unavailable, and CLI exit 1, through the same audit projection and Finalizer. A single regex call remains bounded by its engine backtracking limit; it cannot be forcibly interrupted by the deadline. No general cancellation framework or retry queue is added. The transport or CLI timeout can win the response race; in that case the caller receives a transport failure while the worker still finalizes its safe scan error and releases its slot.

Detection semantics version​

summary.scanner_version identifies detection behavior independently of the AgentSecCore package version and RPC schema. V2 reports 2.0.1 in successful and failed scan reports and audit results; built-in findings use engine regex_v2, custom findings use fancy_regex. ruleset_id includes this version plus the built-in patterns and custom configuration, so rule-engine changes remain traceable even when the YAML bytes are unchanged. A CI commit SHA identifies the tested build; it is not the detector's semantics version.

Version 2.0.0 retains the 11 supported types, confidence scoring, Unicode spans and redaction, with these intentional changes from V1:

  • JWT candidates include compact empty claims ({}), which V1's minimum payload length missed. Large integer claims and deeply nested JSON objects are checked structurally without inheriting Python integer-conversion or recursion limits. Malformed JSON is still rejected; detection does not authenticate the signature or authorize use of the token.
  • Chinese ID date and checksum validation normalize decimal digits consistently, including fullwidth dates/check digits and X/x, while retaining the original text and character spans.
  • All-zero card placeholders are rejected even though they satisfy Luhn. A finding does not confirm that a card number has been issued; version 2.0.1 adds the structural checks below.
  • Custom patterns use the native dialect described below. A behavior difference from Python alone does not make a valid V2 rule invalid or its coverage partial.

The retained V1 corpus checks unchanged behavior; separate positive, negative and resource-limit cases define V2 changes. These checks cover the supported formats, not a guarantee of zero errors on arbitrary data. Short/unlabeled secrets, formats outside the 11 types, and contextual ambiguity still need application-specific rules and representative examples.

Supported card-number formats​

Version 2.0.1 and the retained Python detector require a complete numeric format, a supported network prefix paired with its allowed length, then Luhn. Prefixes and lengths follow these pinned card-type definitions:

NetworkSupported digit counts
Visa16, 18, 19
Mastercard16
American Express15
Diners Club14, 16, 19
Discover16, 19
JCB16, 17, 18, 19
UnionPay14, 15, 16, 17, 18, 19

Continuous digits are supported. Grouped values use one consistent ASCII space or hyphen: Amex uses 4-6-5, 14-digit Diners uses 4-6-4, and other supported combinations use four-digit groups from the left with a final remainder (for example 4-4-4-4-3). Repeated/mixed separators and unconventional splitting are rejected. Existing Unicode decimal normalization and character spans are retained. Every network requires Luhn; all-zero values fail the network-prefix check.

The detector validates the entire run of digits, spaces and hyphens. It never retries inside an invalid run: 1-4111111111111111 is rejected, while card-4111111111111111 and /cards/4111111111111111/ remain detectable. Trailing spaces/hyphens without a following digit stay outside the finding. Commas and newlines separate expressions; two card numbers joined only by spaces form one rejected run. The same rule removes version/timestamp path false positives without exempting paths.

This is a deliberately bounded format/network set, not an issued-card lookup. Formats outside it can be missed, and unrelated numeric identifiers can still satisfy every check. Severity, confidence scoring and host policy behavior are unchanged.

Use the bundled Skill​

With agent-sec-cli installed and the pii-checker Skill available to the Agent, start the daemon first when using V2. Ask it to check a specified file for personal data or credentials, or to generate a redacted copy. The Skill reports redacted evidence and does not rewrite the input file unless requested. A completed scan with no findings is not a guarantee that the content contains no sensitive information.

The Skill ships in agent-sec-skills for RPM installations and in the ANOLISA raw package's shared skills directory. Cosh-NG discovers that directory; the OpenClaw and Hermes adapters declare pii-checker for delivery into their respective Skill directories. Other Agent installations must make the Skill available through their own discovery paths.

Scan text​

Provide exactly one input source: inline text, standard input, or a UTF-8 file.

# Inline text
agent-sec-cli scan-pii --text "contact alice@example.com"

# Standard input
printf '%s' 'token=secret-value-1234567890' \
| agent-sec-cli scan-pii --stdin --redact-output

# UTF-8 file
agent-sec-cli scan-pii --input ./agent-output.txt --format text

Useful options:

OptionPurpose
--format json|textSelect structured JSON or human-readable output; default is json
--redact-outputInclude redacted_text; the input file is never modified
--include-low-confidenceInclude findings below the default confidence threshold
--raw-evidenceInclude raw evidence in local CLI output only
--max-bytes NScan at most N UTF-8 bytes and mark the result as truncated
--source SOURCELabel the audit context, such as user_input or tool_output

Supported source labels are user_input, model_input, tool_input, tool_output, model_output, observability, manual, and unknown.

Built-in detection​

The built-in detector combines regex matching, format validation, and context-based confidence adjustment.

CategoryTypesDefault severity
Personal dataemail, phone_cn, credit_card, cn_idwarn
Credentialsprivate_key, bearer_token, api_key, jwtdeny
Alibaba Cloud credentialsaliyun_access_key_id, aliyun_access_key_secretdeny
Secret fieldsgeneric_secret_fielddeny

Credit card, Chinese ID, and JWT candidates are validated before becoming findings. Surrounding security keywords can increase confidence, while fixture markers such as example, dummy, test, and sample can lower it. Findings below the default 0.5 threshold are omitted unless --include-low-confidence is set.

Email detection accepts sentence-ending ASCII periods, including an ellipsis, when the period run is followed by end of input, whitespace, or a supported sentence/closing delimiter. For example, alice@company.co.uk. produces a finding for alice@company.co.uk; the period remains outside both the finding span and the redaction replacement. Invalid domain suffixes such as .123, .c, .-bad, ._bad, and ..evil are rejected without reporting a shorter address. Reserved domains and remote identities retain their existing low-confidence handling.

Verdicts and redaction​

The scanner aggregates findings into one verdict:

VerdictMeaning
passNo finding remains after confidence filtering
warnFindings exist, but none has deny severity
denyAt least one finding has deny severity

Each finding includes its type, category, severity, confidence, span, detector metadata, and redacted evidence. --redact-output also returns a redacted copy of the scanned text. Overlapping findings are preserved, while their overlapping spans are merged and replaced once. If different spans overlap, the complete merged range is fully redacted so a shorter match cannot leave a sensitive suffix visible.

--raw-evidence is intended only for local troubleshooting. Raw evidence is never written to Security Events. Host integrations consume the same verdict and finding schema; whether a host only observes a finding or blocks an operation depends on that host's configured PII policy.

Host hook policy​

Set PII_CHECKER_HOOK_ENABLED=false to skip host PII hooks entirely. When enabled, PII_CHECKER_MODE accepts observe, warn, ask, or block and defaults to observe on hosts with native notice support. Hermes accepts only observe and block; legacy warn / ask values fall back to observe with a host diagnostic. It never injects advisories through the final assistant response. On Hermes, block can enforce a deny only at pre_tool_call. Model output is scanned for audit through post_llm_call, but it is never modified or blocked because Hermes has no plugin-usable pre-stream output gate. On other hosts, an unsupported ask or block boundary may fall back to warn; post-execution hooks never claim to undo an external side effect. The environment policy overrides Hermes/OpenClaw capability configuration. debug maps to observe, and deny maps to block. Qwen Code additionally accepts the legacy PII_CHECKER_ENABLED switch when PII_CHECKER_HOOK_ENABLED is absent.

PII_CHECKER_HOOK_ENABLED and PII_CHECKER_MODE are read by all six hosts. Two further variables are only read by some of them:

Environment variableDefaultHosts that read it
PII_CHECKER_TIMEOUT5Qoder, Codex, Qwen Code (Qwen Code caps it at 8 seconds)
PII_CHECKER_INCLUDE_LOW_CONFIDENCEfalseQoder, Qwen Code

cosh, Hermes, and OpenClaw do not read those two environment variables. Hermes supports both settings through capability configuration (timeout and include_low_confidence). OpenClaw supports only piiIncludeLowConfidence; its PII scanner CLI timeout is fixed at 10 seconds. cosh uses a fixed timeout and never requests low-confidence findings.

The host Agent reads these variables when it loads the plugin. Restart the Agent process that hosts the hook after changing them; the hook and agent-sec-core are not separate policy services.

Scanner verdict deny describes finding severity. Hook policy block controls whether the current adapter attempts enforcement.

OpenClaw input protection​

On stable OpenClaw >=2026.5.12, PII Checker uses before_agent_run to scan only the current user input in prompt. System prompts, session history, media payloads, and message metadata are excluded. A missing, non-string, or blank prompt skips scanning without falling back to other fields. It still records source=model_input to identify the model-entry hook, not full-context coverage. This checks the current input supplied by OpenClaw at run entry. It does not cover every model request inside the run or auxiliary model calls such as session title generation, and does not promise access to the original user input.

Older supported versions (>=2026.4.14), prereleases, and unrecognized versions use before_dispatch with source=user_input. This legacy path only scans the inbound text supplied by OpenClaw. Startup logs name the selected input hook and warn when using the legacy path. Only one PII input hook is registered.

before_agent_run requires plugins.entries.agent-sec.hooks.allowConversationAccess=true; the deployment script already sets it on supported hosts. In block mode, a deny result blocks submission at this gate. observe records the scan, warn logs a warning, and ask falls back to a warning because this gate has no confirmation mechanism. Scanner failures warn and fail open. The integration does not replace model input with redacted text.

A pass describes the text actually scanned. If OpenClaw has already masked a credential and the model request also contains only its masked form, failure to rediscover the original credential is not evidence of disclosure. Validate protection using the scan input and actual model request, rather than requiring a credential finding for every original user message.

before_tool_call retains its parameter checks and approval/block behavior. after_tool_call only records or warns: it cannot redact or withhold a tool result, even in block mode. Tool results can therefore enter subsequent model context; this is an existing enforcement gap. llm_output likewise observes or warns without redacting or blocking delivery. These hooks do not provide complete prevention across all model requests.

Custom regex rules​

Built-in rules ship in the binary. Custom rules are separate configuration:

RuntimeCustom fileReload behavior
V2/etc/agent-sec/pii-checker/rules.yamlValidated and compiled once at daemon startup; restart to update
V1~/.config/agent-sec/pii-checker/rules.yamlNew content is loaded on the next scan

The administrator may select another absolute V2 file with agent-sec-daemon --pii-rules /absolute/path/rules.yaml --socket /run/agent-sec-core/daemon.sock. All callers share one immutable rule set. V2 never reads the caller's HOME, chooses rules by owner, or imports old user directories automatically. The scan RPC cannot override this selection.

The YAML top level is a list. Each rule contains a unique custom type, one regex, and an optional severity.

- type: dogfood_order_no
regex: '(?<=order_no[=:])DFT-[A-Z0-9]{8}'
severity: warn

- type: dogfood_customer_token
regex: 'DFT-[A-Z0-9]{16}'
severity: deny
FieldRequiredDescription
typeYesLowercase snake_case custom type, unique within the file
regexYesOne regex expression; V2 uses native fancy-regex syntax
severityNowarn or deny; defaults to deny

The complete regex match is the finding and redaction span. Capture groups and named capture groups do not change that span. If a regex matches both a field name and its value, both are redacted. Use lookaround when the full match must cover only the value. Multiple formats for one type must be combined with regex alternation (\|); the same type cannot appear in multiple rules.

Custom findings use category custom, confidence 1.0, detector custom_rule, and engine regex in V1 / fancy_regex in V2. They are fully redacted with a stable type marker such as [DOGFOOD_ORDER_NO_REDACTED] and flow through the same verdict, policy, Security Event, and Observability paths as built-in findings.

V1 has no rules-path override. Neither runtime merges multiple rule files. V2 rules are detector configuration; they are not PAP policies and do not themselves authorize operations.

Custom rule validation and runtime limits​

The complete custom ruleset is accepted or rejected as one unit.

Limit or ruleValue
Maximum file size256 KiB
Maximum number of rules100
Maximum regex length2,048 characters
Maximum YAML nesting depth64
V2 regex group recursion limit64 parser frames, including the root
Type format^[a-z][a-z0-9_]{0,63}$
Allowed severitywarn or deny
Per-rule matching limitV1: 20 ms; V2: 1,000,000 backtracking steps
Total custom matching budget per scan200 ms
Maximum custom findings per scan100

Unknown YAML fields, duplicate types, built-in type names, invalid regexes, and regexes that match an empty string make the complete custom ruleset invalid. Other zero-length matches encountered at runtime are ignored.

Rules with deny severity run before warn rules. File order is preserved within each severity. The 100-finding limit sets truncated only when an additional valid match is omitted. V1 continues evaluating later rules; V2 stops remaining custom matching at that point and reports partial coverage. V2 checks its 200 ms budget between matching operations and does not promise to interrupt one match after 20 ms. Backtracking and other matching limits preserve earlier findings.

V2 uses the locked fancy-regex dialect directly. Valid flags such as (?i) / (?x), comments, lookaround and set operations are accepted when the engine supports them. Rules are not rewritten or rejected solely because Python would interpret them differently:

  • $ asserts the input end by default; (?m)$ also asserts line ends. \z is a strict input end, while \Z accepts the position before trailing newlines.
  • \h / \H are hexadecimal / non-hexadecimal classes. Use [ \t] for horizontal space.
  • (?i) uses the engine's Unicode case folding; it does not reproduce Python's dotted/dotless-I folding. \< / \> are word-edge assertions outside character classes.
  • Character classes support nested sets and set operations. For a named-group participation conditional, use (?(<name>)yes\|no); Python's (?(name)yes\|no) spelling has different semantics.

invalid_regex is reserved for actual parse/compile/engine-limit or load-time evaluation failures. For example, branch-reset groups and variable-length lookbehind remain unsupported. The engine counts its root frame toward group recursion: 63 nested groups are accepted, while 64 can fail; nested character classes have separate engine limits. YAML aliases and multiple documents are rejected. Matching limits report partial coverage instead of hiding skipped work.

Validate migrated custom rules against both matching and non-matching examples, including Unicode and trailing newlines, using the versioned V2 dialect. Arbitrary administrator patterns cannot be judged for business correctness by compilation alone.

V2 keeps the startup rule set for every request until restart. At the next startup, an invalid replacement disables the entire custom set; no old valid set is silently reused. V1 reloads on the next scan. Built-in detection remains active in both cases; V2 marks coverage as partial.

Custom rule status​

Every default scan includes sanitized custom rule state in summary.custom_rules:

{
"custom_rules": {
"status": "loaded",
"rule_count": 2,
"runtime_error_count": 0,
"budget_exhausted": false,
"truncated": false
}
}

status is absent when the default file does not exist (V2 explicitly selected missing files are invalid), loaded when validation succeeds (including an empty list), and invalid when reading, YAML parsing, schema validation, or regex compilation fails. An invalid status includes a sanitized error_code; loaded or invalid content may include its SHA-256 digest. Runtime counters do not contain input text or regex content. A direct scan-pii invocation also prints a sanitized invalid-configuration warning to stderr while still exiting successfully.

The current error_code values are:

Error codeMeaning
read_errorThe rules file could not be read
file_too_largeThe rules file exceeds 256 KiB
invalid_utf8The rules file is not valid UTF-8
invalid_yamlThe YAML content cannot be parsed safely
top_level_not_listThe YAML top level is not a list
too_many_rulesThe file contains more than 100 rules
invalid_rule_schemaA rule has missing, unknown, incorrectly typed, or unsupported fields
invalid_rule_typeA rule type does not match the required naming format
duplicate_rule_typeThe same custom type appears more than once
reserved_rule_typeA custom type conflicts with a built-in PII type
invalid_regexA regex is unsupported, cannot compile, exceeds engine limits, or fails load-time evaluation
regex_matches_empty_textA regex can produce a zero-length match on an empty string
load_errorV1: an unexpected loader error was handled in fail-open mode

Security Events and Observability​

Every scan follows the existing pii_scan Security Event path. Events contain the source, verdict, summary, finding type, severity, category, span, and redacted evidence. They do not contain the custom rules path, regex expressions, or raw sensitive matches.

Host hooks remain fail-open when custom rules are invalid and do not add a separate host warning. The sanitized summary.custom_rules state in the Security Event is the structured audit source for hook invocations.

Observability uses the existing trace context and input hash to correlate telemetry with the Security Event instead of storing another copy of finding details.

V2 audit and rule migration​

V2 uses the shared Action Runtime and Finalizer. Completed, partial, failed scans and authorized requests with invalid scan parameters produce one terminal scan event during normal execution. Malformed envelopes, unknown methods, authorization failures, and transport failures are handled at their respective entry boundaries. A crash or forced shutdown can prevent finalization.

Audit persists only safe request metadata, digests, rule identifiers, coverage, redacted findings, and error codes. Raw text, raw evidence, complete redacted_text, rule contents, and input-bearing exception messages are excluded. JSONL/SQLite sink health is distinct from scan success; the existing daemon startup still requires SQLite initialization. The V2 event-query CLI is not yet available.

Hooks can pass top-level --trace-context '{"session_id":"session-1"}' before scan-pii. V1 snake_case/camelCase aliases are normalized; strings are trimmed and limited to 256 characters. The shared CLI adapter stores these fields in OTel Context and propagates them through the top-level carrier. The opaque trace label is not an OpenTelemetry TraceId. PII RPC parameters do not accept traceContext; use the shared top-level traceContext / compatibility envelope. Invalid legacy trace input exits 1; ordinary CLI usage errors exit 2. UID/GID/PID always come from UDS peer credentials. agent_name is bounded caller metadata and grants no authority.

To migrate rules, review the selected V1 file, resolve conflicting type names, and explicitly place the approved YAML in the administrator-managed V2 file. Start/restart the V2 daemon, check summary.custom_rules.status, ruleset_id, and coverage, then run positive and negative samples. No automatic collection of per-user files occurs. To revert a rules change, restore the previous central file and restart. To roll back the runtime, stop the validation daemon and restore the V1 RPM/entrypoint and its independently retained user rules; V2 never modifies those V1 files.

The six Hook adapters are tested through fixed events and real Rust PII subprocesses, including observability redaction. Unmigrated observability storage is isolated in that test harness. This is Hook contract acceptance, not a live-host cutover or full PIP/PDP/PEP integration. See the two-stage design.