Vivek Kairi

August 3, 2024

Copying otp from iMessage using Hammerspoon

Mac ecosystem works great and makes life easier. But I am not a big fan of Safari, so I use Chrome as my default browser. One thing that always bothers me is how I receive OTP on my iPhone and Safari can pick it up easily and do autofill, but Apple’s decision to make it only work on Safari and not system-wide is irritating. I can see the OTP in the notification but have to manually read and write it.

Just like all developers, I care about automating repetitive tasks, and thus my search for a solution began.

Recently, I have started using Hammerspoon on Mac, and it’s quite a powerful tool for automation, so that’s where I started looking first.

Automation Idea

  1. Somehow get the iMessage text
  2. Use regex to extract the OTP
  3. Get it in my clipboard
  4. Voila! We can paste it directly

Hammerspoon

macOS stores all the iMessage content in a SQLite database, and Hammerspoon has a SQLite module which can help query databases. The database path is ~/Library/Messages/chat.db, and the table name is message. I quickly opened my Hammerspoon config and started experimenting, not being a big fan of the SQLite documentation on Hammerspoon. After multiple trials and errors, I got the SELECT query working.

databasePath = os.getenv("HOME") .. "/Library/Messages/chat.db"
local db = hs.sqlite3.open(databasePath, hs.sqlite3.OPEN_READ)
for row in db:rows("SELECT * FROM message WHERE service = 'SMS' ORDER BY ROWID DESC LIMIT 1") do
end

It’s a fairly basic query to get the last message of service type SMS, as the message table also has iMessages and other services.

Now that we have the row, we just need to extract the OTP and copy it to the clipboard, which is fairly easy with Hammerspoon.

Here’s the full script; it does the job perfectly. I initially decided to have it on the clipboard, but it turns out I can just emit it through event APIs, which is much better in my opinion.

So, Cmd + Shift + O, and I have the OTP in the field. It also works on websites with paste disabled :)

databasePath = os.getenv("HOME") .. "/Library/Messages/chat.db"
hs.hotkey.bind({"cmd", "shift"}, "o", function()
local db = hs.sqlite3.open(databasePath, hs.sqlite3.OPEN_READ)
for row in db:rows("SELECT * FROM message WHERE service = 'SMS' ORDER BY ROWID DESC LIMIT 1") do
local otp = row[3]:match("(%d+)")
hs.eventtap.keyStrokes(otp)
end
end)