Skip to content

Instantly share code, notes, and snippets.

@ewilderj
Created February 18, 2026 22:59
Show Gist options
  • Select an option

  • Save ewilderj/3428969853e3d19c52e20029df96da87 to your computer and use it in GitHub Desktop.

Select an option

Save ewilderj/3428969853e3d19c52e20029df96da87 to your computer and use it in GitHub Desktop.
Teams PWA URL Handler for macOS — makes msteams:// meeting links open in the Teams PWA
on open location theURL
set logFile to (POSIX path of (path to home folder)) & "Library/Logs/TeamsURLHandler.log"
-- Deduplicate: check if we handled this same meeting in the last 30 seconds
try
set lastHandled to do shell script "cat /tmp/teams_url_handler_last 2>/dev/null || echo ''"
set lastTime to do shell script "stat -f %m /tmp/teams_url_handler_last 2>/dev/null || echo 0"
set nowTime to do shell script "date +%s"
set timeDiff to (nowTime as integer) - (lastTime as integer)
-- Extract just the meeting ID to compare (ignore deeplinkId which changes)
set meetingID to theURL
if meetingID contains "?" then
set AppleScript's text item delimiters to "?"
set meetingID to text item 1 of meetingID
set AppleScript's text item delimiters to ""
end if
if lastHandled is equal to meetingID and timeDiff < 30 then
do shell script "echo 'DEDUP: skipping duplicate within " & timeDiff & "s' >> " & quoted form of logFile
return
end if
-- Record this meeting
do shell script "echo " & quoted form of meetingID & " > /tmp/teams_url_handler_last"
end try
-- Extract path from msteams URL
set pathAndQuery to theURL
if pathAndQuery starts with "msteams://" then
set pathAndQuery to text 11 thru -1 of pathAndQuery
else if pathAndQuery starts with "msteams:/" then
set pathAndQuery to text 10 thru -1 of pathAndQuery
else if pathAndQuery starts with "msteams:" then
set pathAndQuery to text 9 thru -1 of pathAndQuery
end if
if pathAndQuery does not contain "webjoin=true" then
if pathAndQuery contains "?" then
set pathAndQuery to pathAndQuery & "&webjoin=true"
else
set pathAndQuery to pathAndQuery & "?webjoin=true"
end if
end if
set meetPath to "/" & pathAndQuery
do shell script "echo 'Handling: " & meetPath & "' >> " & quoted form of logFile
tell application "Microsoft Edge"
set found to false
repeat with w in windows
if (count of tabs of w) is 1 then
set tabURL to URL of tab 1 of w
if tabURL contains "teams.cloud.microsoft" or tabURL contains "teams.microsoft.com/v2" then
execute tab 1 of w javascript "window.location.assign('" & meetPath & "');"
set index of w to 1
activate
set found to true
exit repeat
end if
end if
end repeat
if not found then
do shell script "echo 'Launching PWA...' >> " & quoted form of logFile
set homeDir to POSIX path of (path to home folder)
do shell script "open " & quoted form of (homeDir & "Applications/Edge Apps.localized/Microsoft Teams (PWA).app")
delay 5
repeat with w in windows
if (count of tabs of w) is 1 then
set tabURL to URL of tab 1 of w
if tabURL contains "teams" then
execute tab 1 of w javascript "window.location.assign('" & meetPath & "');"
set index of w to 1
activate
set found to true
exit repeat
end if
end if
end repeat
end if
do shell script "echo 'Done: found=" & found & "' >> " & quoted form of logFile
end tell
end open location

Teams PWA URL Handler for macOS

Problem

If you use the Microsoft Teams PWA (installed via Edge) instead of the native Teams desktop app, clicking Teams meeting join links doesn't work. Here's why:

  1. The interstitial uses the wrong protocol — meeting links go through a launcher page that fires msteams:// URLs, but the PWA only registers web+msteams://.
  2. PWAs can't receive protocol URLs — even if you register the scheme, Chromium's app_mode_loader doesn't pass protocol URLs to the web content. The PWA just opens to its home page.
  3. Direct /meet/ URLs redirect to the launcher — navigating to teams.cloud.microsoft/meet/ID triggers a server-side redirect back to the launcher interstitial, creating a loop.

Solution

A small AppleScript app (TeamsURLHandler.app) that bridges the gap:

  1. Registers as the macOS handler for msteams:// URLs
  2. Translates the protocol URL to a web path — strips the msteams:/ prefix and adds &webjoin=true (which tells Teams to handle the join in-browser instead of redirecting to the launcher)
  3. Injects JavaScript into the running Teams PWA — uses window.location.assign() via AppleScript to navigate the PWA's SPA router directly
  4. Deduplicates — the interstitial fires msteams:// multiple times per click; the handler skips duplicates within 30 seconds

Flow

Click meeting link in browser
  → Edge loads interstitial at teams.microsoft.com/dl/launcher/launcher.html
    → Interstitial fires msteams://meet/ID?p=PASSCODE&...
      → macOS routes to TeamsURLHandler.app
        → Handler finds Teams PWA window (single-tab Edge window at teams.cloud.microsoft)
        → Executes: window.location.assign('/meet/ID?p=PASSCODE&webjoin=true')
          → Teams SPA router loads the meeting pre-join screen

Prerequisites

  • macOS (tested on Tahoe/26.x)
  • Microsoft Edge with the Teams PWA installed from teams.cloud.microsoft
  • Edge setting: View → Developer → Allow JavaScript from Apple Events must be enabled

Installation

The source is in source.applescript.

1. Compile the AppleScript app

osacompile -o ~/Applications/TeamsURLHandler.app source.applescript

2. Configure the app bundle

Add the msteams:// URL scheme, set the bundle identifier, and make it background-only (no Dock icon):

APP=~/Applications/TeamsURLHandler.app/Contents/Info.plist

/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes array" "$APP"
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0 dict" "$APP"
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLName string com.ewj.teamsurlhandler.msteams" "$APP"
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLSchemes array" "$APP"
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLSchemes:0 string msteams" "$APP"
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier com.ewj.teamsurlhandler" "$APP"
/usr/libexec/PlistBuddy -c "Add :LSBackgroundOnly bool true" "$APP"

3. Sign and register

codesign --force --sign - ~/Applications/TeamsURLHandler.app

LSREG=/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister
$LSREG -f ~/Applications/TeamsURLHandler.app

swift -e '
import CoreServices; import Foundation
LSSetDefaultHandlerForURLScheme(
    NSString(string:"msteams") as CFString,
    NSString(string:"com.ewj.teamsurlhandler") as CFString
)'

4. Configure Edge policy (optional)

Suppress the "Open this app?" prompt when Edge fires msteams://:

defaults write com.microsoft.Edge AutoLaunchProtocolsFromOrigins \
  -string '[{"protocol":"msteams","allowed_origins":["teams.microsoft.com","*.teams.microsoft.com","teams.cloud.microsoft","*.teams.cloud.microsoft"]}]'

5. Verify

# Check the handler is registered
swift -e 'import AppKit; print(NSWorkspace.shared.urlForApplication(toOpen: URL(string:"msteams://test")!)!)'
# Should print: file:///Users/YOU/Applications/TeamsURLHandler.app/

# Test it
open "msteams://meet/123?p=test&anon=true"
# Should open the Teams PWA to the meeting pre-join screen

Customization

  • If your Teams PWA is at a different path (e.g., you renamed it), edit the do shell script "open ..." line in source.applescript and recompile.
  • If you use Chrome instead of Edge, change tell application "Microsoft Edge" to tell application "Google Chrome" (the AppleScript API is identical).

Troubleshooting

  • View the debug log: tail -f ~/Library/Logs/TeamsURLHandler.log
  • PWA shows a spinner: The interstitial may have fired multiple times. The dedup logic should prevent this, but if it persists, wait ~10 seconds for the SPA to finish loading.
  • Handler stops working after Edge update: Re-run steps 3 (sign and register) above.
  • "Allow JavaScript from Apple Events" prompt: This is required for the handler to inject navigation into the PWA. Enable it once in Edge's View → Developer menu.

Key discoveries

  • webjoin=true is the magic query parameter that tells Teams to handle meeting joins in the web client rather than redirecting to the launcher.
  • msteams://web+msteams:// — the interstitial uses the former, the PWA only registers the latter. They're different URL schemes.
  • JavaScript location.assign() is the only reliable navigation method — AppleScript set URL of t and Edge's --app-launch-url flag both fail due to PWA scope restrictions and server-side redirects.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment