Talk to your coding agent — Claude Code or Codex — from Telegram.
Agent2Telegram is a tiny, dependency‑free bridge. It long‑polls Telegram for messages from you (and only you), hands each one to the agent CLI of your choice, and sends the reply back. No public IP, no webhook, no cloud — it runs on your own machine, behind your router.
Telegram ⇄ Agent2Telegram ⇄ claude / codex
What you can send: plain text, images and files (downloaded and handed to the agent), and voice messages — transcribed automatically when you add your own ElevenLabs key (see Voice messages below).
- Robust by default — one bad message never crashes the loop; network errors, Telegram
flood‑control (
429), 4096‑char limits and Markdown parse failures are all handled. - Secure — the agent can run code on your machine, so only allow‑listed Telegram users can drive it. Everyone else is politely refused.
- Zero install friction — the core uses only the Python standard library. Nothing to
pip installfor it to work, which means far fewer “it doesn’t run on my machine” moments. - Works behind NAT — long polling, so no port‑forwarding or domain needed.
If Agents Monitoring
0.3.2+ is installed, setup and connect automatically register each bridge after
its process starts. Each config has an independent daemon/health row and restart
command; repeated registration updates the same entry. The registration preserves
the Python interpreter, PYTHONPATH and state directory without copying bot tokens
or changing Agents Monitoring's config directly. Existing stopped/enabled settings
in the monitor are preserved. Its keepalive must be enabled for automatic recovery.
Without the monitor, setup prints one informational sentence and continues normally.
An old/unreachable monitor produces a warning; upgrade it and run agentsmon add
to register an existing bridge. The bridge itself keeps running. Newly created
sessions require an explicit provider choice and confirmation; missing CLIs never
silently select another provider.
# 1) Get the code
git clone https://github.com/petrludwig-collab/Agent2Telegram.git
cd Agent2Telegram
# 2) Run the setup wizard (pick agent → paste token → authorize yourself)
python3 -m agent2telegram setup
# 3) Start the bridge
python3 -m agent2telegram run…or the one‑liner:
curl -fsSL https://raw.githubusercontent.com/petrludwig-collab/Agent2Telegram/main/install.sh | bash- Provider — it auto-detects which agents are installed (Claude Code / Codex) and you pick one.
- Session — attach to an existing tmux session or create a fresh one (the wizard launches the chosen agent in it for you).
- Telegram — paste the bot token from @BotFather; the wizard verifies it live, then captures your user id from the first message you send the bot and wires everything up. It can start the bridge for you on the spot.
Codex needs no extra setup — its rollout log records turn boundaries. For Claude Code the wizard also registers the end-of-turn Stop hook automatically.
Manual Claude attach configurations are covered too: run checks and registers the Stop
hook in the active user settings (CLAUDE_CONFIG_DIR/settings.json, or
~/.claude/settings.json). Registration is atomic and idempotent, preserves other settings,
and recognises custom Python hooks that write this bridge's turn_end marker using a
literal path. Invalid/unreadable settings and disabled hooks produce a warning and are
left untouched. Project/managed policy overrides and dynamically computed custom hook
paths still need manual inspection.
doctor --config /path/to/config.json reports a missing hook. To repair it without starting
a second bridge, use python3 -m agent2telegram claude-hook --config /path/to/config.json,
then resume the same Claude session to load it. Registration never restarts your agent.
Until the bridge observes a Stop marker, 90 seconds without transcript activity suppresses
busy acknowledgements and typing. This only changes the indicator: the reply window and
cross-chat ownership guard stay in place, so a late answer retains its original destination.
You get the same live UX both ways: progress messages kept, a one-line italic status bubble for
tool calls that trails at the bottom and clears at the end, and a typing… indicator that stays
lit for the whole turn.
Running several agents (e.g. one per tmux session)? Give each its own bot in one step:
python3 -m agent2telegram connect # pick a session → pick provider → paste tokenconnect writes a separate config (~/.config/agent2telegram/<name>.json), starts that
bridge alongside the others, and — for Claude Code — pins the session id so the Stop hook routes
each turn-end to the right bridge. Your existing bots are left untouched. Name it explicitly with
--name.
python3 -m agent2telegram update # git pull + restart every running bridge on the new code
python3 -m agent2telegram update --no-restart # code only; restart with your existing supervisorPulls the latest code into ~/.agent2telegram-src and restarts each running bridge, preserving
its own --config — no re-setup, no re-entering tokens.
The restart plan is checked before pulling changes. Linux restarts preserve each process's
original environment. On macOS, a bridge owned by the com.agent2telegram.bridge launchd
service restarts through launchctl kickstart -k gui/<uid>/com.agent2telegram.bridge (the PID
must match). A standalone bridge can also restart from an absolute --config path when the
file contains its token and explicit state_dir; unrelated bot overrides are not inherited.
When the original environment cannot be recovered (for example an older, environment-only
configuration on macOS), the source update succeeds with a WARNING and exit code 0.
That bridge stays running on its previous code. The warning identifies its PID and prints
a shell-quoted start command requiring the original config/state paths. Restart it with its
original supervisor, or stop the standalone bridge and use that command in its original
environment. Never guess a default state path: it can replay messages. For custom supervisors,
prefer --no-restart. Agent/tmux sessions are never restarted.
After a successful pull, missing bot usernames in saved configurations are backfilled with
Telegram getMe before restart, including with --no-restart; existing usernames are kept.
Local source changes or failed restart preflight stop the update before pulling. A failed pull
preserves the checkout; a failed restart after a successful update is reported as an error.
Source-archive installations update by re-running install.sh. Older installations without
an archive marker are recognised at the standard ~/.agent2telegram-src location; the old
source is kept in a neighbouring .backup.* directory, including local files. Config and
state are outside that source tree and remain unchanged. Review/remove old backups yourself.
For unattended installation, use bash install.sh --no-setup; --no-pip installs only the
stdlib launcher. A source clone is installed from its current checkout; pull updates explicitly.
If you already have Codex (or Claude Code) installed and logged in, you can let it do
the whole install and fix any environment hiccups itself. The repo ships an
AGENTS.md playbook the agent follows step by step.
Three steps:
-
Install the agent CLI and log in (the one genuinely manual part on a clean machine):
- Codex — https://github.com/openai/codex, then run
codexonce to sign in. - or Claude Code — https://docs.claude.com/claude-code, then run
claudeonce.
- Codex — https://github.com/openai/codex, then run
-
Have a Telegram bot token ready (create a bot with @BotFather).
-
Paste this prompt into your agent:
Install Agent2Telegram from
https://github.com/petrludwig-collab/Agent2Telegramby following itsAGENTS.mdexactly. Connect it to me on Telegram. Ask me for the bot token and my Telegram user id when you need them. Do not weaken the security rules. When finished, verify withpython3 -m agent2telegram doctorand confirm I get a reply from the bot.
The agent reads AGENTS.md, checks prerequisites, installs, configures, verifies with
doctor, and sets up auto‑start. Because the package is dependency‑free and self‑diagnosing,
the agent has an easy, checkable job — and can repair the rare clean‑server quirk on its own.
Prefer a deterministic install with no agent? Use the Quick start above or the one‑liner.
- Python 3.10+
- The agent you want to connect, installed and logged in:
- Claude Code — https://docs.claude.com/claude-code (run
claudeonce to sign in) - Codex — https://github.com/openai/codex (run
codexonce to sign in)
- Claude Code — https://docs.claude.com/claude-code (run
The bridge shells out to these tools, so whatever they can do in your terminal, they can do from Telegram.
| Command | What it does |
|---|---|
| (any text) | sent straight to the live agent |
/start, /help |
short intro + what you can send |
/status |
which agent + tmux session you're connected to (and whether voice is on) |
/setkey <key> |
enable voice transcription with your ElevenLabs key — your message is deleted right after so the key isn't left in the chat |
/voice |
toggle spoken replies — the agent's answer comes back as a voice note (needs an ElevenLabs key) |
/id |
show your user / chat id (handy for the allow‑list) |
Anything that isn't one of these (including other /commands) is passed through to the agent.
Run the whole attach experience against a real agent with a fake Telegram — it needs no bot token and touches nothing live. It spins up a throwaway tmux session, launches the agent, and checks text round-trip, a ❤️ reaction, a multi-step task (tool-call status bubble), and voice transcription:
python3 -m agent2telegram selftest --agent codex
python3 -m agent2telegram selftest --agent claude-codeBoth report 6/6 checks passed on a working setup.
One line — the exact mirror of the install one‑liner:
curl -fsSL https://raw.githubusercontent.com/petrludwig-collab/Agent2Telegram/main/uninstall.sh | bashStop any custom keepalive/supervisor first. This stops the current user's bridges and removes
the default config (token included), state, installed source, shell launcher, Claude Stop hook,
and pip package. Agent tmux sessions, other users, custom source/state paths and archive backups
are left untouched. (If you prefer, the equivalent from a clone is python3 -m agent2telegram uninstall; add --yes to skip the confirmation prompt.)
The bridge forwards the agent's replies during a Telegram‑originated turn. A cron job, a background task, or a long build can't reach you that way — there's no turn to reply in. For those, push a message to yourself with:
python3 -m agent2telegram notify "build finished ✅"
echo "deploy done" | python3 -m agent2telegram notify # or pipe it on stdin
python3 -m agent2telegram notify --config ~/.config/agent2telegram/codex-config.json "…"It sends to the configured owner via the bot (Markdown rendered, same as chat replies). Handy
in a cron line or at the end of a script so the agent tells you when something finishes.
Voice notes are transcribed with ElevenLabs Scribe (scribe_v1) and the transcript is
sent to the agent. It's off by default and uses your own API key — there is no shared
key and no extra Python dependency.
Enable it by setting your key (get one at https://elevenlabs.io):
export ELEVENLABS_API_KEY="sk_..." # or put "elevenlabs_api_key" in config.jsonWithout a key, voice messages get a short "not enabled" notice. Images and files work with no extra setup.
Stored at ~/.config/agent2telegram/config.json (mode 0600). The token may instead be
provided via the TELEGRAM_BOT_TOKEN environment variable to keep it out of the file.
{
"agent": "claude-code",
"token": "123456:ABC...",
"allowed_user_ids": [123456789],
"agent_timeout": 600,
"command": null,
"continue_command": null
}Custom command — the default invocation for each agent is overridable, because these CLIs
evolve. Use {prompt} where the message should go:
{
"agent": "codex",
"command": ["codex", "exec", "--model", "gpt-5.5", "{prompt}"],
"continue_command": ["codex", "exec", "--last", "{prompt}"]
}Run python3 -m agent2telegram doctor to validate everything (config, token, agent binary).
For groups, optional "require_mention": true together with "bot_username": "your_bot"
accepts only @mentions, commands addressed to that bot, and replies to its messages.
Other group messages are silently ignored, including receipt acknowledgments. Private chats
are unchanged. The default is false; the authorized-user allowlist is always enforced.
In attach mode, optional "inbound_sender_label": true adds sender/chat context immediately
after origin_prefix: [From: Alex · DM] or [From: Alex · group "Team"].
The default language is English. With explicit language: "cs", the original Czech
[Od: ...] prefix is preserved, including reactions, voice notes, and attachments.
Names come from the current Telegram sender, including voice messages, attachments and reactions;
reply quotes do not replace the sender. Display names (64 characters) and group titles (80)
are sanitized to one line without brackets, quotes or control characters.
These user-controlled descriptions are not proof of identity or approval: authorization still
uses Telegram user IDs. Routing is unchanged, and notify still targets the owner's DM.
The default is false, preserving existing prompts.
Operational notices: "language": "cs" translates the busy acknowledgment and the default
startup notice into Czech ("en" is the default; this does not configure speech recognition).
"restart_notice": true enables one short notice after successful bridge startup checks.
Attach mode sends it to its owner chat; a group adapter can supply its configured group.
One-shot mode sends it to the first authorized owner. "restart_notice_text" optionally
overrides the text with a literal single line, maximum 200 characters.
The default is off. A host-state marker enforces a 30-second cooldown across process restarts,
including failed/uncertain sends. The notice is not retried in a background loop; an uncertain
Telegram acknowledgement can mean no visible notice. Keep a distinct state directory for each bot.
New setup/connect configurations store an absolute state_dir; connect gives each named
bridge its own directory. For existing configurations, the old shared default remains unchanged
unless you explicitly set state_dir or AGENT2TELEGRAM_STATE (the environment wins). Preserve
the existing directory and offset when upgrading; do not choose a new empty path for a live bot.
Background progress (attach mode, Claude Code): "background_progress": true shows work that
runs outside a Telegram turn. Running sub-agents (Claude Code writes them to
<session>/subagents/) get ONE message, edited in place with each sub-agent's description,
elapsed time and latest tool call, and deleted when they finish or go silent. Turns not started
from Telegram (e.g. a background-task notification) get the usual one-line tool status bubble,
deleted at that turn's end. Only one short line per step is sent, never tool output, and nothing
is shown for history replayed at startup. "background_edit_interval" (seconds, default 10)
throttles edits; "background_stale_after" (default 300) drops progress that went silent. The
labels follow "language". The default is off.
# Prints a systemd unit (Linux) or launchd plist (macOS) to stdout, hints to stderr:
python3 -m agent2telegram serviceFollow the printed steps. On Linux you’ll typically:
mkdir -p ~/.config/systemd/user
python3 -m agent2telegram service > ~/.config/systemd/user/agent2telegram.service
systemctl --user enable --now agent2telegram
loginctl enable-linger "$USER"The image is tiny, but the agent CLI and its login are not baked in (auth must stay out of images). Mount an authenticated agent and your config:
docker build -t agent2telegram .
docker run -d --name agent2telegram \
-v "$HOME/.config/agent2telegram:/data" \
-v "$HOME/.claude:/root/.claude" \ # example: bring your Claude Code login
agent2telegram- Allow‑list is the only thing between a stranger and code execution on your box. Keep it tight. An unauthorized user gets a refusal and their own id (so you can add them on purpose).
- The bot token is a secret: the config file is
0600, the token is never logged, and/status/doctoralways print it redacted. - Prompts are passed to the agent as a single
argvelement (never through a shell), so a message can’t inject shell syntax. - The wizard launches the agent with its "run without asking" flag (Codex
--dangerously-bypass-approvals-and-sandbox, Claude Code--dangerously-skip-permissions) — the bridge drives it unattended, so it can't stop to ask you to approve each command. That makes the allow‑list and a least‑privileged user the real safeguards. If you'd rather approve every action by hand, start the agent yourself without the flag and point the wizard at that tmux session instead of creating a new one. - Consider running the agent under a dedicated, least‑privileged user.
python3 -m unittest discover -s tests -v # zero-dependency test suiteMIT — see LICENSE.
If your agent has its own file-sending tool, just use it. Claude Code's SendUserFile is
recognised automatically: the bridge sees the call in the transcript and delivers the files. No
marker, no configuration, nothing to remember — it works on the first try.
The harness runs that tool itself, so the bridge never sees a call, only its record in the transcript. Before this was recognised the file silently vanished: the reply arrived, the attachment did not, and nothing in the log said so.
There is also an explicit marker, which works with any agent — put a line like this in the reply:
[tg-file] /Users/me/.local/state/agent2telegram/outbox/clip.mp4
The bridge removes that line from the message, sends the text, then uploads the file. The API method follows the extension, so video arrives playable, audio as a track and images as photos; anything else goes as a document. Bots can upload at most 50 MB — larger files fail immediately with a clear message instead of a long doomed upload.
The path is checked against an allowlist, and that is a security boundary. The path
comes from the agent's own reply text, so a wide allowlist would turn a prompt injection
into file exfiltration. By default only ~/.local/state/agent2telegram/outbox is allowed;
symlinks are resolved before the check, so a link inside the outbox pointing at
~/.ssh/id_rsa is refused. Add more folders explicitly if you need them:
{ "outbox_dirs": ["/Users/me/renders"] }A refused attachment is always reported back into the chat — silently dropping a file would be worse than a visible error.
From cron or a background job (where there is no Telegram turn to reply in) use
notify, which enforces the same allowlist:
python -m agent2telegram notify --file ~/.local/state/agent2telegram/outbox/clip.mp4 \
"render finished"