PrivaCI Error Codes & Message Format¶
This page is the authoritative reference for every exit code PrivaCI can return and the format of every error message it prints. It is written for two audiences:
- Operators / customers running PrivaCI in CI or Kubernetes who need to branch on exit codes and act on failures.
- AI tooling and contributors that parse errors, generate runbooks, or add new error sites in the code.
Where this is enforced: the base exception
PrivaCIErrorrenders every error in the Context + Cause + Remediation format below and carries the matchingexit_code. Tests live intests/test_errors.py.
1. Message format: Context + Cause + Remediation¶
Every operator-facing error follows this structure:
Context: <what the engine was doing>
Cause: <why it failed — never contains PII or secret values>
Remediation: <the exact next step to take>
See: docs/error-codes.md#<anchor>
Example¶
Context: Validating the resolved anonymization salt
Cause: Resolved salt has length 5, minimum is 32.
Remediation: Generate a new salt with `privaci gen-salt`.
See: docs/error-codes.md#exit-code-4-missing-or-invalid-salt
How the CLI emits this: all commands raise
PrivaCIErrorsubclasses and let the centralized boundary (privaci.cli._errors.run_cli) print the block to stderr and exit with the error'sexit_code. Commands never callsys.exitdirectly.
Rules¶
- Context names the operation, not the function. ("Loading
mask-rules.yaml", not "in
load_config"). - Cause is factual and PII-free. Never echo column values, salts, rows,
or full connection strings. Redact to
<redacted>. - Remediation is a concrete command or config change, and links back here.
- Terse internal raises MAY pass only a context string; the structured format
activates as soon as a
causeorremediationis supplied.
Raising a structured error (contributors)¶
from privaci.errors import ConfigError
raise ConfigError(
"Loading mask-rules.yaml",
cause="Unknown action type 'shuffle' on column users.email",
remediation="Use a supported action; see docs/configuration.md#actions.",
)
2. Exit code catalogue¶
Indexable pages: each exit code also has a dedicated page under Exit code pages (generated) for search and deep linking.
| Code | Name | Meaning | Writes occurred? |
|---|---|---|---|
0 |
Success | Run (or --dry-run) completed successfully |
Maybe (run) / No (dry-run) |
1 |
Generic error | Unexpected/uncategorized failure | Maybe |
2 |
Pre-flight failure | Target not empty, schema mismatch, missing privileges, catalog error | No |
3 |
Config validation failure | mask-rules.yaml invalid or commercial-only action requested |
No |
4 |
Missing or invalid salt | Salt unresolved or shorter than 32 chars | No |
5 |
License / entitlement failure | License Manager entitlement check failed (plugin-installed) | No |
6 |
Drift detected | Schema/config drift detected (plugin-installed) | No |
130 |
Interrupted | SIGINT/SIGTERM; checkpoints flushed | Partial (checkpointed) |
CI scripts SHOULD treat 2, 3, and 4 as "operator must fix input" and 1
as "open a bug or check logs".
Exit code 0: Success¶
The run completed. For run, the target holds masked data and a run.end
event with status: succeeded was emitted. For dry-run, pre-flight passed
and no rows were written.
Remediation: none.
Exit code 1: Generic error¶
An unexpected error not covered by a specific code. Raised by
PrivaCIError, MaskingError, and StateError by default.
Common causes
- A bug in the engine or a plugin.
- An unhandled database or network condition mid-run.
privaci verifyfound at least one failing check (a masked column that did not change, passthrough drift, row-count mismatch, duplicate unique values, or an orphaned foreign key). Failing checks are printed to stderr; warnings alone do not trigger this exit.- A
when:CEL evaluation failed for a row (non-bool result, runtime error, or elapsed budget overrun). Errors omit cell values. ner_maskran on a non-empty cell but SpaCy /en_core_web_smwas unavailable (normally caught earlier at validate exit 3 or preflight exit 2).
Remediation
- Re-run with
--log-level debug. - Capture the full
Context/Cause/Remediationblock. - For
verifyfailures, fix the offending rule inmask-rules.yaml(see Verifying a run) and re-run the mask job. - File an issue with the redacted error and engine version (
privaci --version).
Exit code 2: Pre-flight failure¶
A pre-flight or schema-DDL check failed. Raised by PreflightError and
CatalogError. Pre-data DDL failures happen before streaming; post-data
DDL failures happen after rows are loaded — the run is marked failed (not
succeeded) and is resumable once the fault is cleared.
Common causes
- Target database contains user tables and
on_existing_data: fail(default) inschema_mode: replicate. schema_mode: assume_existingwithon_existing_data: failand an in-scope target table that already has rows (empty prebuilt tables are allowed; absence of identity/SERIALcolumns does not change this).schema_mode: assume_existingfound a missing table or incompatible column type on the target (seeschema.validation_failedin_privaci.audit_log).passthrough_copy: require_binaryand a passthrough table is not binary-COPY eligible (column order/type mismatch).ner_maskis configured or auto-detected but SpaCy /en_core_web_smis not available (pip install 'privaci[nlp]'or change the action).- Source or target unreachable, or insufficient privileges.
- Missing
CREATE SCHEMAprivilege for_privaci. - A table referenced in config does not exist in the source.
- Pre-data or post-data DDL failed on the target (permissions, missing
dependencies, or invalid object definitions). Catalog identifiers in engine
DDL are escaped via
quote_pg_identifierbefore interpolation. privaci resumefound no resumable run, or the config, source database, or salt changed since the interrupted run. The error names which one drifted; a run is resumable while its status isin_progress,interrupted, orfailed. Restore the original input or start a fresh run withprivaci run --force-restart(requireson_existing_data: truncateordrop_create).privaci resumeinschema_mode: replicatewhen the incomplete run has no persistedsource_schema_snapshot(schema cloning did not finish).privaci run --force-restartwithon_existing_data: fail(unsupported collision policy for force-restart).passthrough_copy: require_binarytogether withnull_orphan_fkson a table that references an excluded parent.passthrough_copy: require_binarytogether with any columnwhen:guard (conditional masking requires the batch/row path).
Remediation
# Inspect what pre-flight objected to:
privaci dry-run --config mask-rules.yaml
# If the target legitimately has data, choose an explicit policy:
# on_existing_data: truncate # wipe in-scope target tables first
# on_existing_data: drop_create
# then abandon incomplete runs and start clean:
privaci run --force-restart --config mask-rules.yaml
# (append is rejected in the MVP — see docs/configuration.md)
Grant schema creation if needed:
GRANT CREATE ON DATABASE privaci_target TO privaci_role;
Exit code 3: Config validation failure¶
mask-rules.yaml failed schema validation, or a plugin-only action
(ai_refine) was requested without a plugin package. Raised by
ConfigError and L3NotInstalledError.
Common causes
- Unknown field (configs use
extra = forbid). - Unknown
actiontype or missing required field for an action. action: ai_refinewithout a plugin package that registers an LLM connector.- Engine v2 reading a
version: "1.0"config withoutmigrate-config. when:references an unknown column, an unsupported type (jsonb, arrays,numeric, composites), a disallowed CEL builtin (e.g.matches,map), or exceeds size/AST limits.- Explicit
ner_maskin YAML when SpaCy /en_core_web_smis not available (pip install 'privaci[nlp]'or change the action).
Note: Level-3 is not wired into the masking path yet. Even when a
plugin package is installed and registers LLM connectors, ai_refine still
raises L3NotInstalledError (exit 3) at mask time. Connector stubs in
the plugin package fail closed if called directly; do not configure
ai_refine in production until L3 is implemented and wired.
Remediation
# Validate and see the offending field (pydantic-style path):
privaci validate --config mask-rules.yaml
# Export the JSON Schema your editor can lint against:
privaci schema config > privaci.schema.json
Exit code 4: Missing or invalid salt¶
The anonymization salt could not be resolved, or the resolved value is shorter
than 32 characters. Raised by SecretError / SecretResolutionError.
Common causes
ANONYMIZATION_SALTunset or points at a missing secret.- Resolved salt is too short.
Remediation
# Generate a strong 64-char salt and store it safely:
privaci gen-salt > .privaci-salt
chmod 600 .privaci-salt
export ANONYMIZATION_SALT=file://$(pwd)/.privaci-salt
See configuration.md (global_salt, database URLs) for
aws-sm://, azure-kv://, vault://, env://, and file:// URI schemes.
Important: changing the salt changes every deterministic fake value. Store it like a production secret and rotate deliberately.
Exit code 5: License / entitlement failure¶
A required license capability is missing, or a plugin LicenseValidator
entitlement check failed. Raised by LicenseError (and plugin entitlement
failures).
Common causes
- Config uses
when:but capabilities omitconditional_masking(community mode with an empty capability set, or a plugin that does not grant the token). - Config uses keyed actions (
hmac_hash/pseudonym) withoutkeyed_actions. - Plugin package cannot validate an active License Manager entitlement.
Remediation
- Install a plugin package whose
LicenseValidatorgrants the required capability tokens, or remove the gated fields frommask-rules.yaml. - For subscribed images: confirm the container can reach AWS License Manager and that the entitlement is active.
- See Conditional masking and Keyed actions.
Exit code 6: Drift detected (commercial)¶
The commercial drift detector found schema or config drift versus the last recorded run.
Remediation
- Review the drift report.
- Re-run with
--accept-drift(commercial) once changes are intentional.
Exit code 130: Interrupted by signal¶
The engine received SIGINT/SIGTERM. It flushed in-flight checkpoints, closed
connections, emitted run.end with status: interrupted, and exited.
Remediation
# Resume from the last committed checkpoint (same source + config):
privaci resume --config mask-rules.yaml
3. Quick reference for CI¶
privaci run --config mask-rules.yaml
code=$?
case "$code" in
0) echo "ok" ;;
2) echo "pre-flight failed — see target state / privileges"; exit 2 ;;
3) echo "config invalid — run 'privaci validate'"; exit 3 ;;
4) echo "salt problem — run 'privaci gen-salt'"; exit 4 ;;
130) echo "interrupted — run 'privaci resume'"; exit 130 ;;
*) echo "unexpected error $code — check logs"; exit "$code" ;;
esac