Two Claude Code hooks that fix the most common failures with the official Telegram plugin. Both run as deterministic Python — zero Claude tokens, zero model inference.
Drop these in if your Telegram plugin throws
409 Conflictbetween sessions, or if you've ever caught your agent silently dropping a Telegram reply.
| Hook event | Name | What it does |
|---|---|---|
| SessionStart | Telegram Cleanup | Kills stale Telegram processes from the last session so the new one starts clean. |
| Stop | Reply Guard | Checks every Telegram message got a reply before the session ends — so nothing gets dropped. |
The 409 Conflict bug. Telegram allows exactly one getUpdates consumer per bot token. On Windows the plugin's clean-shutdown path is unreliable — when a terminal is X'd out, the machine sleeps, or a session crashes hard, the bot subprocess can linger and hold the token. The next session then fails with 409 Conflict: terminated by other getUpdates request. Telegram Cleanup fixes this by reading the plugin's bot.pid file at SessionStart and force-killing whatever PID it points to before the new MCP connection tries to grab the token.
The dropped-reply bug. The Telegram plugin only delivers messages to the agent as terminal text — it never reaches the user's chat unless the agent calls the reply tool. Sometimes the agent forgets. Reply Guard runs at Stop, walks the transcript backwards to find the most recent Telegram inbound, and confirms a reply tool call followed it. If not, it sends a fallback message directly via Telegram's Bot API so the user knows their message was received but the agent dropped it.
git clone https://github.com/robonuggets/telegram-hook-fix
Then:
- Copy both
hooks/*.pyfiles to a stable location on your machine —~/.claude/hooks/is a sensible default. - Open your Claude Code settings (
~/.claude/settings.json) and merge thehooksblock fromsettings.example.json. Update the absolute paths to match where you placed the scripts. - Restart Claude Code. Done.
telegram-hook-fix/
├── README.md
├── LICENSE
├── settings.example.json
├── .claude/skills/telegram-hook-fix/SKILL.md
└── hooks/
├── telegram-preemptive-kill.py (SessionStart · Telegram Cleanup)
└── telegram-reply-guard.py (Stop · Reply Guard)
settings.example.json contains the hook config to merge into your existing ~/.claude/settings.json. The shape:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "python /absolute/path/to/hooks/telegram-preemptive-kill.py \"~/.claude/channels/telegram\"",
"timeout": 10
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "python /absolute/path/to/hooks/telegram-reply-guard.py \"~/.claude/channels/telegram\"",
"timeout": 15
}
]
}
]
}
}The path passed as the second argument is the channel state directory — where the Telegram plugin keeps its bot.pid and .env. Default for the official plugin on most systems is ~/.claude/channels/telegram. If you've configured a different location, point both hooks at it.
If you already have a hooks block in your settings, merge — don't overwrite. Multiple SessionStart / Stop hooks can coexist; just append to the hooks array.
Telegram Cleanup (telegram-preemptive-kill.py)
- Runs at SessionStart, before MCP servers spin up.
- Reads
<channel_state_dir>/bot.pid. - Force-kills that PID via
taskkill /Fon Windows. (POSIX: swap forkill -9.) - Removes the pid file so the plugin starts fresh.
- Exits 0 always — a hook crash must never block session start.
- Logs to
<channel_state_dir>/preemptive-kill.log.
Reply Guard (telegram-reply-guard.py)
- Runs at Stop, after the agent's turn ends.
- Reads
transcript_pathfrom the hook payload (Claude Code's session transcript). - Walks backwards to find the most recent user message containing a
<channel source="plugin:telegram:telegram" chat_id="...">tag. - From there, walks forward looking for an assistant
tool_usewhose name ismcp__plugin_telegram_telegram__reply. - If found → no-op (logged).
- If not found → sends a fallback message directly to that
chat_idviahttps://api.telegram.org/bot<TOKEN>/sendMessage, so the user sees:⚠️ Session ended without a Telegram reply — I may have forgotten to use the reply tool. Ping me again if you need a response. - Token lookup order:
$TELEGRAM_BOT_TOKENenv var, then<channel_state_dir>/.env. - Exits 0 always.
- Logs to
<channel_state_dir>/reply-guard.log.
The Cleanup hook uses taskkill /F /PID <pid> — Windows-native. If you're on macOS or Linux, swap that subprocess call for os.kill(pid, signal.SIGKILL) or subprocess.run(["kill", "-9", str(pid)]). Pull requests welcome.
The Reply Guard is platform-agnostic — pure Python stdlib, only needs internet to hit the Bot API.
- Hook doesn't fire at all — confirm
pythonresolves on your PATH. Runpython --versionin a fresh terminal. If it doesn't resolve, swappythonfor the absolute path to your interpreter insidesettings.json. - Reply Guard never sends a fallback — the script needs the bot token. It checks
$TELEGRAM_BOT_TOKENfirst, then<channel_state_dir>/.env. If neither is set, the fallback is silent — check<channel_state_dir>/reply-guard.logfor ano token in envline. - Cleanup runs but I still see 409 — the plugin may have already grabbed the token before the hook ran. Kill the process manually once (
taskkill /F /PID <pid>on Windows,kill -9 <pid>elsewhere), then start a fresh session.
CC BY 4.0 — free to use with attribution.