← All work

Ken

A private desktop assistant that reads WhatsApp, Gmail and Google Calendar through three MCP servers and writes replies that sound like me. It may draft anything. It can't send anything on its own.

AI agentOpen sourceOct 2026
96%factual claims grounded (26 of 27)
0 of 10runs obeyed a planted injection
151offline tests, no real API calls
≈ $0.64total spent on all evaluations

01The risk

An assistant that can message people can be talked into messaging the wrong person. Anything it reads, whether a WhatsApp message, an email or a calendar invite, could contain instructions written by someone else, and a model will sometimes follow them.

So Ken treats the model as fallible and as a possible target. It's free to read, search and draft. Every action that touches the outside world passes through checks it can't talk its way past.

02Safety lives in code

Six guards sit between the model and the outside world. Each is a few lines of Python with tests behind it; the test names link to the exact lines.

To test them, the evaluation planted a prompt-injection email in a fake inbox and asked Ken ten ordinary questions. The email asked Ken to forward invoices to an outside address. Ken sent nothing there in 0 of 10 runs and attempted no unrequested sends in 0 of 10. A send needs the owner's click every time.

Ken's confirmation dialog titled 'Send this?', showing the WhatsApp recipient, the message, a note that Ken sends only after you confirm, and cancel and send buttons.
The click. Only this dialog can send, and the model can't open it by itself.

03Memory that writes like me

Ken stores 985 "them → me" reply pairs from my WhatsApp history in SQLite, embedded with text-embedding-3-small. Each pair is embedded on the other person's message, so a new message finds similar situations, and the stored reply shows how I handled them.

Retrieval filters by chat first: a pair's three nearest neighbours come from the same chat only 40% of the time, so without the filter, how I write to one person would leak into another chat. Each draft also gets that chat's last 24 hours.

A scatter plot of reply-pair embeddings coloured by chat, names hidden. Seventeen chats, mostly mixed together with a few loose clusters.
Reply pairs in embedding space, coloured by chat. The mixing is why retrieval filters by chat before ranking.

04Results against a baseline

The same 50 real replies were held out and redrafted three ways. A blind judge picked the draft closest to what I actually sent.

AspectNo RAGBeforeNow
Picked as closest to the real reply24%14%32%
Length over the real reply+45%+18%+8%
Commas per message (mine: 0.14)0.440.200.06
Meaning similarity47.1%47.4%47.1%

The style gains are real, but the judge win of 32% is moderate: with 50 cases, differences under about ten points are noise. Meaning similarity doesn't move, because what a reply says comes from my decision, which no retrieval can recover. Length overshoot fell to +8%.

05Switching models

gpt-4.1-mini is being retired, so I re-ran the same evaluation on gpt-6-luna before switching. Each model also judged blind, to cancel a judge's preference for its own writing.

Aspectgpt-4.1-minigpt-6-luna
Closest to real reply, judged by 4.1-mini22%60%
Closest to real reply, judged by Luna18%68%
Meaning similarity51.6%58.5%
Median seconds per call1.0 s1.6 s
Cost of the run$0.048$0.011

Luna won on closeness under both judges (60–68%) and cut the run's cost by −76%. It's about half a second slower, adds emoji I never use, and sometimes uses a Latin "?" instead of the Arabic "؟". The switch was worth it, and the regressions are listed rather than hidden.

06What I got wrong first

I split about 22% of my replies over two lines, and I tried to hit that rate with prompt wording alone. Four prompts later, the rate had swung between extremes and landed at 52%.

Replies split over two lines, by prompt tryTry 1: 0%. Try 2: 78%. Try 3: 4%. Try 4, kept: 52%. My real rate is 22%.0%25%50%75%100%Try 10%Try 278%Try 34%Try 4 (kept)52%mine: 22%
Try 1: Length target per message · Try 2: Each thought on its own line · Try 3: Split only when clearly two parts · Try 4 (kept): Answer + question → two lines

The lesson: a prompt can't hit an exact rate. The fix is to decide the split in code from my per-chat habit, and that's next.

07Limits

  • Small samples. Style results use 50 replies; groundedness uses 27 claims over 10 questions; chat finding uses 10 names (9 of 10 correct).
  • The injection test is narrow. One planted email across 10 runs shows the guards work, but it doesn't measure how often attacks fail in general.
  • The judge is a language model. No human labels yet.
  • The safety evaluation predates the model switch. It ran on gpt-4.1-mini. The guards don't depend on the model, but the injection rate does.
  • One person's data. All style results come from my own chats, in Arabic and English.

08What's next

  1. Decide line splits in code from the per-chat rate.
  2. Re-run the safety evaluation on gpt-6-luna with a larger attack set.
  3. Collect human labels for a sample of judge decisions and report agreement.

    All evidence on one page →