A thin TypeScript Telegram bot that relays your messages into your locally installed Claude Code CLI (authenticated via your Claude Pro/Max login — no API key involved) so you can manage GitHub issues, GitHub Projects boards, Google Calendar events, and Teamwork timers/task assignment just by texting a bot. Nothing here is tied to one person's setup — every credential, name, and repo/project reference is read from your own config, so anyone can clone this and point it at their own accounts.
Licensed under PolyForm Noncommercial 1.0.0 — free to use, fork, and modify for personal, educational, nonprofit, or other non-commercial purposes. Commercial use (selling it, running it as part of a paid product/service, or using it inside a for-profit company's revenue-generating operations) requires the copyright holder's permission.
- Quick start
- How it works
- What's reachable from Telegram
- Prerequisites
- Verify the exact tool names before relying on this
- Setup
- Run
- Project layout
- Known limitations (v1)
If you already have Claude Code installed and logged in, and the three MCP servers set up:
npm install
cp .env.example .env
# edit .env: at minimum set TELEGRAM_BOT_TOKEN and ALLOWED_CHAT_IDS
npm run build
npm startMessage your bot on Telegram to confirm it replies, then read Setup below for the full walkthrough (group-chat mode, per-chat repo/project defaults) and MCP_SETUP.md for the one-time GitHub/Calendar/Teamwork credential setup this depends on.
- You send a message to the bot.
- The bot runs
claude -p "<your message>" --output-format json --model claude-haiku-4-5-20251001 --append-system-prompt "<headless context>" --allowedTools <26 allowed tools>(adding--resume <session_id>if this chat has talked to Claude before). - Claude Code either calls one of the allowed MCP tools or just replies conversationally.
- The bot sends Claude's
resulttext back to you on Telegram, and remembers thesession_idfor that chat so "actually make that 3pm" works as a follow-up.
See ALLOWED_TOOLS in src/claudeRunner.ts for the exact list:
- GitHub issues: create, edit/close/reopen, look up a specific issue, list, search (including "assigned to me"), and comment.
- GitHub Projects (project boards): list projects and their items, look up a project/item/field, add an issue or PR to a board, move an item between statuses, post a status update. Requires extra setup — see MCP_SETUP.md for the PAT scope and Docker toolset changes this needs.
- Google Calendar events: create, edit, cancel, list, search, check free/busy, and respond to invitations.
- Teamwork timers and task assignment: search for a project or task by name (no need to know numeric IDs), list timers, create a timer for a task, start/pause/complete a timer, look up a task, and assign yourself to a task. Requires extra setup — see MCP_SETUP.md's Teamwork section; there's no official Teamwork MCP server, so this project includes a small custom one (
teamwork-mcp-server/). Assignment is specifically built to only ever add you to a task's assignees, never replace the list — it can't accidentally bump a teammate off a task.
Everything else Claude Code can normally do (file edits, bash, browsing, pull
requests, other MCP tools) is locked out by --allowedTools.
Because this runs headlessly (no terminal, no browser), a tool call that
isn't on the allowlist is simply denied — there's no way to "approve" it from
Telegram after the fact. --append-system-prompt tells Claude this
explicitly, so instead of asking you to approve something that can never be
approved (a confusing loop hit during development), it just says what it
can't do. If you hit a capability gap, the fix is always to add the specific
tool it needs to ALLOWED_TOOLS in src/claudeRunner.ts, not to reply
"yes" to it.
-
macOS with Node.js 18+ (Linux should work too; not tested on Windows).
-
Claude Code installed and logged in with your Claude Pro/Max subscription (
claudeworks in your terminal without anyANTHROPIC_API_KEYset). -
Three MCP servers registered at user scope in Claude Code, named
github,google-calendar, andteamwork, each connected via a long-lived credential (not an interactive OAuth connector — see MCP_SETUP.md for why and exactly how to set these up). In short: this bot invokes Claude Code headlessly, so any MCP server that needs a browser popup or in-chat "grant permission" click to (re)authenticate will silently stall. Local, token-authenticated servers (a GitHub PAT via Docker; a cached Google OAuth refresh token; a Teamwork API key for the customteamwork-mcp-server/) avoid that entirely.Confirm all three are connected with:
claude mcp list
src/claudeRunner.ts's ALLOWED_TOOLS follows Claude Code's
mcp__<server-name>__<tool-name> convention, and was checked against each
server's live tool docs on 2026-07-06 — including catching a real mistake:
GitHub's MCP server doesn't have a standalone create_issue tool anymore,
it's been consolidated into issue_write with a method param. GitHub in
particular has changed tool names across releases before, so re-verify after
any Docker image update. Your local server names (the github /
google-calendar / teamwork part of each tool string) come from whatever
you called them in claude mcp add, so double check by running:
claude mcp listand confirming the server names match. If they don't, edit ALLOWED_TOOLS in
src/claudeRunner.ts to match. If a name is wrong, nothing breaks loudly —
--allowedTools just won't match, and Claude Code will decline to use that
tool or ask for permission, which will surface as an odd reply from the bot.
Teamwork's tools are less battle-tested than GitHub's/Calendar's — two of the
nine (create_timer's request body, and the pause_timer/complete_timer
endpoint paths) were built from documented conventions rather than an
independently confirmed live call. Test each one directly per
MCP_SETUP.md before trusting Telegram with it.
npm install
cp .env.example .envEdit .env:
TELEGRAM_BOT_TOKEN— create a bot via @BotFather on Telegram (/newbot) and paste the token it gives you.ALLOWED_CHAT_IDS— strongly recommended. Message your new bot once, then check the logs (or use @userinfobot) to find your numeric chat ID, and put it here. Without this, anyone who finds your bot's username can trigger it.CLAUDE_BIN— leave asclaudeunless the CLI isn't on your PATH.CLAUDE_TIMEOUT_MS— how long to wait for a single Claude Code call before giving up (default 2 minutes).CLAUDE_MODEL— defaults toclaude-haiku-4-5-20251001. Haiku is fast/cheap and plenty for issue/event creation; bump toclaude-sonnet-5if you want stronger reasoning.OWNER_ID— optional, enables team-group-chat mode. See below.OWNER_NAME/BOT_NAME— optional, purely cosmetic.OWNER_NAMEshows up in the message anyone outsideALLOWED_CHAT_IDSsees (e.g. "wired up for<OWNER_NAME>'s personal use");BOT_NAMEshows up in the startup log and the bot's own self-introduction. Both fall back to generic text ("the bot owner"/"Telegram Assistant") if left blank — set them so the bot reads naturally for whoever's actually running it.
If you add the bot to a team group, add that group's chat ID to
ALLOWED_CHAT_IDS and set OWNER_ID to your own Telegram user ID (find both
via @userinfobot). With OWNER_ID set:
- Messages from anyone else in the group are silently ignored — the bot won't react, reply, or run Claude Code for them at all.
- Your own messages in the group still run normally, but the reply doesn't always get posted there. Claude decides whether the result is worth the whole team seeing (an error, something needing someone else's input, a result that affects the team) — if not, the group just gets a 👍 reaction and the full reply comes to you as a private DM from the bot instead. Errors are always posted in the group regardless of Claude's judgment, so a failure never silently disappears into a DM only you see.
- Your private 1:1 chat with the bot is unaffected either way — always a full reply, no reactions.
Leave OWNER_ID unset if you'd rather every allowed chat just get full
replies, same as before.
Copy groups.config.example.json to groups.config.json (gitignored, since
it's specific to your teams) and add an entry per chat you want defaults for,
keyed by that chat's numeric Telegram id (as a string, including the - for
groups):
{
"groups": {
"-1001234567890": {
"label": "Website Team",
"githubRepo": "your-org/your-repo",
"githubProject": { "owner": "your-org", "number": 1 },
"context": "We label bugs/features/chores. Board columns: Backlog, In Progress, Review, Done. Default assignee is whoever opens the ticket unless someone else is named."
}
}
}With this in place, a plain "create a ticket for the login bug" in that group
resolves to your-org/your-repo and project 1 automatically — you don't have
to name the repo/board every time, and the context field lets Claude follow
that team's conventions (labels, columns, default assignee, anything else
worth telling it once). All fields are optional; a message can still name a
different repo/board explicitly and Claude will use that instead of the
default. The file is re-read on every message, so edits take effect
immediately — no restart needed. Any chat without an entry just behaves as
before (Claude asks for the repo/board if it's not obvious).
npm run build
npm startOr for local development with auto-reload:
npm run devMessage your bot on Telegram. Try things like:
- "create a github issue in your-org/your-repo titled 'fix login bug' with a short description"
- "what's assigned to me in your-org/your-repo?"
- "close issue #42 in your-org/your-repo as a duplicate of #10"
- "add a comment to issue #7 saying I'll pick this up tomorrow"
- "what's on the Website Redesign project board?" (needs the GitHub Projects setup in MCP_SETUP.md)
- "move item X to In Progress on the your-org project 1 board"
- "add a calendar event tomorrow at 2pm called Dentist"
- "actually make that 3pm" (follow-up, same chat — edits the existing event via
update-eventinstead of creating a duplicate) - "am I free at 3pm tomorrow?"
- "what's on my calendar this week?"
- "start a Teamwork timer for task 12345, mark it billable"
- "what timers do I have running right now?"
- "assign me to the PT10-25-P060 task" (looks the task up by name/reference via
search_tasks, no numeric ID needed, and adds you to the assignee list without removing anyone already on it) - "what tasks are assigned to me on the Website Redesign project?" (resolves the project name via
search_projectsfirst) - "complete my current Teamwork timer"
- "what's the capital of France?" (no tool matches, so Claude just replies)
- Reply to a teammate's message with "create tickets for this" — the quoted message's text (and any photo it has) gets included as context.
- Send a photo of a whiteboard or screenshot with a caption like "make an issue for each of these" — the image gets downloaded and Claude looks at it directly.
- Photos (yours, or one you're replying to) are downloaded to a temp folder and Claude gets a narrowly-scoped permission to
Readjust that one file — not a standing filesystem grant — then the file is deleted once the call finishes. - Replies to another message include that message's text/caption as quoted context, so "create tickets for this" works when "this" is something someone else said.
- Everything else (voice notes, video, stickers, documents, locations, contacts, polls, GIFs) gets a short explanation of what it can't do instead of silence — silence in a group chat is indistinguishable from "you're not the owner" or "the bot crashed," so it always says something.
src/index.ts— Telegram polling loop, per-chat session map, per-chat request queue.src/claudeRunner.ts— spawns theclaudeCLI, parses its JSON output, extractsresultandsession_id.src/messageContent.ts— turns a Telegram message (text, caption, reply-to, photo) into the prompt text and any extraReadpermissions sent to Claude.src/groupConfig.ts— loadsgroups.config.jsonand formats a chat's repo/board/context defaults for the system prompt.src/types.ts— TypeScript shapes for the CLI's JSON output.teamwork-mcp-server/— separate small Node/TS package: a custom local MCP server for Teamwork timers and safe task assignment (no official Teamwork MCP server exists). Its ownpackage.json, builds independently, runs as its own process. Seesrc/index.ts(tool registrations) andsrc/teamworkClient.ts(the actual API calls, including the safe assignee-merge logic).MCP_SETUP.md— one-time setup for the three MCP servers this bot depends on (GitHub, Google Calendar, Teamwork)..env.example/groups.config.example.json— copy these to.env/groups.config.json(both gitignored) and fill in your own values; see Setup above.LICENSE— PolyForm Noncommercial 1.0.0 (free for non-commercial use; commercial use needs permission).
- Sessions are stored in memory only (
Map<chatId, sessionId>) — restarting the bot loses conversation continuity for all chats. No DB, by design, per the v1 scope. - One Claude Code call per chat runs at a time; concurrent messages in the same chat are queued rather than parallelized, to avoid racing on the same
--resumesession. - Photos are supported (downloaded + given to Claude via a scoped
Readgrant); voice, video, documents, and other media types are not — the bot explains this rather than staying silent. Adding real support for those would need transcription (voice/video) or document parsing, which is out of scope for now. - Teamwork support is newer and less verified than GitHub/Calendar:
create_timer's request body shape and thepause_timer/complete_timerendpoint paths were inferred from documented conventions, not confirmed against a live call, since Teamwork's own API docs render some schemas client-side in a way that wasn't fetchable while building this. Test these directly (see MCP_SETUP.md) before relying on them. - This is a single-user (or single-team) personal tool by design: it runs against one Claude Code login and one set of MCP credentials on one machine. It's not multi-tenant — if a teammate wants their own instance, they need their own Claude subscription, their own GitHub/Calendar/Teamwork credentials, and their own copy of this repo running on their own machine (see MCP_SETUP.md).