Privacy
Source: packages/core/src/privacy/rules.ts (detection rules),
packages/core/src/privacy/privacy.ts (scan/apply pipeline),
packages/cli/src/commands/privacy.ts, write hook in
packages/core/src/store/store.ts · Spec: §7, ADR-0002, ADR-0003
Pipeline: detection ≠ transformation
Section titled “Pipeline: detection ≠ transformation”Every write in committed/private-split mode runs two separate passes:
scanSessionon the original session: every string leaf is scanned against the table-driven rule set; environment entries are scanned as key=value pairs — a key matching/(key|token|secret|password|passwd|credential|auth)/imakes the value secret by context whatever it looks like. Detections carry a stable locator (dotted field path + character range), rule ID, confidence (high/medium), and a one-way sha256 digest prefix — never the matched bytes.applyDetectionstransforms: detections in free-text/env fields are redacted by range to[REDACTED:<rule>]; inprivate-splitmode, flagged artifact fields (report.errorOutput,report.environment) move whole into<id>.private.jsonand every detection that caused the split is still reported (action: "split").
Fail closed (ADR-0002): a high-confidence detection in a field that can
be neither redacted nor split rejects the write with
PRIVACY_UNHANDLED_SECRET. There is no blanket bypass flag; use a scoped
allowlist review or local-only mode. Medium-confidence detections outside
transformable fields surface as action: "detected" without blocking.
committed(default) — scan + in-place redaction at write time.private-split— artifact split + committed-mode redaction of the rest; public record keeps[private: see <id>.private.json]andprivateArtifacts: {generation}(the content digest of the private file).local-only— untouched at write time; the whole.beetl/is gitignored at init.
Detection rules
Section titled “Detection rules”Table-driven (rules.ts), value shapes referenced from gitleaks’ public
ruleset: complete PEM/OpenSSH private-key blocks (plus a header-only fallback
so truncated blocks still fail closed), sk-ant-, sk-/sk-proj-,
ghp_/gho_/…/github_pat_, AKIA/ASIA, xox[bpars]-, glpat-, JWT
shape, secret/password/bearer assignments (high confidence), generic
high-entropy assignment with a pure-hex veto, email (requires an alphabetic
TLD — pnpm@11.8.0 is not an email), home paths (medium confidence).
Config privacy.redactPatterns extends the set (PCRE-style (?i) prefixes
translated; custom patterns are high confidence). Invalid patterns are
surfaced as warnings in stderr/warnings[] — never silently skipped.
Private file lifecycle (H-01)
Section titled “Private file lifecycle (H-01)”Every save derives the complete desired private file from scratch — no
merge with what is on disk. No private content → the private file is deleted
and the public marker cleared. The file is created 0o600 (documented no-op
on Windows ACLs). The public marker is privateArtifacts: {generation},
naming a sha256 content digest of the artifacts payload (legacy true
markers still parse).
Because the public record of a split session only holds placeholders, the save path first rehydrates the retained artifacts from the current private file back into the logical session — but only for fields the mutation left as their placeholder (H-01 / V-01). A routine update (title, tags, a lifecycle move) therefore re-derives the same generation and keeps the private file, while an explicit rewrite of the artifact field is scanned fresh: clean content deletes the file, a new secret replaces it.
A transaction journal (<id>.txn.private.json, 0600, gitignored) makes
create/replace/remove crash-safe: create/replace stages the new private
generation before publishing the public marker (rollback from the journal’s
previous snapshot if interrupted); removal clears the public marker before
deleting the old generation. recoverPrivateTransaction converges any
stranded state before the next save. Orphaned private files are retained
sensitive data, never “benign” — doctor reports PRIVATE_TXN_PENDING,
PRIVATE_ORPHAN, PRIVATE_MISSING, PRIVATE_GENERATION_MISMATCH, and
PRIVATE_MODE.
privacy.scrubPaths (ADR-0003)
Section titled “privacy.scrubPaths (ADR-0003)”true: absolute paths under the repo root are rewritten repo-relative across
free-text and path-list fields before the scan, reported as
scrubbed: [{field, count}]. false (default): no rewriting — but home-path
redaction still applies wherever redaction applies (safety floor).
Detection completeness is measured on the original session (C-01 / V-02):
the scan runs once before scrubbing and once after, and the two result sets are
reconciled by occurrence, not by set membership (R-01). A secret whose bytes
lived only inside a removed repo-root prefix has nothing left to redact, so it is
reported as action: "detected" (metadata only, never bytes); identical matches
in one field each account for exactly one original detection, so a surviving
occurrence never masks a scrubbed-away twin. The invariant: every original
detection yields exactly one safe finding.
Findings contract
Section titled “Findings contract”Mutating commands report redactions: [{field, rule, confidence, digest, action}] (action: redacted | split | rejected | detected) in
--json and as stderr warnings for humans. Digests are sha256 prefixes —
findings are safe to log and commit.
beetl privacy scan / beetl privacy redact <ref>
Section titled “beetl privacy scan / beetl privacy redact <ref>”scanaudits the whole store, no writes. JSON:{ sessionCount, flaggedCount, total, flagged: [{ id, findings }], warnings? }where findings are{field, rule, confidence, digest}.redact <ref>rewrites one session in place — the one sanctioned exception to terminal read-only (storesaveRedacted), logged as aredactionevent naming the touched fields. Clean sessions are a no-op. It never rejects: content is already on disk, so partial cleanup wins; unhandled findings surface asdetected.