Your Claude Code sessions can page you

· Desktop bridge 8 min read

A coding agent that runs for twenty minutes has a failure mode nobody designed for: it finishes, or it stalls on a question, and you are in another room. The work is done and sitting there, or it is not done and sitting there, and you cannot tell which without walking back to the machine.

Telechat's Desktop bridge exists for that gap. It hooks into Claude Code on your own machine and turns each of those moments into a card on Telegram — one you can read at a glance and reply to, with the reply landing back in the same session. This post is how it works, including the parts that were not obvious until they broke.

The hook surface

Claude Code fires hooks at defined points in a session and reads the hook's stdout for a structured response. Telechat registers four of them in ~/.claude/settings.json:

$ telechat bridge install --approval

 Registered hooks in ~/.claude/settings.json
    Stop         → telechat bridge notify Stop
    Notification → telechat bridge notify Notification
    SubagentStop → telechat bridge notify SubagentStop
    PreToolUse   → telechat bridge approve

Three of them are notifications: the session finished a turn, the session wants your attention, a subagent finished. The fourth is different in kind — PreToolUse runs before a tool call and can block it, which is what makes phone-side approval possible at all.

There is no daemon in this picture. The hooks are short-lived subprocesses; the already-running Telechat Telegram poller dispatches your replies and button taps back into the same module. If you already run Telechat for chat, the bridge costs you no additional process.

A wall of text is not a notification

The naive version posts the last assistant message to Telegram. We built that first. It is useless on a phone: the last message of a long agent turn is routinely two thousand characters of summary, and the one sentence that matters — the question it is waiting on — is somewhere in the middle of it.

So every notification is run through a fast model (Haiku) that produces a triage card instead:

💬 Reply from apprend-backend [b2ca0347]

⚠️ NEEDS DECISION
Migrated password hashing to argon2id, consolidated token
paths, added refresh rotation. All 31 tests pass, staging
verified.

⚠️ NEEDS YOU: Migrate OAuth1 to the new flow or drop legacy
support?

[📄 Full output]  [💬 Use session]

Three things about this shape earned their place:

That last rule is worth stating as a principle: a summariser in a notification path must degrade to the raw thing, never to silence. A lossy notification you cannot expand is worse than no notification, because you will trust it.

The recursion that ate itself

Here is the bug that only exists because of how the pieces fit together.

The digest is produced by running claude. Claude Code fires the Stop hook when a session ends a turn. The digest's own session ends a turn. So the digest fires Stop, which calls telechat bridge notify Stop, which produces a digest, which fires Stop.

The fix is four lines and an environment variable. The digest subprocess is spawned with TELECHAT_BRIDGE_INTERNAL=1, and both hook entry points check for it first:

def cli_notify(event: str) -> int:
    # Recursion guard: the AI digest itself runs `claude`, whose Stop
    # hook calls us again. The digest subprocess carries
    # TELECHAT_BRIDGE_INTERNAL=1 → swallow that notification.
    if os.environ.get("TELECHAT_BRIDGE_INTERNAL"):
        return 0
    ...

The same guard sits in the approval path, for a sharper reason: without it, the digest's own tool calls would prompt you for approval — the machinery that tells you about a decision would start asking permission to tell you.

The general shape here is one to watch for. Any time a tool observes an event class and then performs an action in that same class, it can observe itself. Environment-variable tainting of the child process is the cheapest reliable break, because it survives whatever subprocess layering sits in between.

Approving tool calls from your phone

With --approval, the PreToolUse hook intercepts every Bash, Write and Edit call, posts a card with Approve and Deny buttons, and blocks until you tap. Claude Code respects the returned decision.

Blocking raises the obvious question: what happens when nobody taps? The honest answer for a personal tool is that you are usually sitting at the machine, so the historical behaviour — hand the decision back to Claude Code's normal permission flow, which prompts at the desktop — is right most of the time.

But it is exactly wrong for the person who turned approval on because they are away. So the timeout became a policy instead of an accident:

BRIDGE_APPROVAL_TIMEOUT=300                 # seconds to wait for a tap
BRIDGE_APPROVAL_TIMEOUT_ACTION=fallthrough  # fallthrough | deny | allow

Two details matter more than the options themselves. First, the card states which one it will do — "Auto-denies in 5 min" — so the policy is visible at the exact moment you would act on it, rather than living in a config file you last read in March. Second, an unrecognised value means fallthrough: a typo cannot silently become a security posture.

Approval is off by default, per project. You opt in by replying /desktop_approve_on to a session card. A global "approve everything from my phone" default would be the wrong shape for a tool that can run rm.

Replying into a live session

A reply is injected with claude --resume against that session id. Headless resume needs a long-lived OAuth token — created with claude setup-token and stored as CLAUDE_CODE_OAUTH_TOKEN — which is the single most common reason a first bridge install half-works. Replies fail with a 401 and nothing else looks wrong, so telechat bridge install checks for it in preflight alongside the Claude CLI and your Telegram credentials, and warns before you discover it at the worst moment.

From Telegram you get session control that mirrors what you would do at the keyboard:

/desktop              # list running sessions, tap to select
/desktop_use b2ca0347 # switch by 8-char short id
/desktop_all <msg>    # broadcast to every running session

Reply to a specific card and the message goes to that session. Pick a session once and plain text goes there without the reply gesture — which is the difference between a tool you use at a desk and one you use walking.

What it does not do

Being direct about the edges, because finding them yourself is worse:

Try it

If you already run Claude Code, the bridge is one command on top of a Telechat install, and it removes hooks as cleanly as it adds them with telechat bridge uninstall.

Get the bridge

Install Telechat, then hook up Claude Code.

npm install -g telechat && telechat init

Bridge documentation · What Telechat is