app.py is the primary entrypoint for this repository. It runs a Telegram bot gateway that can send and receive messages, watch updates, and delegate replies to local agent CLIs such as Claude Code and Codex CLI.
telegram_claude_gateway.sh is still included, but it is now a secondary legacy PoC. It remains useful for quick Claude-only experiments and for comparing the newer Python path against the original shell prototype, but it is not the main interface and is not kept at feature parity with app.py.
- Sends plain text messages to a Telegram chat
- Receives Telegram updates through long polling
- Watches new messages continuously
- Auto-replies with a simple built-in acknowledgement mode
- Auto-replies with Claude Code through
app.py - Auto-replies with Codex CLI through
app.py - Responds to a heartbeat keyword without calling a provider CLI
Primary runtime:
python3.6+- A Telegram bot token and target chat ID
claudeCLI if you want to use Claude-backed commandscodexCLI if you want to use Codex-backed commands
Legacy PoC only:
bashcurljqfor formatted shell output
app.py itself depends only on the Python standard library plus whichever external provider CLIs you choose to use.
Copy .env.example to .env, then set at least:
TELEGRAM_BOT_TOKEN=your_bot_token
TELEGRAM_CHAT_ID=your_chat_idRun the Python gateway:
python3 app.py watch-codex-replyor:
python3 app.py watch-claude-replyAll variables below are described for app.py unless explicitly noted otherwise.
Shared Telegram and reply settings:
ENV_FILE: path to the env file. Default:./.envnext toapp.py.TELEGRAM_BOT_TOKEN: Telegram bot token. Required.TELEGRAM_CHAT_ID: default Telegram chat ID. Required for send commands.TELEGRAM_MAX_MESSAGE_LENGTH: max Telegram message length before truncation. Default:3500.WATCH_POLL_TIMEOUT: TelegramgetUpdateslong-poll seconds for watch commands. Lower values makeCtrl+Cmore responsive on terminals that do not interrupt blocking network calls promptly. Default:10.HEARTBEAT_KEYWORD: direct reply keyword that skips provider execution. Default:ping.HEARTBEAT_RESPONSE: direct reply text for the heartbeat keyword. Default:pong.RAW_OUTPUT=1: print raw Telegram JSON instead of formatted output.
Claude Code settings for app.py:
CLAUDE_EXECUTABLE: Claude executable name. Default:claude.CLAUDE_SETTINGS_PATH: Claude settings file path. Default:~/.claude/settings.json.CLAUDE_WORKDIR: Claude working root.app.pyuses it as processcwdand also passes it via--add-dir. Default: repository root.CLAUDE_NO_SESSION_PERSISTENCE: set to1to append--no-session-persistence. Default:0.CLAUDE_PENDING_MESSAGE: placeholder reply sent before Claude finishes. Default:[CLAUDE CODE] Processing your request....
Codex CLI settings for app.py:
CODEX_EXECUTABLE: Codex executable name. Default:codex.CODEX_MODEL: Codex model name. Default:gpt-5.3-codex.CODEX_REASONING_EFFORT: Codex reasoning effort. Default:high.CODEX_WORKDIR: working root passed tocodex exec -C. Default: repository root.CODEX_PENDING_MESSAGE: placeholder reply sent before Codex finishes. Default:[CODEX CLI] Processing your request....
app.py uses a dotenv-style parser, not shell source semantics:
- environment variables from the parent process override values from
.env ${VAR}and${VAR:-default}are supported- bare
$VARis treated literally - single-quoted values stay literal and are not interpolated
- the file is configuration only; shell execution syntax is not supported
Legacy shell PoC support:
telegram_claude_gateway.shis Claude-only- it supports the older shared subset such as
ENV_FILE,TELEGRAM_BOT_TOKEN,TELEGRAM_CHAT_ID,RAW_OUTPUT,CLAUDE_SETTINGS_PATH,TELEGRAM_MAX_MESSAGE_LENGTH,CLAUDE_PENDING_MESSAGE,HEARTBEAT_KEYWORD, andHEARTBEAT_RESPONSE - it does not support
CLAUDE_EXECUTABLE,CLAUDE_WORKDIR, or anyCODEX_*variables
python3 app.py send "hello"
python3 app.py claude-send "hello"
python3 app.py codex-send "hello"
python3 app.py receive
python3 app.py watch
python3 app.py watch-new
python3 app.py watch-reply
python3 app.py watch-claude-reply
python3 app.py watch-codex-reply
python3 app.py webhook-info
python3 app.py delete-webhookwatch-reply watches incoming text messages and sends a lightweight acknowledgement reply:
Rcvd msg from <sender>
watch-claude-reply skips existing pending updates, then only handles new incoming text messages:
- If the normalized message matches
HEARTBEAT_KEYWORD, the bot replies withHEARTBEAT_RESPONSE - Otherwise the bot immediately sends
CLAUDE_PENDING_MESSAGE - Then it runs Claude and sends the final response as a second message
app.py runs Claude with:
claude -p "$prompt" \
--settings "$CLAUDE_SETTINGS_PATH" \
--permission-mode bypassPermissions \
--dangerously-skip-permissions \
--add-dir "$CLAUDE_WORKDIR"It also starts the Claude process with cwd="$CLAUDE_WORKDIR".
If CLAUDE_NO_SESSION_PERSISTENCE=1, app.py also appends:
--no-session-persistencewatch-codex-reply skips existing pending updates, then only handles new incoming text messages:
- If the normalized message matches
HEARTBEAT_KEYWORD, the bot replies withHEARTBEAT_RESPONSE - Otherwise the bot immediately sends
CODEX_PENDING_MESSAGE - Then it runs Codex and sends the final response as a second message
app.py runs Codex with:
codex exec \
--dangerously-bypass-approvals-and-sandbox \
--ephemeral \
--skip-git-repo-check \
-m "$CODEX_MODEL" \
-c "model_reasoning_effort=\"$CODEX_REASONING_EFFORT\"" \
-C "$CODEX_WORKDIR" \
-- "$prompt"The -- separator is intentional. It ensures prompts that start with -, such as --help or --fix, are passed to Codex as user input instead of being parsed as extra CLI flags.
The original shell entrypoint is still available:
./telegram_claude_gateway.sh watch-claude-replyUse it when you specifically want the old shell-based Claude-only flow. It is not the recommended path for new work.
- Non-text messages are ignored in auto-reply modes
- Long Claude or Codex responses are truncated before sending to Telegram
- Terminal output shows both incoming messages and outgoing placeholder or provider replies
- On Windows/Git Bash, formatted incoming-message logs prefer
pythonorpython3overjqto avoid Chinese text garbling in the terminal - Telegram sends message text through stdin instead of shell arguments to avoid UTF-8 encoding errors with non-ASCII content