One Telegram Bot + Two Machines = Silent Message Loss

Running the same Telegram bot relay as a systemd service on multiple machines means both instances poll the same bot token. Telegram delivers each update to one consumer only — so messages vanish at random, or land on the wrong host. Both sides think they're healthy; neither is.

The fix: one bot per machine, one channel per bot. Each channel becomes a dedicated console for exactly one host. No coordination needed, no distributed locks, no split-brain.

To manage multiple bot tokens in a shared dotfiles repo, key them by hostname:

# dotfiles — all tokens in one file, encrypted as usual
export TG_TOKEN_homeserver="token_aaa"
export TG_TOKEN_vps="token_bbb"

# relay picks its own token at runtime
export TG_TOKEN=$(eval echo \$TG_TOKEN_$(hostname))

Every machine runs the same service file unchanged — hostname resolves to the right token automatically. Adding a third machine is just a new bot, a new channel, and one more TG_TOKEN_<hostname> line.

TG Channel: homeserver  →  bot-A  →  machine 1
TG Channel: vps         →  bot-B  →  machine 2
TG Channel: pi          →  bot-C  →  machine 3

`exec bash` vs `bash`: don't nest your shells

After changing your hostname with hostnamectl set-hostname, the current terminal still shows the old name. You want a fresh shell — but bash and exec bash behave differently.

Running bash spawns a child process. Your old shell is still alive underneath, and $SHLVL increments by one. Environment changes (like the new hostname) are picked up, but you've added a layer:

$ echo $SHLVL
1
$ bash
$ echo $SHLVL
2

Nested shells can cause subtle issues — exit only drops you back to the parent, aliases and env vars may differ between layers, and deep nesting is easy to accidentally accumulate.

exec bash replaces the current process in-place (same PID). The old shell is gone, a new one takes over, and $SHLVL stays the same:

$ echo $SHLVL
1
$ exec bash
$ echo $SHLVL
1

This is the clean way to reload your shell config after changing .bashrc, updating $HOSTNAME, or any other env-level change. No nesting, no leftover state.

Telegram Bot Custom Commands: setMyCommands and the empty-array delete

Telegram bots can show a command menu when users type / in a chat. Registering commands is a single API call — no approval, no webhook setup, no special bot-side handling.

curl -s -X POST "https://api.telegram.org/bot<TOKEN>/setMyCommands" \
  -H "Content-Type: application/json" \
  -d '{"commands": [
    {"command": "status", "description": "Show active host"},
    {"command": "switch", "description": "Switch active host"}
  ]}'

From the bot's perspective, /status or /switch desktop arrives as plain text in message.text — you parse it like any other message.

Scope-aware deletion

deleteMyCommands supports optional scope and language_code parameters so you can clear commands for specific contexts (private chats, groups, a single chat, a specific language). But there's no API to delete individual commands.

The elegant part: setMyCommands always does a full overwrite. Sending an empty array clears everything:

# These are equivalent:
curl -s -X POST "https://api.telegram.org/bot<TOKEN>/setMyCommands" \
  -d '{"commands": []}'

curl -s -X POST "https://api.telegram.org/bot<TOKEN>/deleteMyCommands"

To remove a single command, just setMyCommands with the updated list minus the one you want gone. The overwrite-everything semantics means there's no "add" or "remove" — only "set the complete list." deleteMyCommands is a convenience method, not a necessity.

Tmux Windows Replace Terminal Tabs (And Then Some)

If you're still using terminal tabs, tmux windows do the same job with extras you didn't know you needed.

The hierarchy: Session > Window > Pane. A session holds multiple windows, each window can be split into panes. The bottom status bar shows all windows at a glance:

[main] 0:bash* 1:vim- 2:server

* marks the active window, - marks the previous one. You always know where you are.

Key bindings to internalize:

Action Key
New window Ctrl+b c
Next window Ctrl+b n
Previous window Ctrl+b p
Jump by number Ctrl+b 0~9
List all windows Ctrl+b w
Rename window Ctrl+b ,

What tmux gives you over plain terminal tabs:

  • Survives disconnects — SSH drops? Reconnect with tmux attach, everything is still there. Terminal tabs are gone the moment the connection dies.
  • Panes — Split any window horizontally or vertically without opening another tab.
  • Scriptable — Create and arrange windows/panes from a script or config.

The one real risk: if the machine itself reboots, the session is gone. tmux-resurrect and tmux-continuum can save/restore layouts, but in practice most people use tmux to survive network hiccups, not server reboots. A reboot means re-opening a session, which takes seconds.

nohup Is Insurance, disown Is a Lifeline

Both prevent a process from dying when you close the terminal, but they work at different stages and with different guarantees.

nohup intervenes before launch. It sets the process itself to ignore SIGHUP via sigaction, so no matter who sends the signal, the process won't react. It also redirects stdout/stderr to nohup.out (or a file you specify). POSIX-standard, portable to any shell.

disown intervenes after launch. It removes the process from the shell's job table so the shell won't notify it on exit — but the process has no built-in signal immunity. Some SSH/terminal implementations broadcast SIGHUP to the entire process group on disconnect, bypassing the shell entirely. In that case disown alone won't save you.

# nohup: know you need it upfront
nohup python train.py > train.log 2>&1 &

# disown: the "oh crap I need to leave" rescue
./long_task.sh        # already running...
# Ctrl+Z to suspend, then:
bg
disown %1

# Belt and suspenders — use both
nohup cmd > out.log 2>&1 &
disown

The combination is the safest one-liner: nohup gives the process its own signal shield, disown cleans up the job table so closing the terminal is truly a no-op.

For anything long-lived (a server, a daemon), neither is the right tool — use systemd, supervisord, or tmux. nohup and disown are emergency measures, not service managers.

One gotcha with nohup: if you don't redirect output yourself, it writes to ./nohup.out, which can quietly grow to fill a disk. Always pair it with an explicit > log 2>&1.