diff --git a/whisper/example.env b/whisper/example.env
new file mode 100644
index 0000000..b60b650
--- /dev/null
+++ b/whisper/example.env
@@ -0,0 +1,2 @@
+TELEGRAM_BOT_TOKEN=
+TELEGRAM_CHAT_ID=
diff --git a/whisper/install.sh b/whisper/install.sh
new file mode 100755
index 0000000..78f7d55
--- /dev/null
+++ b/whisper/install.sh
@@ -0,0 +1,89 @@
+#!/bin/sh
+set -e
+
+BIN_SRC="./whisper"
+BIN_DST="/usr/bin/whisper"
+CONF_DIR="/etc/whisper"
+CONF_DST="$CONF_DIR/.env"
+CONF_SRC="./example.env"
+
+die() {
+ printf '%s\n' "$1" >&2
+ exit 1
+}
+
+usage() {
+ printf 'Usage: install.sh [-i|--install] [-u|--uninstall] [-h|--help]\n'
+ printf ' -i, --install Install whisper (default)\n'
+ printf ' -u, --uninstall Remove whisper\n'
+ printf ' -h, --help Show this help\n'
+ exit 0
+}
+
+do_install() {
+ printf 'This will:\n'
+ printf ' - Copy whisper to %s\n' "$BIN_DST"
+ printf ' - Create %s/ with config\n\n' "$CONF_DIR"
+
+ printf 'Proceed? [Y/n] '
+ read -r answer
+ case "$answer" in
+ [nN]) printf 'Aborted.\n'; exit 0 ;;
+ esac
+
+ [ -f "$BIN_SRC" ] || die "Cannot find $BIN_SRC in current directory"
+ [ -f "$CONF_SRC" ] || die "Cannot find $CONF_SRC in current directory"
+
+ cp "$BIN_SRC" "$BIN_DST"
+ chown root:root "$BIN_DST"
+ chmod 755 "$BIN_DST"
+
+ mkdir -p "$CONF_DIR"
+
+ if [ ! -f "$CONF_DST" ]; then
+ cp "$CONF_SRC" "$CONF_DST"
+ else
+ printf 'Keeping existing %s\n' "$CONF_DST"
+ fi
+
+ chown root:root "$CONF_DST"
+ chmod 600 "$CONF_DST"
+
+ printf '\nInstalled. Edit %s and set your values.\n' "$CONF_DST"
+}
+
+do_uninstall() {
+ printf 'This will remove:\n'
+ printf ' - %s\n' "$BIN_DST"
+ printf ' - %s/\n\n' "$CONF_DIR"
+
+ printf 'Proceed? [y/N] '
+ read -r answer
+ case "$answer" in
+ [yY]) ;;
+ *) printf 'Aborted.\n'; exit 0 ;;
+ esac
+
+ rm -f "$BIN_DST"
+ rm -rf "$CONF_DIR"
+
+ printf 'Done.\n'
+}
+
+# --- main ---
+[ "$(id -u)" -eq 0 ] || die "Must be run as root"
+
+ACTION="install"
+while [ $# -gt 0 ]; do
+ case "$1" in
+ -i|--install) ACTION="install"; shift ;;
+ -u|--uninstall) ACTION="uninstall"; shift ;;
+ -h|--help) usage ;;
+ *) die "Unknown option: $1" ;;
+ esac
+done
+
+case "$ACTION" in
+ install) do_install ;;
+ uninstall) do_uninstall ;;
+esac
diff --git a/whisper/test.sh b/whisper/test.sh
new file mode 100755
index 0000000..8d525f9
--- /dev/null
+++ b/whisper/test.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+# manual smoke tests for whisper via telegram
+
+SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
+
+echo '--- test 1: plain text ---\n'
+"$SCRIPT_DIR/whisper" -t telegram -m "Hello from whisper test" || true
+
+echo '\n--- test 2: HTML formatting ---\n'
+"$SCRIPT_DIR/whisper" -t telegram -m "bold italic code" || true
+
+echo '\n--- test 3: multiline ---\n'
+MSG=$(printf 'Line one\nLine two\nLine three')
+"$SCRIPT_DIR/whisper" -t telegram -m "$MSG" || true
+
+echo '\nAll tests done.\n'
diff --git a/whisper/whisper b/whisper/whisper
new file mode 100755
index 0000000..e68592d
--- /dev/null
+++ b/whisper/whisper
@@ -0,0 +1,82 @@
+#!/bin/sh
+# whisper - send messages via transport (telegram)
+set -e
+
+die() {
+ printf 'whisper: %s\n' "$1" >&2
+ exit "${2:-1}"
+}
+
+usage() {
+ printf 'Usage: whisper -m MESSAGE [-t TRANSPORT]\n\n'
+ printf ' -m, --message Message to send (required)\n'
+ printf ' -t, --transport Transport to use (default: telegram)\n'
+ printf ' -h, --help Show this help\n'
+ exit 0
+}
+
+# --- config loading ---
+# source .env files if they exist (later file wins, env vars win over both)
+SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
+for _envfile in /etc/whisper/.env "$SCRIPT_DIR/.env"; do
+ [ -f "$_envfile" ] && . "$_envfile"
+done
+unset _envfile
+
+# --- dependency check ---
+command -v curl >/dev/null 2>&1 || die "curl is required but not found" 1
+
+# --- arg parsing ---
+TRANSPORT="telegram"
+MESSAGE=""
+
+while [ $# -gt 0 ]; do
+ case "$1" in
+ -m|--message)
+ [ -n "${2+x}" ] || die "missing value for $1" 1
+ MESSAGE="$2"; shift 2 ;;
+ -t|--transport)
+ [ -n "${2+x}" ] || die "missing value for $1" 1
+ TRANSPORT="$2"; shift 2 ;;
+ -h|--help)
+ usage ;;
+ *)
+ die "unknown option: $1" 1 ;;
+ esac
+done
+
+[ -n "$MESSAGE" ] || die "message is required (-m)" 1
+
+# --- env validation ---
+validate_telegram_env() {
+ [ -n "$TELEGRAM_BOT_TOKEN" ] || die "TELEGRAM_BOT_TOKEN is not set" 1
+ [ -n "$TELEGRAM_CHAT_ID" ] || die "TELEGRAM_CHAT_ID is not set" 1
+}
+
+# --- transports ---
+send_telegram() {
+ validate_telegram_env
+ local url="https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage"
+
+ local response
+ response=$(curl -s -X POST "$url" \
+ -d chat_id="$TELEGRAM_CHAT_ID" \
+ --data-urlencode text="$MESSAGE" \
+ -d parse_mode="HTML")
+
+ if echo "$response" | grep -q '"ok":true'; then
+ printf 'whisper: message sent\n' >&2
+ return 0
+ else
+ local err
+ err=$(echo "$response" | sed -n 's/.*"description":"\([^"]*\)".*/\1/p')
+ printf 'whisper: send failed: %s\n' "${err:-unknown error}" >&2
+ exit 2
+ fi
+}
+
+# --- dispatch ---
+case "$TRANSPORT" in
+ telegram) send_telegram ;;
+ *) die "unknown transport: $TRANSPORT" 1 ;;
+esac