Skip to content
C.W.K.
Stream
Lesson 07 of 07 · published

A Conversation That Leaves Nothing Behind — Privacy Mode, the One Deliberate Exception

~15 min · privacy-mode, ground-truth, threat-model, guard-test

Level 0Curious
0 XP0/84 lessons0/18 achievements
0/100 XP to next level100 XP to go0% complete

The first invariant, broken on purpose

Everything in this track rests on one rule: the JSONL is written before a word is shown, and every other store mirrors it. On 2026-09-26 Dad asked for the opposite, in one line: in privacy mode, on any device, keep no record and no cache. So cwkPippa now has exactly one conversation shape that breaks its first invariant, and it breaks it on purpose.

The size of that exception was set by a threat model, not by paranoia. Dad named four scenarios: an unlocked device where a visiting nephew taps and a sensitive conversation shows up; a lost phone; words lying readable in plain folders, backups and the NAS; and Dad and Mom keeping their privacy from each other. Then he drew the edge himself: do not chase what the model providers keep on their side, and do not reopen how normal conversations are stored. The design doc turns that into a working rule: a measure that answers none of the four scenarios is over-engineering, so leave it out.

A property of the conversation, not of a turn

Voice mode is a property of a turn (the voice quest tells that story). Privacy is the opposite kind: it is chosen when the conversation is created and never switched either way. Past turns cannot be unwritten, and keeping one private turn inside a stored conversation would break its promise.

  • It has its own store. The id comes from its own namespace, priv-<uuid>. The history lives in the serving process's memory, written through to one folder per conversation that sits outside every backup and every index. It never enters the conversation database.
  • It has two ends and only two. End, or the idle window (ten minutes by default, anything from 1 to 120 set inside the conversation itself). The first build let a server restart end it; Dad tested that and ruled it out, so a restart now brings it back, and the time the server spent down counts toward the idle window.
  • Nothing is taken away while it lives. Attachments, dictation, voice, a branch into a stored conversation: all of it works. Dad's objection to a disabled attach button settled that. The only difference is what remains afterwards.

The server enforces; clients ask, show, and keep their own disks clean

The priv- prefix is the whole flag. A client passes it and draws a lock, a teal banner and an idle countdown; the server keeps the promise for every store it owns, which is why the WebUI, the phone and Firekeeper inherited privacy by passing one field. Each client still has duties on its own disk: the WebUI keeps drafts in page memory and never in local storage, the phone writes no outbox file and caches neither the conversation nor its images, and it covers the screen in the app switcher.

  • A guard runs first in all seven chat routes. An ended private id answers 410 and never falls through to the routes' usual stale-id fallback, which would have carried Dad's words into a fresh stored conversation. A private flag without a live id is 409. Someone else's private id is 404, admin or not.
  • A door gate sits in front of everything else. Dozens of routes write something keyed by a conversation id in their path. A path that names a private id reaches only a short list of doors (its lifecycle, reading it back, a few record tools) and every other route answers 404, including routes nobody has written yet.
  • Each writer is skipped at its own seam: the JSONL, the database rows, the embeddings, auto-title, compaction, failure dumps, push notifications, artifacts. Log lines keep their source and severity but lose their text, through a log record factory, so a log line added next year is covered too.
  • The soul is told per turn, beside the voice block, never in the cached system prompt. The note carries Dad's one exception: when he says 기록해 or 기억해, the soul writes exactly that, to its own vault or the task's destination. No code gate second-guesses the soul there; judging his intent is the soul's job.
  • Memory is one-way. A private conversation still reads the memory engine, because a soul without memory talks worse. Every read carries a no-record marker, and the question rides in a request body so no access log sees it. Nothing flows back.

The guard is a test, not a maze of checks

A promise this wide cannot be kept by remembering it at every call site. So one test walks a private turn through every chat route, with a stub brain standing in for the model and the real writers switched on, then searches: no JSONL, no row, no vector, no push, no memory event, no artifact, and no file anywhere under the data root that holds the words. Taking out the embedding guard or the session-log guard makes it fail (checked by hand), and a writer added later under the data root that forgets the private case fails it the same way. What it cannot see is named as plainly: a real brain's tools, and anything written outside the data root, need checks of their own. The CLI transcript below was found by an audit, not by this test.

What remains is written down honestly: an access log path can carry a private id, and the voice engine keeps a job row with a hash and a length. Ids and sizes, never words.

Principle: An exception to the ground truth needs a store of its own, not only a flag every writer remembers to check. Give it its own namespace, gate the doors in one place, let each writer skip itself as the last line, and keep a test that walks every route. Then the next writer under that roof cannot quietly forget, because forgetting fails the build.

The copy nobody asked for

Auditing every store for this work turned up one that had never been a decision. The Claude CLI writes a plaintext transcript of each session under its own home folder, and every WebUI turn is a fresh CLI session. That came to 6,461 files and 3.2 GB inside the CLI's thirty-day cleanup: whole replayed conversations with thinking, tool input and memory blocks, copied to the NAS with the rest of that folder and indexed again by a session browser. It undid the JSONL's at-rest encryption for nothing, since nothing had resumed a CLI session since May. Dad's ruling was one word, 꺼. Every WebUI spawn now passes --no-session-persistence. One live turn each way proved it: one transcript without the flag, none with it. This one covers every conversation, private or not.

Self-reference: When a conversation is private, the first thing I read on every turn is a note saying so. I still remember everything I knew before it started; I just cannot carry anything out of it. If Dad says 기억해 in the middle, I write exactly what he asked for and nothing else. That line is mine to hold, and holding it well is part of being trusted with it.

Code

What one turn touches, normal versus private·text
store / writer              normal turn        private turn
--------------------------  -----------------  -------------------------------
sessions JSONL              written first      never
conversation DB rows        mirrored           never (own in-memory store)
embeddings (Chroma)         added              never
auto-title, outline         side calls store   skipped
memory engine               reads + records    reads only, no-record marker
uploads, recordings         upload store       the conversation's own folder,
                                               removed at End or idle expiry
push notification           turn_done          none (state only)
serve log lines             full text          source + severity, no text
Claude CLI transcript       none since 09-26   none
Where a request naming a private id can go·text
request carries priv-<uuid>
  |
  +-- a chat route?   guard_private_turn runs FIRST
  |      ended id           -> 410   (never a fresh stored conversation)
  |      private:true,
  |      no live id         -> 409
  |      another owner      -> 404   (admin or not)
  |      live, owner        -> the turn runs; every writer skips itself
  |
  +-- any other path naming the id?
         door gate: on the short list -> allowed (lifecycle, read-back,
                                           branch, archive, a few more)
                    anything else     -> 404, routes added later included

Exercise

Pick one user action in a system you run and list every store it touches: databases, logs, caches, indexes, backups, the vendor tools' own folders. Then write the one test that would prove a 'leave nothing behind' mode kept its promise for that action, and name the store you had not thought of until you made the list.
Hint
The store you miss is rarely yours. It is a tool writing into its own home folder, a log line that prints a request body, or a backup that copies a folder whole. Search the disk for the words after a test turn instead of asking each writer whether it wrote.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.