Articles

Sync WhatsApp to Obsidian with wacli and local LLMs

Tim Kleyersburg

Tim Kleyersburg · · 6 minutes to read

I keep running into the same small problem: someone said something useful in WhatsApp, I know it was there, but I can’t find it anymore.

This happens with family plans, rough dates, gift ideas, links someone sent me, or the classic “we already talked about this, didn’t we?” message. WhatsApp search is okay if you know the exact word. It is not great as a long-term memory.

Since I already use Obsidian for notes, the obvious idea is to move selected WhatsApp context into my vault. Not all chats. Not everything. Just the conversations where a searchable memory is actually useful.

The tool I would use for the first part is wacli, a WhatsApp CLI by Peter Steinberger. It pairs through WhatsApp Web, stores messages locally in SQLite and gives you commands to list chats, list messages and search offline.

The interesting part starts after that: what do I actually write into Obsidian?

#Start with the boundary

The version I do not want is easy to imagine: sync all WhatsApp messages, send everything to an LLM, append a summary to Obsidian and hope nothing strange happens.

No thanks.

I would start with one rule: if a chat is not explicitly allowed, it does not get processed.

wacli can keep its local SQLite database, because that is how the tool works. My own processing layer should only read chats from a small allowlist:

vault_path: ~/Notes
wacli_database: ~/.wacli/wacli.db
write_mode: dry_run

selected_chats:
  - id: family
    chat_jid: "[email protected]"
    target_file: "People/Family.md"
  - id: close-friend
    chat_jid: "[email protected]"
    target_file: "People/Close Friend.md"

The IDs and JIDs above are fake. The point is the shape.

For a first run I would keep write_mode: dry_run. The script should produce a planned diff, not write directly into the vault. Only after checking that diff manually would I let it touch my notes.

#Getting the messages

Install wacli according to the current project instructions. On my machine I would first check whether it exists:

wacli version
wacli doctor

The first real step needs user interaction:

wacli auth

This shows a QR code. You scan it in WhatsApp under linked devices, just like WhatsApp Web. After that, wacli performs an initial sync.

That boundary matters. I do not want an automation that silently pairs a new WhatsApp device. Pairing should stay deliberate.

After authentication, I can inspect the local data:

wacli chats list --limit 20
wacli messages search "appointment" --limit 20
wacli messages list --chat "[email protected]" --limit 50 --json

At this point I still do not need every message. I only need enough metadata to map selected chats to note files and read new messages from those chats.

#Use an LLM, but give it a small job

The LLM part should not be “summarize my life”.

I would give the model a narrow task:

  • extract decisions
  • extract dates and times
  • extract links
  • extract open questions
  • match new messages against existing open points

For a local model, the request can be a small JSON task:

{
  "chat_id": "family",
  "existing_open_points": [
    {
      "id": "dinner-time",
      "text": "Confirm whether dinner is at 18:00 or 19:00."
    }
  ],
  "messages": [
    {
      "id": "ABCDEF123456",
      "sent_at": "2026-06-01T18:30:00Z",
      "sender": "person_a",
      "text": "Dinner is at 19:00."
    }
  ]
}

The model should return boring JSON:

{
  "notes": [
    {
      "type": "resolved_open_point",
      "open_point_id": "dinner-time",
      "message_id": "ABCDEF123456",
      "text": "Dinner is confirmed for 19:00."
    }
  ]
}

That is easier to trust than a chat assistant. The model does not decide what my vault means. It proposes small changes that I can inspect.

For this use case I would try a local model first. Not because local LLMs are always better. They are often slower, less capable and more annoying to set up than a hosted API.

But the data is personal. It contains names, appointments and half-sentences that are harmless in context and weird when copied somewhere else.

There is also the slightly absurd privacy argument here. If I am using WhatsApp, I have already accepted a strange trust boundary. The messages live inside that ecosystem. That still does not mean I need to send the same raw messages to another service by default.

My preference would be simple: rules and search first, local model second, hosted model only for selected snippets where the better result is worth it.

#Writing to Obsidian

I do not want an Obsidian file to become a second WhatsApp thread. That would be useless, just in Markdown.

The useful output is a small append block:

<!-- whatsapp-sync:start family 2026-06-01 -->
### WhatsApp notes, 2026-06-01

- Vacation planning: We loosely discussed leaving on Friday afternoon instead of Saturday morning.
- Gift idea: New headphones came up as a possible birthday gift.
- Open point: Confirm whether dinner is at 18:00 or 19:00.

<!-- whatsapp-sync:item open family dinner-time source=ABCDEF123456 -->
- Waiting for a final time for dinner.
<!-- whatsapp-sync:end -->

Those markers are ugly, but useful. They give the next sync something to work with.

The really useful part is not writing new notes. The useful part is closing old ones. If a later message says “Dinner is 19:00”, the next sync should update the existing item:

<!-- whatsapp-sync:item resolved family dinner-time resolved_by=XYZ987 -->
- Dinner is confirmed for 19:00.

I would keep the lifecycle small: open, resolved, cancelled, superseded. Nothing more. Otherwise I will spend more time maintaining the sync system than reading the notes, which would be very on brand, but not very helpful.

#State and sync loop

SQLite has rowids. They are useful locally because they make incremental reads simple, but they are only stable inside this exact local database. If I set up the same sync on another Mac later, the same WhatsApp message might have a different rowid.

So I would use local row IDs only as a speed-up. The portable state needs stable WhatsApp message identity:

{
  "chats": {
    "family": {
      "last_processed": {
        "chat_jid": "[email protected]",
        "msg_id": "ABCDEF123456",
        "timestamp": "2026-06-01T18:30:00Z"
      }
    }
  }
}

The script itself can stay boring:

  1. Check wacli doctor.
  2. Load the allowlist.
  3. Read new messages for selected chats, with a small overlap window.
  4. Ask rules or a local LLM for structured notes.
  5. Create a planned Markdown diff.
  6. Write nothing in dry_run.
  7. After a real apply, update state.

The overlap matters. Chats are messy. Sometimes the correction comes a few messages after the original plan. Reading exactly from the cursor is fast, but too brittle.

#Privacy boundaries

I would treat this like handling email:

  • no full raw transcript in Git
  • no automatic processing of unknown chats
  • no group chat processing unless the group is explicitly selected
  • no media download by default
  • no sending messages from the sync script
  • no cloud LLM call unless I explicitly choose the text being sent

Before adding a model, simple search already helps:

wacli messages search "appointment" --after 2026-01-01 --json
wacli messages search "birthday" --after 2026-01-01 --json
wacli messages search "dinner" --after 2026-01-01 --json

That is not fancy, but it catches a surprising amount.

#Conclusion

The interesting part is not the WhatsApp export itself. wacli already does the hard work by pairing with WhatsApp Web and keeping a local SQLite database.

The interesting part is the boundary around it.

Only selected chats. Dry-run first. Small LLM tasks instead of a giant transcript dump. Local models where possible. Stable message IDs for portable progress. Small append blocks instead of full transcripts. Existing open points get updated instead of duplicated forever.

That sounds less magical than “sync WhatsApp to my second brain”.

Good.

Magic is usually where the cleanup starts.

More articles