Codexa is offline-first, but it still writes local operator data: logs, saved chats, pending chat-save WAL records, pilot-run batches, benchmark artifacts, and optional JSON exports. Keep config.yml local and use config-example.yml as the shareable template.
No operator-data egress. Corpus content, queries, embeddings, and the index never leave the host, and there is no telemetry. Online Wikipedia enrichment and LLM provider calls are strictly opt-in, and the default index→extract→chunk→sparse-search path opens no network sockets. This is enforced as an executable invariant in tests/test_todo_rspurp1_no_egress_2026_06_24.py (RS-PURP-1), which blocks every outbound socket and exercises both the default path and Paddle construction. The decision record — including the rejected cloud-default and telemetry options — is ADR 0004.
One inbound exception: the first-run model fetch. allow_model_download (top level) defaults to true, so an embedding model missing from <persist_dir>/hf_cache is fetched once from the Hugging Face Hub. The fetch is inbound-only and carries no operator data; a warm cache opens no socket, because loads run under HF_HUB_OFFLINE=1. To prove zero egress, set allow_model_download: false and pre-cache with codexa fetch embedding — a cache miss then raises ModelNotCachedError naming that command instead of reaching the network. See ADR 0014.
PaddleOCR also starts with PaddleX model-source connectivity checks disabled: its models must already exist in the local cache, otherwise the engine soft-fails with a remediation event and Tesseract remains available.
All knobs live under privacy: in config.yml.
| knob | default | effect |
|---|---|---|
log_retention_days |
7 |
Rotating JSONL logs under <store.persist_dir>/logs/; 0 keeps indefinitely. |
pending_wal_retention_days |
30 |
Pending chat-save WAL replay/cleanup window. |
pending_wal_max_bytes |
1048576 |
Safety cap for pending chat WAL storage. Records evicted to stay under it are appended to <wal>.dropped and logged as session_wal_records_evicted, never discarded silently. |
chat_retention_days |
0 |
Saved chat retention; 0 keeps indefinitely. |
pilot_retention_days |
0 |
Pilot-run retention; 0 keeps indefinitely. |
query_history_enabled |
true |
Browser localStorage recent-query history. Set false to clear and stop writing it. |
query_history_retention_days |
0 |
Browser recent-query history retention; 0 keeps indefinitely. Positive values clear expired local history on page load. |
redact_log_paths |
false |
When enabled, absolute POSIX, Windows, and UNC paths are reduced to basenames in structured log/export path fields. |
redact_export_content |
false |
When enabled, JSON exports keep metadata/provenance but set session name, query, answer, and passage body fields to null. |
Use these when a workstation changes owner, a corpus is removed, or you need to minimize local operator data:
codexa sessions purge --config config.yml --dry-run
codexa pilot purge --config config.yml --dry-runBoth commands use their configured retention period when --older-than N is
omitted (privacy.chat_retention_days and privacy.pilot_retention_days,
respectively). Review the dry-run count, then repeat the command with --yes
to purge without an interactive confirmation.
Logs are plain files under <store.persist_dir>/logs/ — beside the index they
describe, so removing a store removes its logs with it. CODEXA_LOG_DIR overrides
the location, and with no store configured they fall back to $XDG_STATE_HOME/codexa/logs
(else ~/.local/state/codexa/logs). Benchmark output is separate and unaffected:
scripts/bench_indexer.py still writes logs/bench/<timestamp>/ under the repository
root. Remove old run directories when they are no longer needed.
Browser recent-query history is held in localStorage["codexa_query_history_v1"].
Set privacy.query_history_enabled: false and reload the UI once to remove it
from that browser profile, or clear the site's storage in the browser.
In generation-published mode, each persisted assistant extra carries
generation_id and root_binding_digest as both keys or neither; flat-mode turns omit
both. The forward-compatible extra envelope preserves this pair through the local JSON
and SQLite session stores, pending-save WAL records and replay, and JSON session export.
These are metadata fields, so redact_export_content does not remove them.
The persistence builder validates only that both values are nonempty strings; a real
generation lease supplies the canonical form. Its root digest is 64 lowercase hexadecimal
characters: SHA-256 of a domain-separated byte string containing the decimal device and
inode numbers from the canonical (st_dev, st_ino) root binding. It contains no corpus
path or corpus content. It is not secret and not anonymous merely because it is path-free.
The digest is not
portable across a copied/recreated root and is not a content hash.
The pair is matching-only metadata. Its only persisted consumer is prior-pin snapshot matching, which rejects reuse when the saved pair differs from the active snapshot. The values cannot reopen a generation, recreate a read lease, or authorize access. They are not durable authority, not user content, and must not be displayed as assistant text or treated as a transcript field. The saved-turn render and persistence path does not emit the pair as log fields; other generation lifecycle logs can intentionally name a generation. A later exact-generation read still acquires and validates a fresh filesystem lease.
The local session and WAL files can still contain raw queries, answers, passages, and paths.
For saved-session export, content and path redaction retains the identity pair because it
is path-free metadata; that does not make an export share-safe by itself. Review the
remaining metadata before sharing; result-only exports never invent the pair because they
do not serialize assistant extra.
The executable contract covers the path-free digest, both-or-none turn persistence, actual WAL journal/replay and result-only export exclusion, JSON/SQLite session round trips, and privacy-aware session export.
Saved-session JSON exports and result JSON exports include full passage text by default. With privacy.redact_log_paths: true, source paths are minimized. With privacy.redact_export_content: true, session name, query, answer, and passage body fields are set to null so operators can share metadata/provenance snapshots without raw corpus content.
Wikipedia/ZIM passages carry license, license_url, and attribution fields in rendered/search export surfaces so CC BY-SA provenance survives outside the app. See Wikipedia integration.
scripts/bench_indexer.py redacts sampled corpus paths by default: sample_paths.txt stores corpus-relative paths when possible, and generated per-rung configs are minimized after each rung. Pass --keep-absolute-paths only when you explicitly need reusable absolute-path artifacts for a private local investigation.