Skip to content

feat(channel): forward Claude Code permission prompts to Telegram as interactive buttons#76

Open
letrii wants to merge 5 commits intocodeaholicguy:mainfrom
letrii:feat/channel-permission-prompts
Open

feat(channel): forward Claude Code permission prompts to Telegram as interactive buttons#76
letrii wants to merge 5 commits intocodeaholicguy:mainfrom
letrii:feat/channel-permission-prompts

Conversation

@letrii
Copy link
Copy Markdown

@letrii letrii commented Apr 23, 2026

Problem

When Claude Code requires user approval (bash commands, file operations, etc.),
the agent blocks and waits for terminal input. Users controlling agents remotely
via the Telegram bridge had no way to approve or reject these prompts without
being at their terminal.

Solution

The bridge now detects permission prompts from the terminal output and forwards
them to Telegram as inline keyboard messages with Yes/No buttons. Tapping a
button sends the corresponding keystroke back to the agent terminal. After
responding, the keyboard message updates to show the confirmation result.

Changes

  • TtyWriter: add captureOutput() to read terminal content via tmux, iTerm2, and Terminal.app
  • TelegramAdapter: add sendKeyboard(), resolveKeyboard(), and onCallbackQuery() for inline keyboard support
  • ChannelAdapter: extend interface with new keyboard methods
  • channel: add startPermissionPolling() with prompt detection, keyboard tracking, and callback handler

Demo

  1. Bridge detects permission prompt → sends Telegram message with ✅ Yes / ❌ No buttons
  2. User taps a button → keystroke forwarded to terminal → message updates to "✅ Đã xác nhận: Yes"
  3. If user answers at terminal directly → message updates to "✅ Đã xử lý tại terminal"

Notes

  • Builds on top of fix/strip-system-tags-from-telegram-messages
  • Prompt detection uses terminal capture (tmux/iTerm2/Terminal.app) — same terminals supported by agent open

BrookLe added 5 commits April 23, 2026 15:36
… Telegram

Claude Code injects structured XML tags (command-name, command-message,
local-command-stdout, system-reminder, etc.) into the conversation that
are intended for the UI layer, not for end users. Without filtering,
these tags appear verbatim in Telegram messages.

Add stripSystemTags() to remove known system tags from agent output
before forwarding to Telegram.
Claude Code responses use Markdown formatting (bold, italic, code blocks,
inline code) which Telegram displays as raw text by default.

Add markdownToHtml() converter in TelegramAdapter that transforms standard
Markdown to Telegram HTML, and send messages with parse_mode: 'HTML'.
Code blocks are extracted first to prevent formatting replacements inside them.
…interactive buttons

When Claude Code requires user approval (bash commands, file operations,
etc.), the bridge detects the permission prompt from the terminal output
and sends it to Telegram as an inline keyboard with Yes/No buttons.
Tapping a button forwards the corresponding keystroke back to the agent terminal.

Changes:
- TtyWriter: add captureOutput() to read terminal content via tmux/iTerm2/Terminal.app
- TelegramAdapter: add sendKeyboard() and onCallbackQuery() for inline keyboard support
- ChannelAdapter: extend interface with sendKeyboard() and onCallbackQuery()
- channel: add startPermissionPolling() with prompt detection and callback handler
@codeaholicguy
Copy link
Copy Markdown
Owner

@letrii Thanks for the contribution!

The permission forwarding to Telegram is a really useful addition for remote control. I have a suggestion for an alternative detection approach that might be more robust.

As I understand, the current approach is terminal screen-scraping. The PR captures terminal output via tmux capture-pane / iTerm2 contents of session and uses regex patterns (/Allow\s+.+?/i, /›\s*(Yes|No)/, etc.) to detect permission prompts. This works but has some concerns:

  • If Claude Code changes prompt wording, locale, or TUI rendering, the regex patterns break silently
  • Patterns like Allow\s+.+? could match assistant conversation text (e.g., "Allow me to explain...")
  • Spawns an osascript or tmux process every poll cycle just to read the screen
  • We only get raw text, not the tool name, input, or structured question options

Let's learn more deeply about the JSONL session file.

Claude Code writes structured entries to ~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl. When the agent is waiting for tool approval, the last entry looks like:

{
  "type": "assistant",
  "message": {
    "content": [{ "type": "tool_use", "name": "Bash", "input": { "command": "npm install" } }],
    "stop_reason": "tool_use"
  }
}

And for AskUserQuestion:

{
  "type": "assistant",
  "message": {
    "content": [{ "type": "tool_use", "name": "AskUserQuestion", "input": { "questions": [...] } }],
    "stop_reason": "tool_use"
  }
}

So the detection logic becomes: if the last entry is type: "assistant" with stop_reason: "tool_use", the agent is waiting for approval/input. The tool_use.name tells us exactly what kind of prompt it is, and tool_use.input gives us the details.

Benefits:

  • stop_reason: "tool_use" is unambiguous, zero false positives
  • we get the tool name, command/file path, and for AskUserQuestion the full structured options with labels
  • just reading the session file (which startOutputPolling already does)
  • works without terminal emulator APIs
  • if we add the detection to the core AgentAdapter/AgentStatus layer, every consumer (CLI, Telegram, future channels) benefits for free

The ideal implementation would be:

  1. Add a new status to AgentStatus + pendingAction metadata to AgentInfo (in agent-manager)
  2. Detect it in ClaudeCodeAdapter.readSession() by checking the last assistant entry's stop_reason and tool_use blocks
  3. In the Telegram bridge, use this status to trigger the inline keyboard instead of screen-scraping

What do you think? Happy to discuss further!

@codeaholicguy
Copy link
Copy Markdown
Owner

When doing these changes, please also try dev-lifecycle skills, so that we can have all the artifacts (docs) for future referencing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants