Mac Cron Jobs + Tailscale + Telegram = Chuck Norris at Random Times 🥋
Mac Cron Jobs + Tailscale + Telegram = Automated Awesomeness 🥋
TL;DR: Learn how to create automated Mac cron jobs that report to Telegram via your VPS using Tailscale mesh networking. We’ll use Chuck Norris jokes as our example because… why not? 🎖️
The Mission
Create a system where:
- ✅ Your Mac runs scheduled tasks (cron jobs)
- ✅ Tasks communicate via Tailscale mesh network
- ✅ Results appear in Telegram instantly
- ✅ Bonus: Random Chuck Norris facts throughout the day!
Why? Because if it works for Chuck Norris jokes, it works for ANYTHING - server monitoring, backup notifications, security alerts, you name it!
Architecture Overview
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌─────────────────────────────────────────────────────┐
│ YOUR MAC (M3 Pro, M4 Max, etc.) │
│ ┌──────────────────────────────────────────────┐ │
│ │ CRON JOB (runs at scheduled times) │ │
│ │ ↓ │ │
│ │ Python Script │ │
│ │ ↓ │ │
│ │ Telegram API (sends message) │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ Optional: Via Tailscale to VPS │
│ ↓ │
└─────────────────────────────────────────────────────┘
↓ (over internet)
┌─────────────────────────────────────────────────────┐
│ TELEGRAM BOT API │
│ ↓ │
│ Your Telegram App (notifications!) │
└─────────────────────────────────────────────────────┘
Part 1: Understanding Mac Cron Jobs
What is Cron?
Cron is the Unix/Linux/Mac task scheduler. It runs commands at specified times automatically.
Format:
1
2
3
4
5
6
7
* * * * * command_to_run
│ │ │ │ │
│ │ │ │ └─── Day of week (0-7, Sunday = 0 or 7)
│ │ │ └───── Month (1-12)
│ │ └─────── Day of month (1-31)
│ └───────── Hour (0-23)
└─────────── Minute (0-59)
Examples:
1
2
3
4
5
6
7
8
9
10
11
# Every hour at minute 15
15 * * * * /path/to/script.sh
# Every day at 9:30 AM
30 9 * * * python3 /path/to/script.py
# Every Monday at 8:00 AM
0 8 * * 1 /path/to/backup.sh
# Every 5 minutes
*/5 * * * * /path/to/monitor.py
Mac-Specific Cron Setup
1. Edit your crontab:
1
crontab -e
2. View current crontab:
1
crontab -l
3. Important for Mac:
macOS has strict permissions. You need to grant cron access to:
- Full Disk Access (System Settings → Privacy & Security → Full Disk Access → cron)
- Or use
~/paths that don’t need special permissions
4. Logs:
Unlike Linux, macOS doesn’t have /var/log/cron. Instead:
- Redirect output to your own log:
>> ~/myscript.log 2>&1 - Check system logs:
log show --predicate 'process == "cron"' --last 1h
Part 2: Tailscale Mesh Network (Optional but Awesome!)
What is Tailscale?
Tailscale creates a secure mesh network (VPN) between all your devices. Each device gets a persistent IP address (e.g., 100.118.23.119).
Why use it?
- 🔒 Secure: Encrypted WireGuard VPN
- 🌐 Access anywhere: SSH to your Mac from your VPS
- 🚀 Fast: Peer-to-peer when possible
- 🎯 No firewall config: Works behind NAT
Setup Tailscale
On Mac:
1
2
3
4
5
6
7
8
9
# Install
brew install tailscale
# Start and connect
sudo tailscale up
# Get your IP
tailscale ip -4
# Example output: 100.118.23.119
On VPS:
1
2
3
4
5
6
7
8
9
# Install (Ubuntu/Debian)
curl -fsSL https://tailscale.com/install.sh | sh
# Start
sudo tailscale up
# Get IP
tailscale ip -4
# Example output: 100.103.164.7
Test connectivity:
1
2
3
4
5
# From Mac, ping VPS
ping 100.103.164.7
# SSH to VPS via Tailscale
ssh user@100.103.164.7
Part 3: Telegram Bot Setup
Create a Telegram Bot
- Talk to @BotFather on Telegram
- Send
/newbot - Choose a name and username
- Save the API token (looks like:
1234567890:ABCdefGHIjklMNOpqrsTUVwxyz)
Get Your Chat ID
1
2
3
4
# Send a message to your bot first, then:
curl https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates
# Look for "chat":{"id":123456789}
Test Sending a Message
1
2
3
4
curl -X POST \
https://api.telegram.org/bot<YOUR_BOT_TOKEN>/sendMessage \
-d chat_id=<YOUR_CHAT_ID> \
-d text="Hello from Terminal!"
Part 4: The Chuck Norris Telegram Bot 🥋
Now for the fun part! Let’s create a bot that sends random Chuck Norris jokes to Telegram.
The Python Script
File: ~/chuck_norris_telegram.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#!/usr/bin/env python3
"""
CHUCK NORRIS TELEGRAM BOT
Sends random Chuck Norris jokes to Telegram!
"""
import json
import urllib.request
import random
import os
from datetime import datetime, timezone
# Configuration
TELEGRAM_BOT_TOKEN = "YOUR_BOT_TOKEN_HERE"
TELEGRAM_CHAT_ID = "YOUR_CHAT_ID_HERE"
# Chuck Norris API
CHUCK_API = "https://api.chucknorris.io/jokes/random"
def get_chuck_fact():
"""Get a random Chuck Norris fact from the API"""
try:
req = urllib.request.Request(CHUCK_API)
with urllib.request.urlopen(req, timeout=5) as response:
data = json.loads(response.read().decode())
return data.get('value', None)
except Exception as e:
print(f"Error: {e}")
return "Chuck Norris doesn't need APIs. APIs need Chuck Norris."
def send_telegram(text):
"""Send message to Telegram"""
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
data = json.dumps({
"chat_id": TELEGRAM_CHAT_ID,
"text": text,
"parse_mode": "Markdown"
}).encode()
try:
req = urllib.request.Request(
url,
data=data,
headers={"Content-Type": "application/json"}
)
urllib.request.urlopen(req, timeout=10)
return True
except Exception as e:
print(f"Telegram error: {e}")
return False
def main():
"""Main function"""
print("🥋 Chuck Norris Telegram Bot - Starting...")
# Get random Chuck Norris fact
fact = get_chuck_fact()
print(f"Fact: {fact[:100]}...")
# Format message
now = datetime.now(timezone.utc).strftime("%H:%M UTC %b %d")
message = f"""🥋 *CHUCK NORRIS FACT OF THE MOMENT* 🥋
📅 {now}
_{fact}_
**Chuck Norris doesn't wait for cron jobs.**
**Cron jobs wait for Chuck Norris!** 🥋"""
# Send to Telegram
if send_telegram(message):
print("✅ Chuck Norris fact delivered!")
else:
print("❌ Failed to send")
if __name__ == "__main__":
main()
Make it executable:
1
chmod +x ~/chuck_norris_telegram.py
Test it:
1
python3 ~/chuck_norris_telegram.py
You should see a Chuck Norris joke in your Telegram! 🎉
Part 5: Random Cron Jobs (The Fun Part!)
Instead of predictable times, let’s make Chuck Norris appear at RANDOM times throughout the day!
Random Cron Generator
File: ~/chuck_random_cron_generator.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/env python3
"""Generate random cron times for Chuck Norris jokes"""
import random
def generate_random_cron_times(num_times=5):
"""Generate random cron job times"""
times_used = set()
while len(times_used) < num_times:
hour = random.randint(0, 23)
minute = random.randint(0, 59)
time_key = f"{hour:02d}:{minute:02d}"
if time_key not in times_used:
times_used.add(time_key)
yield (minute, hour)
# Generate 5-8 random times
num_times = random.randint(5, 8)
cron_times = sorted(generate_random_cron_times(num_times),
key=lambda x: (x[1], x[0]))
print("# Chuck Norris Random Facts")
for minute, hour in cron_times:
print(f"{minute} {hour} * * * python3 ~/chuck_norris_telegram.py >> ~/chuck_norris.log 2>&1")
Run it:
1
python3 ~/chuck_random_cron_generator.py
Output example:
1
2
3
4
5
6
# Chuck Norris Random Facts
23 2 * * * python3 ~/chuck_norris_telegram.py >> ~/chuck_norris.log 2>&1
47 7 * * * python3 ~/chuck_norris_telegram.py >> ~/chuck_norris.log 2>&1
12 11 * * * python3 ~/chuck_norris_telegram.py >> ~/chuck_norris.log 2>&1
38 15 * * * python3 ~/chuck_norris_telegram.py >> ~/chuck_norris.log 2>&1
5 19 * * * python3 ~/chuck_norris_telegram.py >> ~/chuck_norris.log 2>&1
Install to Crontab
1
2
3
4
5
6
7
8
9
10
11
# Generate and copy to clipboard (macOS)
python3 ~/chuck_random_cron_generator.py | pbcopy
# Edit crontab
crontab -e
# Paste the entries
# Save and exit (:wq in vim)
# Verify
crontab -l
Pro tip: Re-run the generator weekly/monthly for different random times! 🎲
Part 6: Advanced - VPS Relay (Optional)
Want your Mac to send via VPS? Here’s how:
Setup on VPS
File: vps_telegram_relay.py (on VPS)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#!/usr/bin/env python3
"""
VPS Telegram Relay
Receives messages via HTTP and forwards to Telegram
"""
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
import urllib.request
TELEGRAM_BOT_TOKEN = "YOUR_TOKEN"
TELEGRAM_CHAT_ID = "YOUR_CHAT_ID"
class RelayHandler(BaseHTTPRequestHandler):
def do_POST(self):
# Read message from Mac
content_length = int(self.headers['Content-Length'])
body = self.rfile.read(content_length)
data = json.loads(body.decode())
# Forward to Telegram
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
telegram_data = json.dumps({
"chat_id": TELEGRAM_CHAT_ID,
"text": data.get('message', ''),
"parse_mode": "Markdown"
}).encode()
req = urllib.request.Request(url, data=telegram_data,
headers={"Content-Type": "application/json"})
urllib.request.urlopen(req, timeout=10)
self.send_response(200)
self.end_headers()
if __name__ == "__main__":
server = HTTPServer(('0.0.0.0', 8080), RelayHandler)
print("VPS Relay listening on port 8080...")
server.serve_forever()
Mac sends via VPS
Modified Mac script:
1
2
3
4
5
6
7
8
9
# Instead of direct Telegram, send to VPS
def send_via_vps(message):
vps_ip = "100.103.164.7" # Your VPS Tailscale IP
url = f"http://{vps_ip}:8080/relay"
data = json.dumps({"message": message}).encode()
req = urllib.request.Request(url, data=data,
headers={"Content-Type": "application/json"})
urllib.request.urlopen(req, timeout=10)
Why? Separation of concerns - VPS handles all Telegram tokens, Mac just sends data to VPS.
Part 7: Real-World Applications
Once you have this setup, you can monitor ANYTHING:
Server Health Monitor
1
2
3
4
5
6
7
8
def get_system_stats():
cpu = subprocess.check_output("top -l 1 | grep 'CPU usage'", shell=True)
memory = subprocess.check_output("vm_stat | head -5", shell=True)
disk = subprocess.check_output("df -h /", shell=True)
return f"CPU: {cpu}\nMemory: {memory}\nDisk: {disk}"
# Cron: Every hour
# 0 * * * * python3 ~/system_monitor.py
Backup Completion Alerts
1
2
3
4
5
6
7
8
def check_backup():
backup_dir = "/Backups/daily"
latest = max(os.listdir(backup_dir), key=os.path.getctime)
size = os.path.getsize(latest)
return f"Latest backup: {latest} ({size / 1e9:.2f} GB)"
# Cron: Daily at 3 AM (after backup runs at 2 AM)
# 0 3 * * * python3 ~/backup_check.py
Website Uptime Monitor
1
2
3
4
5
6
7
8
9
10
def check_website(url):
try:
response = urllib.request.urlopen(url, timeout=10)
status = response.getcode()
return f"✅ {url} is UP (HTTP {status})"
except:
return f"❌ {url} is DOWN!"
# Cron: Every 15 minutes
# */15 * * * * python3 ~/uptime_monitor.py
Security Alerts
1
2
3
4
5
6
7
def check_failed_logins():
logs = subprocess.check_output("last -f /var/log/auth.log | grep 'FAILED'", shell=True)
if logs:
return f"⚠️ Failed login attempts detected:\n{logs}"
# Cron: Every 5 minutes
# */5 * * * * python3 ~/security_monitor.py
Part 8: Troubleshooting
Cron Job Not Running?
Check if cron is running:
1
sudo launchctl list | grep cron
Check permissions:
- System Settings → Privacy & Security → Full Disk Access
- Add
/usr/sbin/cronif needed
Check logs:
1
2
3
4
5
# System logs
log show --predicate 'process == "cron"' --last 1h
# Your script logs
tail -f ~/chuck_norris.log
Test script manually:
1
2
# Run as if cron is running it
env -i HOME=$HOME USER=$USER PATH=/usr/bin:/bin python3 ~/chuck_norris_telegram.py
Telegram Not Sending?
Verify bot token:
1
curl https://api.telegram.org/bot<YOUR_TOKEN>/getMe
Verify chat ID:
1
2
3
4
# Send test message
curl -X POST https://api.telegram.org/bot<YOUR_TOKEN>/sendMessage \
-d chat_id=<YOUR_CHAT_ID> \
-d text="Test"
Check firewall:
1
2
# Ensure outbound HTTPS (443) is allowed
curl https://api.telegram.org
Tailscale Not Connecting?
Check status:
1
tailscale status
Restart:
1
2
sudo tailscale down
sudo tailscale up
Test connectivity:
1
2
# Ping VPS
ping $(tailscale ip -4 --peer vps-hostname)
Part 9: Best Practices
Security
- Never commit tokens to git:
1 2 3 4 5
# Store in environment variables export TELEGRAM_BOT_TOKEN="your_token" export TELEGRAM_CHAT_ID="your_id" # Or use config file (add to .gitignore)
- Use restricted bot permissions:
- Only allow sending messages to specific chat
- Don’t give bot admin rights
- Tailscale ACLs:
- Restrict which devices can talk to which
- Use tags for organization
Logging
1
2
3
4
5
6
7
8
9
import logging
logging.basicConfig(
filename=os.path.expanduser('~/chuck_norris.log'),
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logging.info("Chuck Norris fact sent successfully")
Error Handling
1
2
3
4
5
6
7
8
9
def safe_send_telegram(text, max_retries=3):
for attempt in range(max_retries):
try:
send_telegram(text)
return True
except Exception as e:
logging.error(f"Attempt {attempt + 1} failed: {e}")
time.sleep(2 ** attempt) # Exponential backoff
return False
Monitoring Your Monitor
1
2
3
4
5
6
7
# Heartbeat - send daily "I'm alive" message
def send_heartbeat():
message = f"✅ Chuck Norris monitor is alive! Last run: {datetime.now()}"
send_telegram(message)
# Cron: Daily at 8 AM
# 0 8 * * * python3 ~/heartbeat.py
Conclusion
You now have:
- ✅ Mac cron jobs running automated tasks
- ✅ Tailscale mesh network (optional but awesome)
- ✅ Telegram notifications for everything
- ✅ Random Chuck Norris facts throughout the day!
The beauty: This pattern works for ANY automation:
- Server monitoring
- Backup alerts
- Security notifications
- Website uptime checks
- Database backups
- Git commit reminders
- Whatever you dream up!
Resources
- Cron:
man crontab,man 5 crontab - Tailscale: https://tailscale.com/kb/
- Telegram Bot API: https://core.telegram.org/bots/api
- Chuck Norris API: https://api.chucknorris.io/
The Files
All code from this tutorial:
chuck_norris_telegram.py - Main bot script chuck_random_cron_generator.py - Random cron generator vps_telegram_relay.py - Optional VPS relay
Available at: OpenClaw Tools
Remember: Chuck Norris doesn’t need automation. Automation needs Chuck Norris! 🥋
But seriously, this setup has transformed how I monitor my entire infrastructure. Every Mac, every VPS, every Raspberry Pi - all reporting to Telegram. It’s like having eyes everywhere! 👀
Rangers lead the way! 🎖️
Written by David Keane (Irish Ranger) with AI Commander AIRanger (Claude Sonnet 4.5) February 21, 2026