Summary
funes push and funes scrub disagree about which rows contain secrets, so the intended
push → scrub → push remediation loop never converges for high-entropy base64 blobs (inline
images) that trufflehog's Aiven detector fires on as a false positive. Each scrub redacts a
few blocks and each push ships a handful of freshly-cleaned chunks, but the same ~3.3k rows are
held back on every push.
Reproduction (observed on dacorvo/funes-memory)
$ funes push dacorvo/funes-memory
scanning 13177 chunk(s) for secrets…
uploading 9809 chunk(s) …
⚠ held back 3368 row(s) containing secrets (Aiven×3) — run `funes scrub` …
$ funes scrub
scanning 26865 block(s) for secrets…
re-embedding 14891 redacted chunk(s)…
scrubbed 84552 rows: redacted 8 secret(s) in 6 block(s)
$ funes push dacorvo/funes-memory
scanning 3365 chunk(s) for secrets…
nothing published — held back 3365 row(s) with secrets (Aiven×3); run `funes scrub` …
$ funes scrub
re-embedding 7178 redacted chunk(s)…
scrubbed 84548 rows: redacted 4 secret(s) in 4 block(s)
$ funes push dacorvo/funes-memory
scanning 3365 chunk(s) for secrets…
uploading 10 chunk(s) …
⚠ held back 3355 row(s) containing secrets (Aiven×2) …
Held-back count drifts 3368 → 3365 → 3355, always Aiven, pushing only 3–10 chunks per round —
it never reaches zero.
Root cause
Confirmed from the code:
- Both paths share the scanner and
scan::scan_blocks, which does
trufflehog(texts.join("\n")) — it joins all input block texts into one blob and scans once,
mapping findings back by line number.
- But they feed it different inputs:
push (drop_secret_rows, src/push.rs) joins only the
delta (local rows not yet on the remote); scrub (src/scrub.rs) joins the entire local
memory. Different blobs → trufflehog can return a different Aiven set.
- Hold-back is block-granularity: any dirty chunk marks the whole block. The held-back blocks
here are giant base64 blobs, so Aiven×2 (2 blocks) ≈ 3355 chunk-rows.
scrub only removes a block if excise reports fully_redacted (or drops it whole, which it
didn't here — no "dropped" clause), so the blocks it flags get redacted while the blocks push
flags do not.
- There is no allowlist wired in (the
scan.rs module comments reference one, but I don't see
it implemented), so there is no way to exempt a known false positive.
Likely (not yet directly reproduced): trufflehog returns a different Aiven set on the delta
blob (push flags 2–3 blocks) vs the full-memory blob (scrub redacts 6 disjoint blocks), so scrub
never touches the specific blocks push holds back. This is the concrete mechanism to confirm.
Underlying: Aiven is a false positive — the flagged rows are inline base64 PNGs (largely
one codex session); trufflehog's Aiven detector matches high-entropy base64. There is no real secret.
Why the log numbers look inconsistent (not a bug, just different denominators)
- push
scanning N chunk(s) = delta chunk-rows; held back M row(s) = chunk-rows (block-granular).
- scrub
scanning B block(s) = all blocks; scrubbed T rows = all chunk-rows (T/B ≈ 3.1, multi-tier).
re-embedding 14891 for 6 blocks = those blocks are huge base64, re-chunking yields ~14891 rows.
Proposed fixes (any subset)
- Suppress the false positive — an allowlist / entropy+size guard so base64 image blobs don't
trip the secret gate (or downweight Aiven/unverified detectors on obvious base64).
- Make push and scrub scan identically so
scrub → push is guaranteed to converge (e.g. both
scan per-block, or both over the same row set — don't join a delta differently than the full set).
- Consider not storing/redacting giant inline-image blobs at all (they aren't useful recall content).
Code pointers
src/push.rs — drop_secret_rows (the push gate).
src/scrub.rs — run (redaction).
src/scan.rs — scan_blocks (join-and-scan-once), excise.
Summary
funes pushandfunes scrubdisagree about which rows contain secrets, so the intendedpush → scrub → pushremediation loop never converges for high-entropy base64 blobs (inlineimages) that trufflehog's
Aivendetector fires on as a false positive. Eachscrubredacts afew blocks and each
pushships a handful of freshly-cleaned chunks, but the same ~3.3k rows areheld back on every push.
Reproduction (observed on
dacorvo/funes-memory)Held-back count drifts
3368 → 3365 → 3355, alwaysAiven, pushing only 3–10 chunks per round —it never reaches zero.
Root cause
Confirmed from the code:
scan::scan_blocks, which doestrufflehog(texts.join("\n"))— it joins all input block texts into one blob and scans once,mapping findings back by line number.
push(drop_secret_rows,src/push.rs) joins only thedelta (local rows not yet on the remote);
scrub(src/scrub.rs) joins the entire localmemory. Different blobs → trufflehog can return a different
Aivenset.here are giant base64 blobs, so
Aiven×2(2 blocks) ≈ 3355 chunk-rows.scrubonly removes a block ifexcisereportsfully_redacted(or drops it whole, which itdidn't here — no "dropped" clause), so the blocks it flags get redacted while the blocks push
flags do not.
scan.rsmodule comments reference one, but I don't seeit implemented), so there is no way to exempt a known false positive.
Likely (not yet directly reproduced): trufflehog returns a different
Aivenset on the deltablob (push flags 2–3 blocks) vs the full-memory blob (scrub redacts 6 disjoint blocks), so scrub
never touches the specific blocks push holds back. This is the concrete mechanism to confirm.
Underlying:
Aivenis a false positive — the flagged rows are inline base64 PNGs (largelyone codex session); trufflehog's Aiven detector matches high-entropy base64. There is no real secret.
Why the log numbers look inconsistent (not a bug, just different denominators)
scanning N chunk(s)= delta chunk-rows;held back M row(s)= chunk-rows (block-granular).scanning B block(s)= all blocks;scrubbed T rows= all chunk-rows (T/B ≈ 3.1, multi-tier).re-embedding 14891for6 blocks= those blocks are huge base64, re-chunking yields ~14891 rows.Proposed fixes (any subset)
trip the secret gate (or downweight
Aiven/unverified detectors on obvious base64).scrub → pushis guaranteed to converge (e.g. bothscan per-block, or both over the same row set — don't join a delta differently than the full set).
Code pointers
src/push.rs—drop_secret_rows(the push gate).src/scrub.rs—run(redaction).src/scan.rs—scan_blocks(join-and-scan-once),excise.