____
___/ \__
__ __ __ __ __ __ __ / o \
(o> (o> (o> (o> (o> (o> (o> \_ >
~~ ~~ ~~ ~~ ~~ ~~ ~~ \_______/
|| ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Drive Claude Code from Telegram. Spawn Claude sessions, watch plans evolve, answer questions with one tap β all from your phone.
English Β· δΈζ
npm i -g duckling-cli # install
duckling setup # pair with @DucklingCli_Bot via QR
duckling start # run daemon β chat with the botPublished on npm as duckling-cli. Requires Node 18.17+ and a working claude install (uses your existing OAuth β no extra API key).
duckling is a tiny daemon that runs the official Claude Agent SDK on your machine and bridges it to Telegram. You send a prompt from your phone β Claude works on your computer β results, plans, and questions stream back into the chat with tap-to-answer buttons.
There's nothing to host. We run one shared bot (@DucklingCli_Bot) and one Cloudflare Worker relay. You install one npm package, scan a QR code, you're done.
Claude Code is great when you're at your desk. The problem starts when you're not:
- π On the train, watching a long task tick through a TODO list.
- ποΈ In bed, and Claude paused on
AskUserQuestionwaiting for a one-tap answer. - π Out for a run, and you want to kick off a refactor before you forget.
duckling solves exactly this. It does not replace your terminal. SSH is still better for typing code. duckling is for ambient access β "is it done yet", "approve this", "kill the bad branch."
There are two ways to use duckling. Pick one.
You pair with our hosted bot (@DucklingCli_Bot) and our Cloudflare Worker. Zero infra setup on your side.
npm i -g duckling-cli # one-time install
duckling setup # one-time pairing (QR + Telegram tap)
cd /path/to/your/project # β run duckling start from here
duckling start # daemon picks up THIS dir as its cwdWhat happens:
npm i -g duckling-cliinstalls the CLI globally.duckling setupprints a QR + ahttps://t.me/DucklingCli_Bot?start=β¦link. Scan/click β tap Start in Telegram β paired. Config writes to~/.config/duckling/config.json.cdinto the project you want the daemon to work in. This matters: the daemon's working directory is what you'vecd'd into when you randuckling start. The Agent SDK spawns Claude sessions in that directory, and/new's picker shows the same session pool your terminalclaudehas been using in that directory (~/.claude/projects/<encoded-cwd>/).duckling startbrings up the daemon, which connects to our shared relay. It prints the cwd it locked onto, so you can sanity-check.
You did not need a Cloudflare account, a Telegram bot, or any deploy step. Inference still runs on your own Claude OAuth locally β the relay only forwards control events, not model traffic.
To switch projects: duckling stop && cd /other/project && duckling start.
If you'd rather not depend on the shared relay (private team bot, paranoid about an intermediary, etc.), you self-host. The CLI is identical; only the maintainer (you) does an extra one-time Cloudflare deploy.
Maintainer, once:
git clone https://github.com/scao7/duckling-cli.git
cd duckling-cli
npm install
# Follow DEPLOY.md β 5 wrangler commands:
# wrangler login
# wrangler secret put TELEGRAM_BOT_TOKEN # from BotFather
# wrangler secret put TG_WEBHOOK_SECRET # random string
# wrangler deploy # prints your relay URL
# curl β¦ setWebhook # point Telegram at the workerEach user (anyone pointed at your fork), once:
npm i -g duckling-cli
export DUCKLING_RELAY_URL=https://your-relay.your-subdomain.workers.dev
duckling setup
duckling startDUCKLING_RELAY_URL redirects pairing + the daemon's WebSocket to your Worker instead of ours. After pairing, the URL is baked into ~/.config/duckling/config.json β no need to keep exporting it.
Full recipe + cost calculator (spoiler: $0 on Cloudflare's free tier for small teams) in DEPLOY.md.
- Node 18.17+
- A working
claudeinstall, logged in. The Agent SDK uses your existing Claude OAuth β no separate API key, no extra cost. Ifclaude --versionworks on your machine, you're set.
Two ways to start a task:
One-shot: type the whole task with the command.
You: /new refactor the auth middleware to use the new token format
Bot: π refactor-auth-middleware β appears + edits in place
β¬ Read existing middleware
β¬ Adapt to new token shape
β¬ Update tests
β¬ Run lint + tests
Bot: β refactor-auth-middleware Β· Token source
Should I read tokens from headers only, or also cookies?
[ headers only ] [ headers + cookies ] β tap one
β¦ β silent while it works
Bot: β
refactor-auth-middleware Β· completed Β· 4m12s Β· $0.0341
Tap-only: just /new (no args). Bot shows a picker of sessions you already have on this machine β pick to resume, or tap "β ζ°εε·₯" for a blank one:
You: /new
Bot: π¦ ζ΄Ύζ΄»η»θ°?ιδΈδΈͺζ§δ»»ε‘ζ₯ηεΉ²,ζθ
ηΉ ζ°εε·₯ εΌδΈͺε
¨ζ°ηγ
[ refactor auth middleware Β· 12m ago ]
[ write a quicksort Β· 1h ago ]
[ debug the login flow Β· yesterday]
[ β ζ°εε·₯ ] β tap = "εε·₯δΈε·" spawns
The resumable rows come straight from ~/.claude/projects/<your-cwd>/*.jsonl β exactly the sessions your terminal claude has been working in. Pick one and it forks (won't pollute the original transcript); tap "ζ°εε·₯" and a blank session named εε·₯δΈε· (εε·₯δΊε·, etc.) is spawned, waiting for your next message to use as its first task.
Four message types ever: plan, question, done, error. No "I'm now reading file X", no "Running npm test...", no anchor banners. The chat is for milestones, not chit-chat.
| Command | What it does |
|---|---|
/new |
No args β picker (resume an existing session or spawn ζ°εε·₯). With args (/new <task>) β spawn + start on that task right away. |
/kill |
Stop a task. No args β picker of running sessions; with args (/kill <name>) β kill that one directly. |
/help |
Three-line cheatsheet |
That's the whole / menu. Three commands. Anything more would mean adding "things to fiddle with on your phone" β which is exactly what this tool refuses to do.
- Direct message β once a session is current, anything you type without a leading
/continues that task ("look, also handle the case where the token is empty"). If you used/newto spawn a blank εε·₯ with no task yet, your next message becomes its first task. - One-tap decisions β when Claude calls
AskUserQuestion, options become inline buttons. No typing required. - Picker-everywhere β both
/newand/killpop tappable lists when used without args. You never have to memorise session names or IDs.
- Production happens at the desk. This tool serves the moments you're not at the desk. Nothing else.
- Make the user do less, not more. Pickers > typing. Defaults > config. Auto > manual.
- No feature that pulls the user back to the phone. If it tempts you to "just check in", it's wrong.
- Fewer messages is better. Only plan, approve, done, error, plus questions Claude genuinely cannot answer without you.
- Subordinate, not chat buddy. An employee doesn't IM their boss play-by-play; they report at milestones.
- Test before adding a feature: does this let the user leave their computer, or force them to stare at the phone? Stare β no.
- North star: send a task before bed, wake up to one "done" message.
If you fork and find yourself wanting to add notifications, dashboards, "live tail" mode, status bars, anything that buzzes more than once per task β re-read the seven rules. duckling is built around them.
Your phone Cloudflare Worker Your computer
ββββββββββββββββ βββββββββββββββββ ββββββββββββββββββββ
β Telegram ββββ Bot ββββΆβ duckling-relayββββ WS ββββΆβ duckling daemon β
β @Ducklingβ¦ β β + DOs β β β Agent SDK β
β β β β β β session 1 β
β β β β β β session 2 β
ββββββββββββββββ βββββββββββββββββ ββββββββββββββββββββ
β OAuth
βΌ
ββββββββββββββββββββ
β Claude (your sub)β
ββββββββββββββββββββ
- The daemon runs the SDK in-process. One
Sessionper/new. Each Session has its own input stream β turns come from you (TG), responses stream out as discrete events. - The relay is a Cloudflare Worker that fans out per-user Durable Objects. It owns the Telegram webhook, holds your hibernated WebSocket, and renders SDK events into TG messages.
- Anthropic SDK calls never touch the relay. Inference goes from your machine straight to Claude using your own OAuth subscription. The relay is a control-plane only.
- Your code never leaves your machine unless Claude itself decides to read or write it, in which case the file path / preview travels through the relay as part of a tool_use event. Tool outputs (e.g. test results, file contents) don't.
- The relay forwards and forgets. Durable Object state is limited to pairing tokens, device records, the latest sessions snapshot, and short-lived question contexts. No code, no transcripts.
- Self-host if you don't trust the shared relay. The Worker is the whole stack β
npx wrangler deployand you own the data plane. See DEPLOY.md. - Auth on the daemon side is a deviceToken, opaque to you, revocable from the relay. It's the only secret on your machine.
The default points at the shared relay we operate. If you'd rather run your own:
# One-time, as the maintainer of your own bot
git clone https://github.com/scao7/duckling-cli.git
cd duckling-cli
npm install
# follow DEPLOY.md β five commands: wrangler login + secret put + deploy + setWebhookUsers of your fork override the default with:
export DUCKLING_RELAY_URL=https://your-relay.workers.dev
duckling setupFull recipe + cost calculator (spoiler: $0 on Cloudflare's free tier for small teams) in DEPLOY.md.
| Layer | What | Code |
|---|---|---|
| CLI | duckling setup|start|stop|status |
src/cli/ |
| Daemon | SDK runner, session manager, WS client | src/daemon/ |
| Worker | TG webhook, pairing, fan-out to UserDO | src/worker/ |
| Shared | Wire protocol (DaemonToRelay / RelayToDaemon) |
src/shared/protocol.ts |
The codebase is small (~1500 LoC of TS) and self-contained β no framework on either side, just ws + commander + Cloudflare Durable Objects.
See CLAUDE.md for the design doc: architecture decisions, wire protocol, and why duckling is "just a relay" β no approval gates, no second permission system.
git clone https://github.com/scao7/duckling-cli.git
cd duckling-cli
npm install
npm run build # tsc + worker typecheck
# CLI:
node dist/cli/index.js setup
node dist/cli/index.js start
# Worker (local dev, no TG webhook β useful for /pair/* and /healthz):
npm run worker:dev
# Deploy after changes:
npm run worker:deployThings on the table, not yet shipped:
duckling attachβ hand aclaudesession you started in an SSH terminal off to the bot, so the chat picks up that conversation's history.- Multi-user on a single machine β currently one daemon per Linux user (separate
~/.config/duckling/). - Reply-to-route β let TG message-replies target a specific session without
/switch. - Auto-archive β old sessions linger forever; add a TTL.
- Diff rendering β
Edit/Writepreviews as code blocks (short) or images (long).
PRs welcome. Open an issue first if it's bigger than a quick fix.
When sending a PR:
- Run
npm run build(must pass β strict TypeScript on both sides). - If the change touches the wire protocol, update both
src/shared/protocol.tsand any handlers insrc/daemon/andsrc/worker/.
MIT β fork freely. The shared bot/relay is a convenience, not a moat.
Built on top of @anthropic-ai/claude-agent-sdk. Inspired by the pattern in openclaw-claude-code-plugin of using the SDK's streaming-input mode for multi-turn.