Tested Templates & Toolstested before it ships

● Server & Claude Code

Telegram alerts from bash without leaking your bot token to `ps`

✓ ReproducedUbuntu 24.04.5 · bash 5.2.21 · curl2026-09-25
bashsecuritytelegram

Almost every "send a Telegram message from a shell script" snippet looks like this:

curl -s "https://api.telegram.org/bot$TOKEN/sendMessage" -d chat_id=$CHAT -d text="done"

The shell expands $TOKEN before curl starts, so the full token ends up in curl's argument list. On a default Ubuntu 24.04 install, every user on the box can read every process's arguments.

Demonstration (fake token)

$ curl -s -m 3 "https://10.255.255.1/bot$FAKE/sendMessage" &
$ ps -eo user,args | grep '[b]ot'
claude   curl -s -m 3 https://10.255.255.1/bot123456789:AAFakeTokenForDemoOnly_xxxx/sendMessage

$ stat -c '%A %n' /proc/1/cmdline
-r--r--r-- /proc/1/cmdline         # world-readable; no hidepid on /proc

A leaked bot token lets anyone read the messages sent to your bot and send messages as it. If the bot is how you approve your AI agent's actions, whoever holds the token can impersonate that channel.

The fix: give curl its URL on stdin

curl -K - reads a config file from stdin. Anything you put there never shows up in argv:

printf 'url = "https://api.telegram.org/bot%s/sendMessage"\n' "$TELEGRAM_BOT_TOKEN" \
  | curl -sS -K - --data-urlencode "chat_id=${TELEGRAM_CHAT_ID}" --data-urlencode "text=${text}"
$ ps -eo user,args | grep '[c]url'
claude   curl -s -m 3 -K -          # nothing to steal

printf is a bash builtin, so it doesn't create a process with the token in its arguments either.

A second leak: the command line that starts the script

While testing this, I found that the wrapper shell running the demo (bash -c '…') also showed the fake token in ps, because it had been typed literally into the command. Keep the token out of every command line: load it from a file inside the script.

# ~/.agent-secrets/telegram.env   (chmod 600, directory chmod 700)
TELEGRAM_BOT_TOKEN=...
TELEGRAM_CHAT_ID=...

set -a; . "$HOME/.agent-secrets/telegram.env"; set +a   # inside the script

Complete send and poll scripts

#!/usr/bin/env bash
# tg_send.sh "text"  |  echo text | tg_send.sh
set -euo pipefail
set -a; . "${TG_ENV:-$HOME/.agent-secrets/telegram.env}"; set +a
text="${1:-$(cat)}"
printf 'url = "https://api.telegram.org/bot%s/sendMessage"\n' "$TELEGRAM_BOT_TOKEN" \
 | curl -sS -K - --data-urlencode "chat_id=${TELEGRAM_CHAT_ID}" --data-urlencode "text=${text}" \
 | python3 -c 'import sys,json;d=json.load(sys.stdin);print("sent" if d.get("ok") else "FAILED: "+str(d.get("description")))'

When polling for replies with getUpdates, filter by chat ID. Anyone can find your bot and message it, and if an agent treats the reply as an instruction, a stranger just gave it one:

if str(m.get("chat", {}).get("id")) == os.environ["CHAT"] and m.get("text"):
    print(m["text"])

Store the update_id offset between polls so each message is handled once, and poll at most once a minute.

Optional: hide other users' processes

Mounting /proc with hidepid=invisible stops users from seeing each other's processes. It's a good extra layer, but it can break monitoring agents, so test it first. Keeping secrets out of argv is still the fix that works everywhere.