Skip to content

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 PrivaCIError renders every error in the Context + Cause + Remediation format below and carries the matching exit_code. Tests live in tests/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 PrivaCIError subclasses and let the centralized boundary (privaci.cli._errors.run_cli) print the block to stderr and exit with the error's exit_code. Commands never call sys.exit directly.

Rules

  1. Context names the operation, not the function. ("Loading mask-rules.yaml", not "in load_config").
  2. Cause is factual and PII-free. Never echo column values, salts, rows, or full connection strings. Redact to <redacted>.
  3. Remediation is a concrete command or config change, and links back here.
  4. Terse internal raises MAY pass only a context string; the structured format activates as soon as a cause or remediation is 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 verify found 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_mask ran on a non-empty cell but SpaCy / en_core_web_sm was unavailable (normally caught earlier at validate exit 3 or preflight exit 2).

Remediation

  1. Re-run with --log-level debug.
  2. Capture the full Context/Cause/Remediation block.
  3. For verify failures, fix the offending rule in mask-rules.yaml (see Verifying a run) and re-run the mask job.
  4. 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) in schema_mode: replicate.
  • schema_mode: assume_existing with on_existing_data: fail and an in-scope target table that already has rows (empty prebuilt tables are allowed; absence of identity/SERIAL columns does not change this).
  • schema_mode: assume_existing found a missing table or incompatible column type on the target (see schema.validation_failed in _privaci.audit_log).
  • passthrough_copy: require_binary and a passthrough table is not binary-COPY eligible (column order/type mismatch).
  • ner_mask is configured or auto-detected but SpaCy / en_core_web_sm is not available (pip install 'privaci[nlp]' or change the action).
  • Source or target unreachable, or insufficient privileges.
  • Missing CREATE SCHEMA privilege 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_identifier before interpolation.
  • privaci resume found 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 is in_progress, interrupted, or failed. Restore the original input or start a fresh run with privaci run --force-restart (requires on_existing_data: truncate or drop_create).
  • privaci resume in schema_mode: replicate when the incomplete run has no persisted source_schema_snapshot (schema cloning did not finish).
  • privaci run --force-restart with on_existing_data: fail (unsupported collision policy for force-restart).
  • passthrough_copy: require_binary together with null_orphan_fks on a table that references an excluded parent.
  • passthrough_copy: require_binary together with any column when: 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 action type or missing required field for an action.
  • action: ai_refine without a plugin package that registers an LLM connector.
  • Engine v2 reading a version: "1.0" config without migrate-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_mask in YAML when SpaCy / en_core_web_sm is 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_SALT unset 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 omit conditional_masking (community mode with an empty capability set, or a plugin that does not grant the token).
  • Config uses keyed actions (hmac_hash / pseudonym) without keyed_actions.
  • Plugin package cannot validate an active License Manager entitlement.

Remediation

  • Install a plugin package whose LicenseValidator grants the required capability tokens, or remove the gated fields from mask-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