Blip
iMessage. On Linux. For real.
Read, send, group chats, blue dots, desktop toasts — from an Omarchy bar widget.
Your Mac does the talking. Your Linux box does the living.
### ⚠️ You need a Mac. That is the whole trick.
Blip does not reimplement iMessage. Apple's protocol only runs on Apple
hardware, so Blip uses a Mac you already own — any Mac signed into your
Apple ID, awake and reachable over SSH — as the gateway. The Linux side is a
thin client. No Mac, no Blip. When the Mac sleeps, Blip dims and waits.
Every screenshot is the real client rendered against invented people —
scripts/demo/blip-shots points the shipping code at a fake bridge, so
nothing here is anyone's conversation. Numbers are in the 555-0100 range
reserved for fiction. The link card is a real fetch of a real public page.
Blip 2.3
Blip went public on 2026-08-31 and has moved fast since, much of it from people who showed up with pull requests. What is in it now:
- The app. A Messages-style window — sidebar of every conversation, the
SUPER+M, the ⇱ button in
the panel header, double-click the bar icon, or qs ipc … app. Remembers
size and whether it was open across shell restarts.
- Pinned conversations, mirrored from Messages on the Mac, in the same
- The font Messages uses. Omarchy renders everything in monospace, and that
pacman -S inter-font), then your theme font — so
nothing changes for anyone who has installed neither. blip-setup offers to
install Inter; ui_font=theme in bridge.conf opts out. ui_font_size=14
(9–24) is bubble text in pixels so Blip can be larger than the rest of the
shell.
Blip ships no fonts. SF Pro is Apple's and its licence forbids
redistribution, so it is never carried in this repo — if you want the exact
Messages face, get it from Apple.
See Fonts.
- Contact photos from the Mac's Contacts, and group photos — a group
- Link cards for every URL. Messages decorates some links and leaves most
- Real-time push — messages land in ~2 s via a watcher on the Mac.
- Attachments both ways — photos inline and upright (EXIF orientation is
/attach, with captions.
- SMS and RCS send on their own service instead of failing as iMessage,
- Search conversations by name, then messages; new conversation from a
- One source. The Mac-side tools ship in this repo;
blip-setupinstalls
- Audited. A full security + privacy audit, every finding fixed or
Full history: CHANGELOG.md.
Why this exists
iMessage is macOS-only. Everyone who lives on Linux and owns an iPhone knows the dance: pick up the phone, unlock it, type on glass, put it down, lose the thread. BlueBubbles wants SIP off. AirMessage wants a server and a prayer.
Blip does something simpler. The Mac you already own is the gateway. It
holds chat.db and it can drive Messages.app with AppleScript. Everything else
is one multiplexed SSH socket and a bar widget that looks like it belongs.
No SIP disabled. No daemon on the Mac. No private API. No message cache on the Linux side. If the Mac is asleep, the widget dims and says so.
What you get
|
In the bar
|
Conversation
Hotkey for the panel
~/.config/hypr/bindings.lua
is all it takes — o.bind("SUPER + CTRL + M", "Blip", "omarchy-shell shell toggle nixfred.blip").
(The bar finds a widget's panel through open(), close() and an opened
property; without the last one every panel hotkey silently skips the plugin.)
Omarchy's SUPER+CTRL+ (panel n in the bar's right section) reaches
it too. On a multi-monitor bar the panel lives on the first screen's copy.
The app
window is a plain toggle). For the keybind, add
this to ~/.config/hypr/bindings.lua — it asks Hyprland where the
window is (front → close, elsewhere → focus, none → create) instead of
the plugin, because after an Omarchy plugin update the plugin's IPC can
answer from a stale instance until the shell restarts. Match the Quickshell
class and exact app title (Blip or Blip (N)); a browser or editor titled
"Blip documentation" must never be focused or closed by this shortcut:
sidebar of every conversation + the open thread + compose, tiled by
Hyprland like any app, sharing the bar widget's live data; the title
carries the unread count (Blip (3))
Real-time
status shows watch=true)
|
Right-click any link to open its share sheet. Incoming links can also open it automatically; sending a link does not.
Privacy
No server, no telemetry, no accounts. **Everything between the two machines travels inside ssh** — message text, attachment bytes, the push ping — on a dedicated key the Mac confines to Blip's bridge tools — and, over Tailscale, to this machine's address; nothing is ever sent in the clear. The full inventory of what touches disk on both machines is in docs/PRIVACY.md — short version: message text never lands on disk; only attachments in conversations you open are cached (inline images ≤ 5 MB and link previews fetch when the thread does). The threat model and the findings of the 2026-08-31 security audit are in docs/SECURITY.md.
How it works
Linux Mac
───── ───
BarWidget.qml ── push/poll ──▶ collector.ts ──▶ ssh ──▶ imsg --json recent 150+
(sqlite, read-only)
BlipView.qml ── open thread ─▶ thread.ts ──▶ ssh ──▶ imsg --json thread <id> 80
BlipView.qml ── Enter ─────(body on stdin)──▶ ssh ──▶ imsg-send --to <id> --yes --text-stdin
imsg-send --chat-id "any;+;<guid>" …
(AppleScript → Messages.app)
The trick that makes it possible: **sshd on macOS inherits both Full Disk
Access and Automation consent.** cron gets neither. So a plain SSH session can
read chat.db and tell Messages.app to send, where every scheduled approach
dies at a TCC prompt nobody is there to click.
With ControlMaster in ~/.ssh/config, a round trip is ~47 ms warm. Fast
enough to poll, fast enough that the panel feels local.
Install
Blip is one source: the Mac-side tools ride along in bridge/mac/
(vendored from claude-on-mac,
pinned in bridge/BRIDGE-VERSION), and blip-setup wires everything.
Fonts
Blip renders in the first of these it finds, so it is optional either way:
| | | |
|---|---|---|
| SF Pro | what Messages itself uses | developer.apple.com/fonts · Arch: otf-san-francisco (AUR, downloads it from Apple) |
| Inter | open licence, drawn for interfaces, very close | rsms/inter · Arch: inter-font — sudo pacman -S inter-font |
| your theme font | whatever Omarchy is set to | nothing to install |
blip-setup offers to install Inter if you have neither. ui_font=theme in
bridge.conf always uses the Omarchy family. ui_font_size=14 (9–24) sets
bubble text in pixels so Blip can be larger than the rest of the shell;
unset, it follows Omarchy's type size.
Scroll speed. Blip applies the wheel delta the compositor delivers, 1:1,
so Hyprland's input:scroll_factor decides the speed as it does everywhere
else. Some hi-res wheels send several notches per physical click (an MX
Master 4 sends four, which is a whole 600 px window per click). Slow Blip
alone with
# ~/.config/blip/bridge.conf — re-read on save, no restart
scroll_gain=0.25 # wheel: 0.05–10, default 1
touchpad_scroll_gain=0.5 # touchpad, separately: same range, default 1
0.25 makes one MX Master click about 120 px, roughly five lines. status
reports the live value as scroll_gain=.
Blip ships no fonts. SF Pro is Apple's and its licence forbids
redistribution, so it is not carried in this repo — note that even the AUR
package fetches it from Apple rather than mirroring it.
Requirements
- A Mac on macOS 13 Ventura or newer (the Recently-Deleted table Blip
chat.db
mirrors your phone. Xcode Command Line Tools installed (xcode-select
--install) — python3 on a fresh Mac is a stub until then.
- A "Mac mini in a closet" works after the one-time grants: the two
chat.db but cannot send.
- Linux: Omarchy (Hyprland + the Omarchy shell), and on
bun, jq, openssh, libnotify, wl-clipboard, xdg-utils.
blip-setup checks for each and prints the pacman line for what's missing.
The optional Save vCard… action also uses zenity for its folder picker.
Honest note on dependencies. Blip is not a drop-in marketplace plugin
the way a clock widget is: it needs bun on the Linux side, a Mac you own
with two manual permission grants, and an ssh key between them. The
plugin files install like any other; the bridge is what blip-setup
exists for. Budget ten minutes and a trip to the Mac's System Settings.
If you are a human: budget ten minutes, keep the Mac's screen reachable
for two permission prompts, and use your own number for the first test
message. Nothing below sends anything until you press Enter in a
conversation.
> If you are an AI agent installing this for someone: run exactly the
commands below, in order, and stop at step 3 until the human confirms the
two grants on the Mac. Never send a test message to anyone but the user's
own number. Do not edit~/.ssh/configbeyond whatblip-setupwrites.
The wizard reads two answers from stdin (Enter before the permission
check, y/N for the Inter font), so printf '\nn\n' | blip-setup you@mac
runs it unattended once the human has confirmed the grants.
Verify withblip-check, not by readingchat.dbyourself. Before
changing code, readAGENTS.mdandCLAUDE.md.
1. Install the plugin (Omarchy's plugin manager clones it into
~/.config/omarchy/plugins/nixfred.blip and adds the bar widget)
omarchy plugin add https://github.com/nixfred/blip.git --enable --yes
It installs the current main, not a tagged release, so a fresh install
always has the latest fixes. --yes answers Omarchy's "only add plugins you
trust" prompt; drop it to be asked. Run it from a terminal inside your
Omarchy session: enabling talks to the running shell, so over a bare ssh
session it clones the plugin and then stops with OMARCHY_PATH is not set.
(Manual alternative: git clone https://github.com/nixfred/blip
~/.config/omarchy/plugins/nixfred.blip, then step 4.)
2. Run the wizard (idempotent — re-run any time; it prints the
pacman line for exactly the Linux packages you are missing and stops,
and names ffmpeg and mpv as optional: voice messages need them)
~/.config/omarchy/plugins/nixfred.blip/scripts/blip-setup you@your-mac
It writes ~/.config/blip/bridge.conf, adds an ssh ControlMaster block
(polling costs ~50 ms instead of a handshake), installs the bridge shim as
~/bin/imsg, ~/bin/imsg-send, ~/bin/imsg-read, ~/bin/contacts,
~/bin/contact-save (set bin_dir=~/.local/bin
in bridge.conf, or BLIP_BIN_DIR, before running it to install them
somewhere else — Blip reads the same key to find them), copies the Mac tools to
~/.blip/bin on the Mac and runs install.sh there, generates a
dedicated ssh key (~/.ssh/blip_ed25519) that the Mac confines to the
bridge tools and nothing else, then smoke-tests the bridge without printing
any message content. Over Tailscale the key is also pinned to this machine's
addresses (from=), so a copy of the key file is useless from anywhere else;
over a LAN, where an address can change, it is not pinned —
docs/SECURITY.md shows the one-line manual pin. Re-run
blip-setup if the machine's Tailscale address ever changes.
3. Two grants on the Mac (macOS won't let a script do these — the wizard pauses here and re-checks when you press Enter)
- Full Disk Access → System Settings → Privacy & Security → Full Disk
/usr/libexec/sshd-keygen-wrapper (⌘⇧G in the file picker).
That is what lets an ssh session read chat.db — any ssh session: the
grant is per sshd-keygen-wrapper, not per key, so from here on every key
that can open a shell on this account can read your messages. Blip's own
key runs only the bridge tools (step 2), and reading messages is their job;
keep the other keys few, and see docs/SECURITY.md for
pinning them or closing port 22 one layer down.
- Automation → Messages → the first send from ssh pops an Allow prompt on
auth_reason 9,
"Prompt Timeout"), and on macOS 26 the switch under System Settings →
Privacy & Security → Automation → sshd-keygen-wrapper → Messages may then
refuse to turn on: you enter the password and it drops back off (#36).
Recovery, SIP intact, no database edits: on the Mac run
tccutil reset AppleEvents — Apple's own tool; it clears every app's
Automation grants (each simply asks again next time), because a
path-identified client like sshd-keygen-wrapper cannot be reset on its own —
then re-run blip-setup and sit at the Mac's screen for the prompt.
ssh your-mac 'python3 "$HOME/.blip/bin/blip-check"' shows ✅/❌ per grant at any
time, with the fix for each ❌. It does not test the optional read-push grant
unless you add --markread, because that probe pops an Automation prompt of its
own and you should only be asked for a permission you actually want.
4. Bar widget. omarchy plugin add --enable already placed it. If you
cloned by hand: omarchy plugin enable nixfred.blip --section right, or add
{ "id": "nixfred.blip" } to bar.layout.right in
~/.config/omarchy/shell.json. Then omarchy-restart-shell — the speech
bubble is in your bar.
Clock and dates. Out of the box the time follows your locale — 9:08 PM
or 21:08 — and dates read the way Messages writes them: Aug 28, or
Aug 28, 2025 for another year. To change either, put Qt format strings on
the same entry, exactly as Omarchy's clock takes its format:
{ "id": "nixfred.blip", "timeFormat": "HH:mm", "dateFormat": "dd.MM", "dateFormatWithYear": "dd.MM.yyyy" }
Smooth scrolling (opt-in). By default a mouse-wheel notch lands at once. With
# ~/.config/blip/bridge.conf — re-read on save, no restart
smooth_scroll=on
a notch glides to its place (180 ms, easing out) instead; notches that
arrive mid-glide add up, so a fast spin never loses distance. Touchpad
scrolling is always direct. status shows smooth_scroll=on while it is on.
5. (Optional) SUPER+M for the app window — the Lua snippet under
"The app" above.
Updating: omarchy plugin update nixfred.blip, then omarchy-restart-shell
(a hot-reload alone leaves the plugin's IPC on a stale instance — see
CLAUDE.md). Re-run blip-setup after updates that touch bridge/; it is
safe to re-run.
Toasts — desktop notifications fire only for handles you list; everything
else still counts and still shows. Put this in
~/.config/blip/allowlist.json; it is re-read every poll, so no restart.
{ "allow": ["+15551234567", "[email protected]"] }
Strict JSON — no comments, no trailing commas. A file that does not parse
is treated as an empty list, silently, so a stray // line reads exactly like
having no allowlist at all: everything still counts on the badge, and nothing
ever toasts. If toasts are not firing, check the file parses
(jq . ~/.config/blip/allowlist.json) before anything else. List each handle a
person actually messages from — someone with an iCloud address and a phone
needs both lines, or they go quiet whenever they switch.
Mute (spam) — the allowlist's opposite: a muted conversation does not
show at all. No sidebar row, no unread count, no toast. This is the knob for
political fundraising blasts — the "the deadline is TONIGHT, rush $25,
Reply STOP2END" texts that arrive from a short code you have never seen and
that will be a different short code next week. Listing the number is
whack-a-mole, so list the words instead: the PAC platform's name (ActBlue,
WinRed) and the opt-out footer the law makes every one of them carry
(Stop2End) survive the rotation.
Put this in ~/.config/blip/mutelist.json — re-read every poll, and strict
JSON exactly like the allowlist above:
{ "mute": ["ActBlue", "WinRed", "Stop2End", "78462"] }
An entry matches a handle or chat id exactly (like the allowlist), or a phrase of two or more characters anywhere in an inbound message, case-insensitively. One match mutes that whole conversation — a blast puts its footer on some messages and not others. Only inbound text is tested, so forwarding "another ActBlue text, unbelievable" to a friend never mutes the friend. Nothing is deleted: the messages are untouched on the Mac and in Messages, Blip simply stops showing them.
Pick phrases a person would not send you. The test is on inbound text,
so if a friend writes "I got another ActBlue text today", that mutes your
friend's whole conversation until you edit the list. Platform names and
opt-out footers are safe because nobody types them at you; a common word is
not.
Spam and unknown senders — iPhone Messages keeps a Spam folder (and,
when Filter Unknown Senders is on, a separate unknown-senders list). Those
chats are still unread rows in chat.db (is_filtered = 2 and 1), so
Blip's badge used to disagree with the phone. Hide them the way the phone
does:
# ~/.config/blip/bridge.conf — the shim re-reads this every call
hide_spam=on
hide_unknown=on
Either key, or both. Default is off. This is the folder, not a phrase: a
fundraising blast that Messages left in the inbox still needs the mute list.
Needs a blip-setup re-run (or a copy of bridge/mac/imsg to the Mac)
so --hide-spam / --hide-unknown exist on the far side.
Outside North America: set country_code=44 (etc.) in
~/.config/blip/bridge.conf so a number typed without a country code in
"New message" resolves correctly. Contacts saved without a country code
(123 45 678, 07700 900123) get their name and photo the way Contacts
resolves them: the Mac's own region fills in the code. Green-bubble (SMS/RCS)
conversations send on their own service automatically.
Prefer iMessage on mixed 1:1s. A DM that used to be blue and then got an RCS inbound (someone in a mixed-platform group, Continuity falling back) otherwise sends RCS/SMS next. Opt in:
# ~/.config/blip/bridge.conf — re-read every poll, no restart
prefer_imessage=on
A thread with a successful iMessage in the loaded window then stays iMessage. A never-iMessage RCS/SMS thread stays green. A failed iMessage to a phone still flips to SMS so the send does not stick. Groups are unchanged (they send by chat id). Default is off.
Two or more monitors: one bar widget per screen is normal; only the one on the first screen polls and owns the app window, the others show the badge and forward clicks to it.
Keeping the Mac awake
A sleeping Mac is not a broken Blip. It is an unreachable gateway, and from
Linux the two look identical: the widget dims, ~/bin/imsg exits 69, and
the panel says it is offline. Nothing is lost — every message is still on the
Mac and on your phone, and Blip catches up the moment it can reach the gateway
again. But this is the single most common "Blip stopped working", so it is
worth ten seconds of pmset before you go looking for a bug.
See what your Mac does today. On the Mac:
pmset -g | grep -E 'sleep|womp'
sleep 0 means never; any other number is minutes of idle before it goes.
Usefully, that same line names whatever is currently holding it awake:
sleep 1 (sleep prevented by sharingd, Amphetamine, nfsd, powerd)
The durable fix — never sleep while plugged in:
sudo pmset -c sleep 0 # -c = on the power adapter only
Use -c, not -a. A laptop told never to sleep on battery will quietly
flatten itself; a bridge Mac should live on AC.
A closed lid sleeps anyway. sleep 0 does not survive lid-close — a
MacBook suspends regardless unless an external display is attached. To keep a
clamshell Mac reachable, override it outright:
sudo pmset -c disablesleep 1 # stays awake on AC, lid open or shut
sudo pmset -c disablesleep 0 # undo
If you would rather not use sudo, Amphetamine
(free, App Store) does the same job from a menu-bar toggle, and it is what the
author's own bridge Mac runs — it is the Amphetamine in the pmset line
above. caffeinate -s works too, but only for as long as that terminal stays
open, which makes it a good thing to type before a long sync and a bad thing
to rely on.
Do not count on wake-on-network. womp 1 (System Settings ▸ Energy /
Battery ▸ Wake for network access) wakes the Mac for a wake-on-LAN magic
packet on Ethernet. It does not reliably wake a Mac for an inbound SSH
connection, and it will not help at all over Tailscale or from another
network — which is exactly the case Blip is usually in. Treat it as a bonus
on a wired desk Mac, never as the reason you left sleep enabled.
Also worth knowing: the Mac must be in a logged-in desktop session, not
parked at the login window — see the requirements above. Sleep and the login
window fail differently: asleep, Blip goes offline entirely; at the login
window it can still read chat.db but cannot send.
Verify from Linux. With the Mac configured, this should answer instantly rather than hang:
~/bin/imsg chats 1 >/dev/null && echo "gateway reachable"
Removing Blip
Blip installs into five places on Linux and one on the Mac. None of this
touches Messages: Blip never writes chat.db, so your history is unaffected
wherever it is removed from.
1. The plugin and its bar widget.
omarchy plugin remove nixfred.blip --yes
omarchy-restart-shell
(omarchy plugin disable nixfred.blip instead, to take it off the bar but keep
the checkout.)
2. The shims. blip-setup installs blip-shim as five tools in ~/bin,
backing up anything it displaced as :
rm -f ~/bin/imsg ~/bin/imsg-send ~/bin/imsg-read ~/bin/contacts ~/bin/contact-save
ls ~/bin/.pre-blip. 2>/dev/null # restore any of these you want back
3. Config, state and caches.
rm -rf ~/.config/blip # bridge.conf, allowlist.json, mutelist.json
rm -rf ~/.local/state/blip # state.json, window.json, push-read.log, audit-cache.json
rm -rf ~/.cache/blip # fetched attachments, avatars, link previews
rm -rf "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/blip" # draft images; cleared on reboot anyway
Message text is not on disk in any of these — see Privacy — but the attachment cache holds real media, so it is the one worth removing deliberately.
4. The dedicated ssh key.
rm -f ~/.ssh/blip_ed25519 ~/.ssh/blip_ed25519.pub
blip-setup may also have appended a Host block for the Mac to
~/.ssh/config, but only if that host had none. It is a plain ControlMaster
block, harmless to keep; remove it by hand if you want it gone.
5. On the Mac.
rm -rf ~/.blip # bin/ and src/
grep -v 'blip-dispatch' ~/.ssh/authorized_keys > ~/.ssh/authorized_keys.new \
&& mv ~/.ssh/authorized_keys.new ~/.ssh/authorized_keys
Your everyday ssh key and its access are untouched — the line removed above is only the confined Blip key.
6. The Mac's privacy grants, optionally. Removing Blip does not revoke
them, and they belong to /usr/libexec/sshd-keygen-wrapper rather than to
Blip — anything else you reach over ssh may depend on them, which is why they
are not part of the steps above. To revoke anyway: System Settings ▸ Privacy &
Security ▸ Full Disk Access, Automation, Contacts and, if you
enabled mark-read on the Mac, Accessibility, removing the
sshd-keygen-wrapper entry from each. See docs/SECURITY.md.
Contact review
Right-click a conversation or choose Review contact from its header to inspect the matching cards in Mac Contacts. In a group, choose a participant. Each card shows its name, account, source, matching-field count, and whether it has a photo. Open in Contacts on Mac opens that exact card; make edits in Contacts itself.
In a card's details, right-click any field to copy its whole value. To copy part of a value, select the text and press Ctrl+C or Ctrl+Insert, or use Omapop. A brief toast confirms a successful right-click copy or reports a clipboard error.
Scan contacts checks the conversation list for possible duplicate cards and handles shared by different names. Named conversations and short-code senders are included. A shared number or name is evidence to review, never an automatic merge. Scans support up to 200 distinct handles.
The scan cache is private and reused only when both the handle set and the Mac
Contacts fingerprint still match. The feature adds no settings page or display
name overrides. Configuration stays in bridge.conf. Review requires no Swift
helper; the optional availability check needs Automation → Contacts on the Mac.
Save a new contact
When Review contact finds no card for a sender, choose Save new contact, enter a name, review the fields, and confirm Save to Contacts. Blip checks for duplicates and reads the new card back before reporting success. Existing cards are not edited or merged. See contact saving setup.
Keyboard
| where | key | does |
|---|---|---|
| list | j / k · ↑ / ↓ | move |
| list | Enter · 1–9 | open thread (the first nine rows show the digit; Super+M jumps from an empty compose) |
| list (window) | rest on a row | the right pane shows that thread, like Messages' sidebar — without marking it read; Enter, a click or typing commits it |
| window | → · ← | into the compose field · back to the sidebar (from the start of the text, or an empty field) |
| list | PgUp / PgDn · Home / End | select the row at the edge of the view, then a screen further each press · first / last row — in the thread list, the search hits and the new-message picker alike |
| list | r | refresh |
| list | a · mark all read link | clear every badge and dot — and tell the Mac, so your iPhone catches up too |
| list | / | search conversations by name as you type, then messages; Enter opens the highlight, Esc backs out |
| list | n · + new link | start a conversation with anyone — search contacts by name, or type a number/email directly |
| panel or window | Ctrl+1 … Ctrl+9 | open the corresponding pinned conversation, left to right then top to bottom; works while typing; unused numbers do nothing |
| thread | Enter | send (text, or the queued file with the text as caption) |
| thread | Ctrl+V | paste — an image on the clipboard becomes a queued file, text pastes normally |
| thread | /attach + Enter | queue any file on this machine; drag-and-drop works too |
| thread | ↑ / ↓ | move through draft lines; on the first / last visual line, jump to the beginning / end of the draft |
| thread | Enter · Ctrl+C · Ctrl+R (bubble selected) | open its attachment or link · copy its text, or the picture itself when the bubble is only a picture · quote it into the compose field (> …) |
| thread | PgUp / PgDn (Fn+↑/↓ on a Mac keyboard) | select the topmost / bottommost visible bubble, then a screen further each press — also with text in the compose field, since they move no caret |
| thread | Shift+PgUp / Shift+PgDn | one bubble at a time from anywhere in a draft, without moving the caret first |
| thread | Home / End · Ctrl+Home / Ctrl+End | start / end of the current line · start / end of the whole draft |
| thread | Esc | back to list (or clear a text selection first) |
| anywhere | Esc | close |
Drag the diagonal lines in the menubar panel’s bottom-right corner to resize it.
Its width and height are remembered across shell restarts in
$HOME/.local/state/blip/panel.json. The panel stays within the current display
and never exceeds 80% of that display’s logical height, including its border
and padding. Moving to a smaller display clamps the visible size without
replacing your saved preference. The separate app window keeps its own size.
The message composer exposes a named, editable multiline accessibility field for
apps such as hyprcorrect. Quickshell must include upstream accessibility fix
916a0dd90c; unpatched 0.3.1 hides its windows from accessibility clients.
Misspellings receive red underlines using local Hunspell with an English (US)
dictionary. Install hunspell and hunspell-en_us to enable this on Arch, or
provide en_US.aff and en_US.dic under
$HOME/.local/share/blip/dictionaries/. No packages are installed automatically.
spell=en_US,nb_NO in bridge.conf checks against several dictionaries at
once — a word found in any of them is fine, which is what a bilingual draft
needs — and spell=off turns the underlines off. Names follow the dictionary
files (hunspell -D lists them); a name that is not installed is skipped.
Spelling checks debounce for 350 ms, inspect at most 256 words in drafts up to
8 KiB, and skip URLs and email addresses. Missing dictionaries or checker errors
leave the draft usable without underlines. Draft text travels only over stdin;
only character ranges return to the UI, and nothing is saved or sent remotely.
IPC, for scripts and other plugins:
omarchy-shell shell toggle nixfred.blip # open/close the panel (Omarchy's standard path)
qs -p /usr/share/omarchy/shell ipc call nixfred.blip status
qs -p /usr/share/omarchy/shell ipc call nixfred.blip goto 15551234567 # bare digits
qs -p /usr/share/omarchy/shell ipc call nixfred.blip read # mark all read
qs -p /usr/share/omarchy/shell ipc call nixfred.blip share https://example.com # share sheet for a URL
qs -p /usr/share/omarchy/shell ipc call nixfred.blip typecode # type the pending 2FA code into the focused field
qs -p /usr/share/omarchy/shell ipc call nixfred.blip copycode # or copy it
Security-code autofill
Set otp_autofill=on in ~/.config/blip/bridge.conf to offer new codes beside
the focused field. Click Fill code to insert, or × to dismiss. The
prompt shares Blip's fonts and colors and handles separate digit boxes. Sites
need not declare autocomplete="one-time-code"; accessible labels can identify
the field. Codes expire after five minutes, even if you change focus.
This uses Linux accessibility, with no browser extension. When field bounds
are unavailable the prompt appears at the top right; when field metadata is
unavailable, select the intended input before clicking. Known chat, password,
phone and search fields are excluded. It does not copy the code or press Enter.
The field prompt does not require automation=on and replaces the legacy
code toast and typecode/copycode handling while enabled.
The text must reach the Mac first. If it appears only on the iPhone, check Settings → Apps → Messages → Text Message Forwarding and enable the Mac used by Blip. Both devices must use the same Apple Account; Messages in iCloud can provide forwarding automatically. See Apple's forwarding guide.
Linux dependencies, browser activation and troubleshooting: Autofill setup.
Legacy security-code toast. With autofill off, when a text looks like a one-time code
("Your verification code is 483920", "G-482913", the origin-bound
@example.com #493857 form), Blip toasts it. Click the toast to copy it, or
bind typecode to a key and it is typed into whatever has focus, the way
macOS offers a code from Messages to Safari. The digits go to the focused
window as key events through Hyprland, not a virtual keyboard, so holding
the hotkey's modifiers cannot turn them into workspace binds. The code lives
in the widget's memory for five minutes and nowhere else. Needs
automation=on. A binding
for ~/.config/hypr/bindings.lua (any free chord works; stock Omarchy leaves
SUPER + SHIFT + V free, your own bindings may not):
o.bind("SUPER + SHIFT + V", "Type security code",
"qs -p /usr/share/omarchy/shell ipc call nixfred.blip typecode")
Everything that sends or reads message content over IPC (goto, compose,
bubbles, threads, find, newchat, read, typecode, copycode) is off by default — any
local process could otherwise send as you. Turn it on with automation=on
in ~/.config/blip/bridge.conf (re-read live). status, open, close,
toggle, window, app are always available.
Design notes worth knowing
Two read marks, not one. The collector keeps watermark (highest timestamp
it has seen — drives toasts) separate from readMark and per-thread
readMarks (highest timestamp you have looked at — drives the badge and the
dots). Fold them together and the badge flashes to 1 and resets on the next
poll. Yes, that shipped once.
Unread is a ledger, not a window. The latest 150 rows are enough for normal previews, but unread counts and oldest-unread timestamps live in a metadata-only per-chat ledger. The Mac's complete read-state snapshot refreshes this ledger without fetching message bodies. Older bridges fall back to expanding the fetch to cover new arrivals and outstanding unread. An unread cannot fall off the preview window or remain counted after deletion.
Reads reach the Mac through its menu bar. Set push_read=thread in
~/.config/blip/bridge.conf to synchronize each direct conversation you read.
The default all synchronizes only the explicit mark-all gesture; off keeps
read actions local. Per-thread actions briefly select the conversation in
Messages and restore the previous app's focus. With push_read=thread, a
group read reaches the Mac through Messages' groupid link, not only mark-all.
Read actions are saved before contacting the Mac, verified against
Messages' database, and retried after temporary failures or reconnects.
Permission errors stay visible in Blip until resolved. A newer inbound beyond
the visible --seen timestamp cancels an old read retry so it stays unread.
A complete metadata-only imsg read-state snapshot reconciles blue dots even
for conversations outside the recent-message window. Confirmed local overrides
are retired, allowing later Mac/iPhone changes to take effect. Both updated
Mac tools require the sibling read_state.py module.
push_read in bridge.conf takes three values, and the default surprises
people: all (the default) pushes only on the mark-all gesture, so
reading one conversation in Blip clears its dot here and leaves your iPhone's
badge alone. thread also pushes each unread conversation you read, including groups.
It briefly brings Messages forward on the Mac, then restores the previous app.
Groups are addressed by their identifier, never their name or last speaker.
Messages in iCloud must be enabled on your devices for Apple to propagate
that read state; your Messages read-receipt settings still apply. off keeps the Mac out of it entirely. qs ipc call
nixfred.blip status reports the live value as read_push=. Every push records
its outcome in ~/.local/state/blip/push-read.log (no message content), so
"did that reach the Mac?" has an answer.
Groups send by GUID. Message rows carry a group as a bare
chat_identifier (32 hex, or chat); AppleScript's chat id wants
the full any;+;. A group's handle field is whichever member spoke last
— send to that and you DM one person while the panel shows the group.
imsg groups supplies the real GUID; a group whose GUID isn't cached yet is
read-only rather than guessed.
The self-thread lies. A message you send yourself lands twice: once
from_me=true, once from_me=false, same timestamp and text. Every counter and
every bubble runs through dedupeSelfEcho() first or your own notes light the
badge forever.
Deleted means deleted. macOS keeps deleted messages in a 30-day "Recently
Deleted" bin that is still in chat.db. claude-on-mac's imsg hides those rows, so
a conversation you delete on the phone disappears from Blip within one poll of
the iCloud sync. IMSG_INCLUDE_DELETED=1 shows them again.
Notices are not messages. Messages.app writes its grey centered notices —
someone joined, a group was renamed, location sharing started — into the same
table as messages, and never marks them read. imsg hides them, or every one
would be an empty bubble that counts as unread until the end of time.
No message bodies are stored on Linux. ~/.local/state/blip/state.json
holds timestamps, unread counts, SHA-256 toast-dedupe keys, inferred self-chat
ids, and group metadata. It is written atomically with mode 0600; legacy
plaintext toast keys are hashed on migration. No message text is persisted.
The 273,000-message history stays on the Mac where it lives.
What it can't do
- Send tapbacks, edits, or threaded replies. Blip displays all three and
imsg-read already uses with SIP on. What is
unsolved is selecting an arbitrary bubble from Linux, and a group cannot be
addressed at all. Typing indicators have no menu item and stay out. See
issue #69 for the live work; nothing from it is in the tree.
(Showing their receipts on your messages works fine — that's in.)
- Work without a Mac, or while the Mac sleeps. Inherent to the approach.
Development
bun test # 312 tests, ~110 ms
bun collector.ts --deep | jq '.unread, (.threads|length)'
bun thread.ts +15551234567 40 | jq '.bubbles[-1]'
scripts/demo/blip-shots # regenerate docs/img/*.png
Logic lives in TypeScript where it can be tested; QML only renders. Every
layout bug so far was found with grim and eyeballs, not by reading code —
screenshot your changes. See CLAUDE.md for the invariants.
Screenshots without anyone's messages. A messaging client's real screen is
a private conversation, and blurring is not privacy. scripts/demo/blip-shots
builds a sandbox HOME whose bin/imsg is a fake bridge
(scripts/demo/fake-imsg, fixtures in scripts/demo/fixtures.json), then runs
the shipping collector, thread loader, avatar fetcher and QML against it in a
throwaway Quickshell instance and writes docs/img/*.png. Same code paths,
invented people. Change the fixtures and re-run to document a new feature.
Contributing
Pull requests welcome — see CONTRIBUTING.md (short; the one rule that matters: never test sends against a real contact). Security reports go through GitHub's private reporting, not issues.
Credits
Built by Fred Nix and Larry (his Claude Code collaborator) — 1.0 in one evening, 2.0 the next — on Omarchy. The Mac side is entirely claude-on-mac — it predates Blip, and it's the reason this took an evening, not a week.
Contributors: @jethrojones — blip-check across every Contacts source (#2).
MIT.
Export a contact
In Contact review, choose the exact matching source card. Copy vCard copies
a real .vcf file: press Ctrl+V directly in an app that supports pasting files,
such as a file manager or an email attachment editor. Clipboard-history menus
may retain only text and images, so selecting that entry again may lose its
file type. Support for file pasting depends on the receiving app.
Copy vCard and Save vCard… name the file with the contact’s nickname,
or first name when no nickname is set (for example, Ex.vcf).
Save vCard… lets you choose Downloads or another folder. It saves a named
.vcf you can attach, drag, or keep; existing files are preserved by adding a
number to the new filename. Neither action changes the contact on the Mac.