Skip to content

Instantly share code, notes, and snippets.

@0xBigBoss
Created February 18, 2026 11:59
Show Gist options
  • Select an option

  • Save 0xBigBoss/ed55e9620c8a0fb663956f7303ff3a08 to your computer and use it in GitHub Desktop.

Select an option

Save 0xBigBoss/ed55e9620c8a0fb663956f7303ff3a08 to your computer and use it in GitHub Desktop.
Workaround for Tailscale MagicDNS short names broken on macOS 26 (github.com/tailscale/tailscale/issues/17096)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.tailscale.hosts-sync</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/tailscale-hosts-sync</string>
</array>
<key>StartInterval</key>
<integer>30</integer>
<key>RunAtLoad</key>
<true/>
<key>StandardErrorPath</key>
<string>/tmp/tailscale-hosts-sync.err</string>
</dict>
</plist>
#!/bin/bash
# tailscale-hosts-sync — Syncs Tailscale peer short names into /etc/hosts.
#
# Workaround for macOS 26 breaking MagicDNS short name resolution.
# macOS 26 no longer expands single-label hostnames using search domains
# from Supplemental DNS resolvers (which is how Tailscale registers DNS).
# See: https://github.com/tailscale/tailscale/issues/17096
#
# This script parses `tailscale status --json`, extracts short DNS names
# and IPv4 addresses, and writes them into a managed block in /etc/hosts.
# A LaunchDaemon runs it every 30 seconds to keep entries in sync.
#
# Files:
# /usr/local/bin/tailscale-hosts-sync (this script)
# /Library/LaunchDaemons/com.tailscale.hosts-sync.plist (daemon)
# /etc/hosts (managed block)
#
# Uninstall:
# sudo launchctl bootout system/com.tailscale.hosts-sync
# sudo rm /Library/LaunchDaemons/com.tailscale.hosts-sync.plist
# sudo rm /usr/local/bin/tailscale-hosts-sync
# sudo sed -i '' '/# BEGIN tailscale-hosts-sync/,/# END tailscale-hosts-sync/d' /etc/hosts
set -euo pipefail
TAILSCALE="/Applications/Tailscale.app/Contents/MacOS/Tailscale"
HOSTS="/etc/hosts"
BEGIN_MARKER="# BEGIN tailscale-hosts-sync"
END_MARKER="# END tailscale-hosts-sync"
if ! "$TAILSCALE" status --json &>/dev/null; then
exit 0
fi
entries=$("$TAILSCALE" status --json 2>/dev/null | python3 -c "
import json, sys
data = json.load(sys.stdin)
lines = []
def add_node(node):
dns_name = node.get('DNSName', '')
ips = node.get('TailscaleIPs', [])
if not dns_name or not ips:
return
short = dns_name.split('.')[0]
if not short or short == 'localhost':
return
ipv4 = next((ip for ip in ips if '.' in ip), None)
if ipv4:
lines.append(f'{ipv4}\t{short}')
self_node = data.get('Self')
if self_node:
add_node(self_node)
for peer in data.get('Peer', {}).values():
add_node(peer)
lines.sort(key=lambda l: l.split('\t')[1])
print('\n'.join(lines))
")
if [ -z "$entries" ]; then
exit 0
fi
block_file=$(mktemp)
printf '%s\n%s\n%s\n' "$BEGIN_MARKER" "$entries" "$END_MARKER" > "$block_file"
if grep -q "$BEGIN_MARKER" "$HOSTS"; then
tmp=$(mktemp)
python3 -c "
import sys
begin, end, block_path, hosts_path = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
with open(block_path) as f:
block = f.read()
with open(hosts_path) as f:
lines = f.readlines()
out = []
skip = False
for line in lines:
if line.rstrip() == begin:
skip = True
continue
if skip and line.rstrip() == end:
skip = False
out.append(block)
continue
if not skip:
out.append(line)
print(''.join(out), end='')
" "$BEGIN_MARKER" "$END_MARKER" "$block_file" "$HOSTS" > "$tmp"
if ! cmp -s "$tmp" "$HOSTS"; then
cat "$tmp" > "$HOSTS"
fi
rm -f "$tmp"
else
printf '\n' >> "$HOSTS"
cat "$block_file" >> "$HOSTS"
fi
rm -f "$block_file"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment