macOS quietly reaches over to a paired iPhone and fills in your SMS two-factor code for you. Windows just sits there. SyncOTP closes that gap: your iPhone forwards incoming texts to a ntfy topic, a tray app on the PC watches that topic, pulls the code out of whatever arrives, drops it on your clipboard, and tells you it's ready, so by the time you've switched windows to paste, it's already there.
Any ntfy server works: the public one at ntfy.sh, or your own.
Prebuilt binaries are on the Releases page. Each release has two downloads:
SyncOTP-<version>-win-x64-selfcontained.zip— bundles the .NET runtime. Larger download, nothing to install first, just unzip and runSyncOTP.exe.SyncOTP-<version>-win-x64-framework-dependent.zip— small download, but needs the .NET 10 Desktop Runtime installed already.
Either way, unzip somewhere permanent (e.g. %LOCALAPPDATA%\Programs\SyncOTP) and run the exe.
Updating is unzipping a newer release over the old one; config and logs live elsewhere and are
untouched (see Where things live).
If you'd rather build from source, or are developing on the app, use scripts/install.ps1 below.
-
Pick a topic. On the public server, a topic is created by publishing to it and the name is the only thing keeping other people out, so generate a long random one rather than choosing something memorable:
'syncotp-' + [Guid]::NewGuid().ToString('N')
Treat it as a password. Anyone who knows it can read every message you forward. If you would rather have real access control, run your own server or use a reserved topic on an ntfy paid plan, see docs/ntfy-server.md, which covers both credentials and self-hosting.
-
PC. Either grab a prebuilt binary from the Releases page (see Releases above), or build and install from source. Building compiles from source, so it needs the .NET 10 SDK, installing the runtime alone is not enough:
.\scripts\install.ps1 -StartupThe app creates
%APPDATA%\SyncOTP\config.jsonon first run and opens it. Fill inntfy.topic(andntfy.serverif you are not usinghttps://ntfy.sh, plusntfy.usernameandntfy.passwordif your topic requires auth), then choose Reload config from the tray menu. -
iPhone. Create the forwarding automation. See docs/iphone-shortcut.md. If you configured credentials, the tray menu's Copy Shortcut auth header gives you the Authorization value to paste in; without them the Shortcut is just a POST to the topic URL.
Confirm it works with Send test message in the tray menu, which publishes a synthetic code through the real server.
Clipboard. A code lands on the clipboard the moment it arrives, so you can paste straight into
the login form. Two minutes later it is wiped, unless you have already copied something else, in
which case it is left alone. Codes are flagged to stay out of Windows clipboard history (Win+V)
and cloud clipboard, so they do not linger where you would not expect them.
Notification. A toast shows the code, who it came from, and a Copy again button. If toast registration is unavailable the app falls back to a tray balloon. The last five codes stay in the tray menu under Recent codes, which is the escape hatch when the clipboard gets clobbered.
Stale codes. After a reconnect, ntfy replays whatever it cached. Anything older than three minutes goes into Recent codes but never touches the clipboard, since silently pasting an expired code is worse than doing nothing.
Duplicates. The same message is ignored if its id repeats, or if identical text arrives twice within 90 seconds. Beyond that window identical text is treated as a genuinely new code, which is what happens when you request one again.
The extractor masks the things that are never codes first (phone numbers, prices, times, durations,
dates, order numbers, URLs, carrier boilerplate), then scores what is left by pattern quality and by
how close a verification keyword sits. It recognises the common shapes: G-123456, 123-456, bare
4 to 8 digit runs, and alphanumeric codes when a keyword backs them up.
Confidence comes out as High, Medium, or Low. Low means no verification keyword anywhere, just a
lone number in a short message. Those are still copied, and the notification says "probable code".
Set extractor.acceptLowConfidence to false to skip them.
The whole extractor lives in CodeExtractor.cs with no
dependencies, and is covered by a corpus of real message shapes in
CodeExtractorTests.cs. If a message from some
service is missed, add it to the corpus and adjust from there.
%APPDATA%\SyncOTP\config.json:
{
"ntfy": {
"server": "https://ntfy.sh",
"topic": "",
"username": "",
"password": "",
"lastId": ""
},
"clipboard": {
"autoClearSeconds": 120,
"staleAfterSeconds": 180
},
"notifications": {
"toast": true,
"sound": false
},
"extractor": {
"acceptLowConfidence": true
},
"verboseLogging": false,
"messageSnippetChars": 120
}Leave username and password empty for an unauthenticated topic; if either is set, SyncOTP sends
HTTP Basic auth on every request. autoClearSeconds: 0 disables the wipe. lastId is the resume
cursor and is maintained by the app.
verboseLogging writes whole message bodies to the log, which means the codes themselves, so leave
it off unless you are debugging a message that was not detected.
messageSnippetChars is how much of the message text is shown in the log line and the toast. The
sender an SMS gateway reports is usually an anonymous five-digit short code, so the body is what
tells you which service the code is for. Set it to 0 to leave the text out.
| Executable | %LOCALAPPDATA%\Programs\SyncOTP\SyncOTP.exe |
| Config | %APPDATA%\SyncOTP\config.json |
| Logs | %LOCALAPPDATA%\SyncOTP\logs\ |
| Startup entry | HKCU\Software\Microsoft\Windows\CurrentVersion\Run\SyncOTP |
Re-running install.ps1 is the update path and leaves config and logs alone.
Every message you receive is forwarded, so the codes and the message bodies pass through the ntfy
server and sit in its cache for a while, 12 hours on ntfy.sh, whatever you configure on your own
server.
On a public topic there is no access control: the topic name is the whole secret, so use a random one and do not paste it anywhere. On an authenticated topic the Basic auth header is reversible to the password, so treat it as the password wherever it is stored.
Either way, the ntfy operator can see the messages. If that matters, self-host. To narrow the exposure regardless, use per-keyword automations on the phone instead of the catch-all filter, so only likely 2FA messages ever leave the device.
dotnet build
dotnet test
dotnet run --project src\SyncOTP.Appsrc/SyncOTP.Core, extraction, dedupe, config, logging. No UI, fully testable.src/SyncOTP.App, WinForms tray shell, ntfy client, clipboard and notifications.- Message transports sit behind
IMessageSource, so a second one (a direct listener on the LAN, say) is a new class plus a config block rather than a change to the pipeline.
- Windows 10 build 19041 or later
- .NET 10 SDK to install or update, since
install.ps1runsdotnet publishagainst this repo. It is also what you need for development. - .NET 10 Desktop Runtime to run the installed app (the build is framework-dependent). The SDK includes it, so a machine that built the app already has it.