Post

Mac Cron Jobs + Tailscale + Telegram = Chuck Norris at Random Times 🥋

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:

  1. ✅ Your Mac runs scheduled tasks (cron jobs)
  2. ✅ Tasks communicate via Tailscale mesh network
  3. ✅ Results appear in Telegram instantly
  4. ✅ 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

  1. Talk to @BotFather on Telegram
  2. Send /newbot
  3. Choose a name and username
  4. 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/cron if 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

  1. 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)
    
  2. Use restricted bot permissions:
    • Only allow sending messages to specific chat
    • Don’t give bot admin rights
  3. 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

This post is licensed under CC BY 4.0 by the author.