Claude Code Telegram Assistant — Full Setup Tutorial
This tutorial walks you through building a personal AI assistant that lives on your Mac and answers your messages through Telegram. You text your bot from anywhere, it replies with the full power of Claude Code (Anthropic's command-line assistant), and everything it learns about you gets saved as markdown files you can browse in Obsidian.
When you're done, you'll be able to open Telegram on your phone, DM your bot, and have a conversation with an assistant that remembers everything you've told it before.
What you're building
- A Telegram bot that you own and only you can reach
- Running on your Mac (no cloud, no monthly hosting fees, no data leaves your machine)
- Powered by Claude Code (Anthropic's command-line AI assistant)
- With persistent memory stored as plain markdown files in an Obsidian vault
- That stays online 24/7 as long as your Mac is awake
Before you start
You will need:
- A Mac running macOS Sonoma (14) or newer
- About 60–90 minutes of uninterrupted time
- An Anthropic account with a paid Claude Pro or Max subscription (required for Claude Code; starts at $20/month)
- A Telegram account on your phone (install the Telegram app from the App Store if you don't have it)
- Your Mac's login password (you'll type it once or twice during installation — you won't see the characters when you type, that's normal)
You do NOT need:
- Any programming experience
- Prior command-line experience
- A cloud server or any separate hosting account
You can pause between any two Parts and come back later. Your progress is saved on disk.
Important: understand the trust model before you continue
Once this assistant is set up, it will be able to:
- Read and write every file in your home folder
- Run commands on your Mac
- Access whatever services Claude Code is connected to (GitHub, email, databases, etc. if you have those configured)
You will lock the bot to your Telegram account only during setup. Nobody else can message it and get a response. That lock is the only thing keeping strangers out of your Mac, so do not skip the lockdown step in Part 5 and do not share your bot's username publicly.
If this level of access makes you uncomfortable, this tutorial isn't for you. There are more restrictive setups but they're not covered here.
Part 1: Install the foundation (20 minutes)
You'll install five things: the Terminal (already on your Mac), Homebrew (a tool that installs other tools), Claude Code, Bun (a tiny runtime the Telegram plugin needs), and Obsidian (a markdown note app).
1.1 — Open Terminal
Terminal is an app that comes with every Mac. It's how you run commands by typing instead of clicking.
- Press Cmd + Space (this opens Spotlight Search)
- Type
Terminaland press Return - A window opens with white or black background and a prompt that looks like
yourname@Yourname-MacBook-Pro ~ %— this is the Terminal. Leave it open.
For the rest of the tutorial: whenever you see a command inside a gray box, you should copy it, click into the Terminal window, paste it (Cmd+V), and press Return. Wait for the command to finish before running the next one.
1.2 — Install Homebrew
Homebrew is a tool that installs other tools. It's the standard way most Mac developers install software from the command line.
Paste this into Terminal and press Return:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
It will:
- Ask for your Mac login password (type it — you won't see characters, that's normal — then press Return)
- Show you a list of what it's about to install and ask you to press Return to continue
- Download and install for 5–10 minutes
When it's done, it may print a message saying "Next steps: add Homebrew to your PATH." If it does, copy and run the two commands it shows you (they start with echo and eval). This makes sure Terminal can find Homebrew in the future.
Verify Homebrew works by running:
brew --version
You should see something like Homebrew 4.x.x. If you see "command not found," close Terminal completely (Cmd+Q), open it again, and try the verify command once more.
1.3 — Install Claude Code
Now install Claude Code through Homebrew. Run:
brew install --cask claude-code
This downloads and installs the claude command. It takes 1–2 minutes.
Verify it installed:
claude --version
You should see 2.1.x (Claude Code) or newer. If you see a version lower than 2.1.80, the channels feature you need later won't work — run brew upgrade --cask claude-code to update.
1.4 — Create your Claude account and log in
Claude Code needs to authenticate with your Anthropic account. You need a paid Claude Pro or Max subscription ($20+/month).
- If you don't already have an account, go to https://claude.ai in your web browser, click "Sign up," and create one.
- If you haven't subscribed yet, go to https://claude.ai/settings/billing and subscribe to Claude Pro. (Claude Code will not run on the free plan.)
- Back in Terminal, run:
claude /login - This opens your web browser and asks you to confirm the login. Click through the approval.
- When the browser says "You can close this tab," come back to Terminal. You should see a message confirming login.
Verify login worked by running:
echo "say the word banana" | claude -p
Claude should reply with "banana" (or similar). If it does, auth is working. If you get an error about not being logged in, run claude /login again.
1.5 — Install Bun
Bun is a small runtime that the Telegram plugin uses internally. You install it directly (not through Homebrew — Homebrew's version won't put it in the right place for us).
Run:
curl -fsSL https://bun.sh/install | bash
At the end you'll see a message saying "Manually add the directory to ~/.zshrc" followed by two export lines. This matters — the plugin will fail without these.
Run these two commands to add Bun to your shell configuration permanently:
echo '' >> ~/.zshrc
echo '# Bun (required by Claude Code Telegram channel plugin)' >> ~/.zshrc
echo 'export BUN_INSTALL="$HOME/.bun"' >> ~/.zshrc
echo 'export PATH="$BUN_INSTALL/bin:$PATH"' >> ~/.zshrc
Then close Terminal completely (Cmd+Q, not just closing the window) and open it again. This makes the PATH change take effect.
Verify Bun is now reachable:
which bun
You should see /Users/yourname/.bun/bin/bun. If you see "bun not found," close and reopen Terminal one more time. If it still doesn't work, skip to the Troubleshooting section at the end.
1.6 — Install Obsidian
Obsidian is a free note-taking app that reads markdown files from a folder. Your bot's memory will live in an Obsidian vault so you can browse it like notes.
Run:
brew install --cask obsidian
It takes about 1 minute. You can verify by running:
ls /Applications/Obsidian.app
If that prints /Applications/Obsidian.app, you're done with Part 1.
Part 2: Create your Telegram bot (5 minutes)
This part happens inside the Telegram app, not in Terminal.
2.1 — Open BotFather
BotFather is Telegram's official tool for creating bots. You talk to it like a friend.
- Open Telegram on your phone or at https://web.telegram.org in a browser
- In the search bar, type
BotFatherand look for the one with the blue verified checkmark - Tap it, then tap Start at the bottom (or just send the message
/start)
2.2 — Create your bot
Send BotFather the message:
/newbot
BotFather asks two questions:
- "Choose a name for your bot" — this is the display name that appears in chats. Anything works. Examples:
My Claude Assistant,Sam's Personal AI,Desktop Bot. - "Choose a username for your bot" — this has to be unique across all of Telegram and must end in
bot. Examples:sam_claude_daemon_bot,myname_assistant_bot,personal_claude_bot. If it's already taken, BotFather will tell you and ask again.
When both are accepted, BotFather replies with:
- A link to your new bot (looks like
t.me/yourbotname) - A token (a long string that looks like
1234567890:ABCdef...)
2.3 — Save the token somewhere safe (temporarily)
Copy the entire token — both the number part before the colon and the letters/numbers after. You'll paste it in a few minutes.
Treat this token like a password. Anyone who has it can send messages as your bot. Don't post it on the internet, don't paste it into a public Slack, don't email it to yourself unencrypted. Just keep it in the Telegram chat with BotFather (which is encrypted) and copy it when you need it.
2.4 — Disable group adding
You want this bot to only respond to you in direct messages, not be addable to random Telegram groups. Tell BotFather:
/setjoingroups
BotFather shows a list of your bots — tap the one you just made. Then tap Disable when it asks.
Done with Part 2. Back to Terminal.
Part 3: Create the daemon workspace (5 minutes)
You'll create two folders: one for the daemon itself, and one Obsidian vault where its memory will live.
3.1 — Create the daemon folder
In Terminal, run:
mkdir -p ~/claude-daemon
This creates a folder called claude-daemon inside your home folder. The daemon runs from here.
3.2 — Create the daemon's personality file
The daemon needs a CLAUDE.md file that tells it who you are, how to route Telegram messages, and how to save memories. Create it by running this one big command (copy the whole thing, including the cat and EOF lines):
cat > ~/claude-daemon/CLAUDE.md << 'EOF'
# Claude Daemon — Telegram Bridge
You are running as a persistent assistant reachable via Telegram. This CLAUDE.md governs how you behave in this session.
## How messages arrive
Every Telegram message enters your context as:
<channel source="telegram" chat_id="NNNNN" ...>message body</channel>
You receive all chats in a single rolling context — there is no per-chat session. You partition chats yourself by reading the chat_id attribute and keeping context for each chat_id mentally separate.
## How to reply
To send a message back to Telegram, call the reply tool (provided by the telegram channel plugin):
reply(chat_id="NNNNN", text="your response")
**Always pass the chat_id from the inbound channel tag.** Replying to the wrong chat_id sends the message to the wrong person. This is the single most important rule.
If two different chats message you in quick succession, answer each one with its own reply call, passing that chat's specific chat_id.
## Memory discipline (load-bearing)
This session can be killed and restarted at any time. When it restarts, everything that was only in-context is gone. Only what you wrote to memory survives. Be aggressive about writing memory:
1. Every meaningful fact a user tells you — write a memory file immediately, don't wait
2. Every decision that should persist — memory
3. Every preference, name, date, project, goal, ongoing task — memory
4. If in doubt, write it
Memory files live in the auto-memory directory at ~/.claude/projects/.../memory/. Use the standard format — frontmatter with name, description, type — and add one extra field:
name: <short name> description: <one-liner> type: user | feedback | project | reference channel: <chat_id>
The channel field is how chats stay distinguishable after a session restart. Every memory must have it.
When you update MEMORY.md, use wikilink form like [[filename]] (not markdown links) so Obsidian backlinks work.
## Reading memory
At the start of each response to a Telegram message:
1. Check if the chat_id has existing memories
2. If there are relevant memories, read them before composing a reply
3. Trust memory files over in-context recall — the session may have been restarted
## Channel hygiene
- Never mention other chats' content when replying to a chat
- If someone asks about a different chat, refuse politely
- If unsure which chat a fact came from, read the memory file
## Special Telegram commands
Recognize these exact keywords at the start of a message body (case-insensitive, trimmed whitespace) as meta-commands, not as normal conversation.
### /fresh — manual session restart
When you see /fresh from a Telegram chat:
1. Reply first: reply(chat_id, "Restarting for a fresh session — back in ~10 seconds. Memory is durable; anything I learned is already saved.")
2. Write a breadcrumb memory (type: project, channel: that chat_id) with one line: "Operator issued /fresh at <timestamp>, reason: manual context reset."
3. Schedule a detached self-kill via Bash. Run exactly this command:
nohup bash -c 'sleep 3 && pkill -TERM -f "claude.*--channels.*telegram"' >/dev/null 2>&1 &
4. Do nothing else on this turn. The wrapper script will relaunch within ~10 seconds.
### /ctx — context window usage
When you see /ctx from a Telegram chat:
1. Run ~/claude-daemon/ctx via Bash. Capture stdout.
2. Reply with the result to that chat_id. Example: "ctx: 87,432 / 200,000 tokens (43.7%). You can /fresh anytime to reset."
3. Do nothing else on this turn.
### Automatic context warnings
On every reply to a Telegram message (except /ctx and /fresh themselves), before calling the reply tool, run ~/claude-daemon/ctx --compact via Bash. This returns a percentage like "43.7%". Parse the number.
- Below 75%: silent. Send the normal reply with no footer.
- 75 to 89%: append to the reply text on its own line: [ctx: NN% — consider /fresh soon]
- 90% or above: append: [ctx: NN% — /fresh strongly recommended, context is almost full]
If the ctx script errors (e.g. new session with no transcript yet), ignore and proceed normally.
## Tone
Direct, helpful, no corporate hedging. Match the operator's energy.
EOF
Verify it was written:
cat ~/claude-daemon/CLAUDE.md | head -5
You should see the first few lines of the file you just created.
3.3 — Create the Obsidian vault
Run:
mkdir -p ~/Obsidian/ClaudeVault/memory
This creates a folder structure where your bot's memories will eventually live.
3.4 — Open the vault in Obsidian once
Obsidian needs to initialize a config inside the vault folder. This only has to happen once, and it has to be done through the Obsidian app (not Terminal).
- Open Obsidian (Cmd+Space →
Obsidian→ Return) - If it's your first time using Obsidian, it shows a welcome screen. Click "Open folder as vault"
- If it shows a list of existing vaults instead, click the folder-plus icon at the bottom and select "Open folder as vault"
- Navigate to Users → [your name] → Obsidian → ClaudeVault and click Open
- If Obsidian asks about trusting the author or enabling plugins, click "Trust author & enable plugins" (there are no plugins yet, this is harmless)
- You should now see an empty vault with a
memoryfolder in the sidebar - Close Obsidian (Cmd+Q) or leave it running — either is fine
Back to Terminal.
Part 4: Install the Telegram plugin in Claude Code (5 minutes)
4.1 — Start Claude Code
From Terminal, run:
claude
Claude Code starts. You'll see a text interface with a prompt at the bottom. You're now talking to Claude interactively.
4.2 — Add the plugin marketplace
Type this into the Claude Code prompt and press Return (this is a slash command, not a normal message):
/plugin marketplace add anthropics/claude-plugins-official
You should see a confirmation like "Successfully added marketplace."
4.3 — Install the Telegram plugin
Type:
/plugin install telegram@claude-plugins-official
You should see "Installed telegram. Run /reload-plugins to apply."
4.4 — Reload plugins
Type:
/reload-plugins
You should see a count of reloaded plugins and agents.
4.5 — Configure your bot token
Now you use the token from Part 2. Type:
/telegram:configure YOUR_BOT_TOKEN_HERE
Replace YOUR_BOT_TOKEN_HERE with the full token BotFather gave you (the number, the colon, and the letters/digits after). Claude will save it to a hidden config file at ~/.claude/channels/telegram/.env with permissions that only you can read.
Claude will then show you the current status of the Telegram channel — policy should be "pairing" and the allowlist should be empty. That's expected.
4.6 — Exit this Claude Code session
Type:
/exit
Or press Ctrl+D. You should be back at the regular Terminal prompt.
Part 5: First daemon launch and pairing (10 minutes)
Now you'll launch the daemon for real, pair your Telegram account with it, and lock it down so only you can use it.
5.1 — Launch the daemon
In Terminal, run:
cd ~/claude-daemon && claude --channels plugin:telegram@claude-plugins-official
The command does two things: it moves into the daemon folder (so Claude picks up the CLAUDE.md you wrote), and it starts Claude Code with the Telegram channel plugin enabled.
You'll see Claude Code's interface again, plus two lines:
Listening for channel messages from: plugin:telegram@claude-plugins-official
Experimental - inbound messages will be pushed into this session
That second line is a warning about the research-preview nature of the channels feature. It's fine.
Leave this Terminal window alone from now on. It's running the daemon. If you close it, the daemon dies.
5.2 — DM your bot from your phone
On your phone, open Telegram. Open your bot chat — either tap the t.me/yourbotname link BotFather gave you, or search for your bot's username.
Send any message to the bot — hi is fine.
The bot will not respond with an answer yet. Instead, it will reply with:
Pairing required - run in Claude Code:
/telegram:access pair ABCDEF
(Your six-character code will be different.)
Copy the six-character pairing code.
5.3 — Approve the pairing
Do not close the daemon Terminal window. You need to type the pair command in that same window — the one running the daemon. Click into the daemon window and type:
/telegram:access pair ABCDEF
Replace ABCDEF with the code your bot sent you. Press Return.
You should see a confirmation that the pairing was approved. Your Telegram user ID is now on the bot's allowlist.
5.4 — Lock down the bot (MANDATORY — do not skip)
Still in the daemon window, type:
/telegram:access policy allowlist
This switches the bot's policy from "let anyone pair" to "only allow accounts already on the list." This is the only thing keeping strangers out of your Mac. Do not skip this step.
5.5 — Test the round trip
On your phone, DM your bot again. This time, send:
remember that my favorite test phrase is purple-flamingo-42
Within a few seconds, the bot should reply with a real message from Claude — something like "Got it, I'll remember that." Claude is also writing a memory file in the background.
Wait 30 seconds, then DM the bot:
what's my favorite test phrase?
It should reply with purple-flamingo-42. If it does — congratulations, the core loop works. Your personal AI assistant just answered a question about something you told it a minute ago, from memory.
If the bot doesn't reply, or if something else goes wrong, skip to the Troubleshooting section at the end of this tutorial.
Part 6: Connect memory to your Obsidian vault (5 minutes)
Right now, the daemon's memory is saved inside Claude Code's internal folder — a location that's hard to browse. You'll move it into the Obsidian vault you created earlier, so you can read and edit memories like any other note.
6.1 — Stop the daemon
Click into the daemon Terminal window. Press Ctrl+C to stop Claude Code cleanly. The terminal should return to a normal shell prompt.
Leave this window open — you'll restart the daemon in a moment.
6.2 — Find the daemon's memory folder
The daemon stores its memory in a folder whose name includes your username. Find it by running:
ls ~/.claude/projects/ | grep daemon
You should see one folder name that contains claude-daemon. Copy that name.
Now run this command, replacing FOLDERNAME with what you just copied:
ls ~/.claude/projects/FOLDERNAME/memory/
You should see a MEMORY.md file and a file named something like user_test_phrase.md. That's the memory Claude wrote during Part 5.
6.3 — Move the memory into the vault
Run this command, again replacing FOLDERNAME with the folder name you found:
rm -rf ~/Obsidian/ClaudeVault/memory && mv ~/.claude/projects/FOLDERNAME/memory ~/Obsidian/ClaudeVault/memory && ln -s ~/Obsidian/ClaudeVault/memory ~/.claude/projects/FOLDERNAME/memory
This command does three things:
- Removes the empty
memoryfolder from the vault (the one you created in Part 3) - Moves the daemon's real memory folder into the vault
- Creates a "symbolic link" (a pointer) from Claude Code's expected location to the vault — so Claude still reads and writes as if nothing moved, but the actual files live in the vault
You should not see any error messages. If you do, copy the error and see the Troubleshooting section.
6.4 — Restart the daemon
In the same Terminal window, run:
cd ~/claude-daemon && claude --channels plugin:telegram@claude-plugins-official
You should see the same "Listening for channel messages" output as before.
6.5 — Verify in Obsidian
- Open Obsidian
- You should see the
memoryfolder in the left sidebar now containsMEMORY.mdanduser_test_phrase.md - Click on
user_test_phrase.md— you should see its contents, including frontmatter with achannel:field - Now DM your bot again: "what's my favorite test phrase?"
- It should still answer
purple-flamingo-42— proving the symbolic link works transparently
From now on, anything the bot remembers shows up in Obsidian, and anything you edit in Obsidian is read by the bot on its next interaction.
Part 7: Keep the daemon running 24/7 (10 minutes)
If you stop here, the daemon will die whenever your Mac goes to sleep (which happens automatically when you're not using it). To keep it running around the clock, you'll wrap it in a small script that prevents sleep and auto-restarts it if anything goes wrong.
7.1 — Create the wrapper script
Stop the daemon first: click into its Terminal window and press Ctrl+C.
Now create the wrapper by running this (copy the whole thing):
cat > ~/claude-daemon/start-daemon.sh << 'EOF'
#!/usr/bin/env bash
# Claude Code Telegram daemon wrapper
# Prevents idle sleep (caffeinate -i) and auto-restarts if the daemon dies.
# Stop with Ctrl+C inside this window.
set -u
LOG="$HOME/Library/Logs/claude-daemon-wrapper.log"
mkdir -p "$(dirname "$LOG")"
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg"
echo "$msg" >> "$LOG"
}
trap 'log "wrapper stopped by signal"; exit 0' INT TERM
log "wrapper started (pid $$)"
cd "$HOME/claude-daemon" || { log "cannot cd to ~/claude-daemon"; exit 1; }
if ! command -v bun >/dev/null 2>&1; then
log "bun not in PATH — plugin will fail. Check ~/.zshrc BUN_INSTALL export."
exit 1
fi
if ! command -v claude >/dev/null 2>&1; then
log "claude not in PATH"
exit 1
fi
# Try to upgrade Claude Code before each launch. Bounded to 90s so a slow
# Homebrew call can't stall the daemon. On failure, launch the current
# version anyway — an upgrade hiccup must never block the loop.
try_upgrade() {
if ! command -v brew >/dev/null 2>&1; then
return 0
fi
local upgrade_output
if upgrade_output=$(perl -e 'alarm shift; exec @ARGV' 90 brew upgrade --cask claude-code 2>&1); then
if echo "$upgrade_output" | grep -q "already up-to-date\|No outdated"; then
log "claude-code already up to date"
else
log "claude-code upgraded: $(echo "$upgrade_output" | tail -1)"
fi
else
log "claude-code upgrade failed or timed out, launching current version"
fi
}
while true; do
try_upgrade
log "launching daemon"
caffeinate -i claude --dangerously-skip-permissions --channels plugin:telegram@claude-plugins-official
EXIT_CODE=$?
log "daemon exited with code $EXIT_CODE, restarting in 5s"
sleep 5
done
EOF
Note: this uses --dangerously-skip-permissions which means Claude won't ask for permission before running tools. This is fine because your bot is locked to your Telegram account only — you are the only one who can make it do anything. If you prefer the bot to ask permission for every action, remove that flag from the script. (Be warned: you'll be tapping Allow on your phone for every single Bash command and reply.)
7.2 — Make the script executable
chmod +x ~/claude-daemon/start-daemon.sh
7.3 — Launch it
From now on, instead of typing the long claude --channels... command, just run:
~/claude-daemon/start-daemon.sh
You should see:
[timestamp] wrapper started (pid XXX)
[timestamp] launching daemon
Followed by Claude Code's interface. The daemon is now running under the wrapper with sleep prevention and auto-restart.
7.4 — Final test
DM your bot from your phone. It should reply. The round trip is now protected by the wrapper.
Leave the Terminal window open. If you close it, the wrapper dies and the daemon with it. You can minimize the window or move it to another desktop (Mission Control → new Space) and forget about it.
Part 8: Context tracking + daily auto-restart (15 minutes)
After you've used the bot for a few days, two things start mattering:
- Context bloat. Every message you send adds to the daemon's rolling conversation history. Over hundreds of messages, the daemon becomes slower, more expensive per reply, and occasionally less coherent (it starts cross-referencing old unrelated chats). The fix is periodic restarts that wipe the in-context history while keeping memory intact on disk.
- Not knowing when it's getting bad. You can't manage what you can't see. You need a way to check "how full is the context right now?" from Telegram.
This Part adds three things:
- A
ctxscript that reports how full the context window is - Two special Telegram commands (
/ctxand/fresh) the bot recognizes as meta-commands - A daily automatic restart at 4am via macOS's built-in scheduler (launchd), so even if you never think about context, the daemon refreshes itself invisibly every night
8.1 — Install the context-check script
Stop the daemon (Ctrl+C in its window) and run this in Terminal:
cat > ~/claude-daemon/ctx << 'EOF'
#!/usr/bin/env python3
"""Report Claude Code daemon context window usage."""
import json, sys
from pathlib import Path
CONTEXT_WINDOWS = {
"claude-opus-4-6[1m]": 1_000_000,
"claude-opus-4-6": 200_000,
"claude-sonnet-4-6[1m]": 1_000_000,
"claude-sonnet-4-6": 200_000,
"claude-haiku-4-5-20251001": 200_000,
"claude-haiku-4-5": 200_000,
}
DEFAULT_CONTEXT_WINDOW = 200_000
_daemon_working_dir = Path.home() / "claude-daemon"
_encoded = str(_daemon_working_dir).replace("/", "-")
PROJECT_DIR = Path.home() / ".claude" / "projects" / _encoded
def latest_transcript():
if not PROJECT_DIR.exists():
return None
files = sorted(PROJECT_DIR.glob("*.jsonl"), key=lambda p: p.stat().st_mtime, reverse=True)
return files[0] if files else None
def latest_usage(path):
latest = None
with open(path, "rb") as f:
for line in f:
try:
rec = json.loads(line)
except Exception:
continue
msg = rec.get("message") or {}
if msg.get("role") != "assistant":
continue
u = msg.get("usage") or {}
if u.get("input_tokens") is None:
continue
total = (u.get("input_tokens") or 0) + (u.get("cache_read_input_tokens") or 0) + (u.get("cache_creation_input_tokens") or 0)
latest = (total, msg.get("model") or "unknown")
return latest
def main():
compact = "--compact" in sys.argv
path = latest_transcript()
if path is None:
print("ctx: no transcript found", file=sys.stderr)
sys.exit(1)
usage = latest_usage(path)
if usage is None:
print("ctx: no usage data yet (new session)", file=sys.stderr)
sys.exit(2)
total_tokens, model = usage
window = CONTEXT_WINDOWS.get(model, DEFAULT_CONTEXT_WINDOW)
pct = (total_tokens / window) * 100
if compact:
print(f"{pct:.1f}%")
else:
print(f"ctx: {total_tokens:,} / {window:,} tokens ({pct:.1f}%) [{model}]")
if __name__ == "__main__":
main()
EOF
chmod +x ~/claude-daemon/ctx
Test it (requires the daemon to have run at least once so there's a transcript to read):
~/claude-daemon/ctx
If the daemon has never run yet, you'll see "no transcript found" — that's expected. Once you launch the daemon and send one message, running ctx again will show something like ctx: 15,432 / 200,000 tokens (7.7%) [claude-opus-4-6].
8.2 — Teach the bot the /fresh and /ctx commands
The daemon already knows about these commands because you included them in CLAUDE.md back in Part 3.2 — look for the "Special Telegram commands" section. If you followed this tutorial in order, no changes needed and you can skip to 8.3.
If you're adding these commands to an existing setup that doesn't have them yet, append the Special Telegram commands section from Part 3.2 to your existing ~/claude-daemon/CLAUDE.md and restart the daemon.
8.3 — Set up the daily 4am auto-restart
macOS has a built-in scheduler called launchd. You'll create a small config file that tells launchd to kill the daemon at 4am every day. The daemon's wrapper will notice the death and auto-restart it with a fresh session. You sleep through it, but every morning you wake up to a bot that hasn't been running for more than a few hours.
In Terminal, run this to create the launchd config:
mkdir -p ~/Library/LaunchAgents
cat > ~/Library/LaunchAgents/com.user.claude-daemon-restart.plist << 'PLIST_EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.claude-daemon-restart</string>
<key>ProgramArguments</key>
<array>
<string>/bin/zsh</string>
<string>-c</string>
<string>/usr/bin/pkill -TERM -f "claude.*--channels.*telegram" >> "$HOME/Library/Logs/claude-daemon-wrapper.log" 2>&1; /bin/echo "[$(date '+%Y-%m-%d %H:%M:%S')] launchd: scheduled 4am restart signal sent" >> "$HOME/Library/Logs/claude-daemon-wrapper.log"</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>4</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string>/tmp/claude-daemon-restart.stdout</string>
<key>StandardErrorPath</key>
<string>/tmp/claude-daemon-restart.stderr</string>
<key>RunAtLoad</key>
<false/>
</dict>
</plist>
PLIST_EOF
Then load it into launchd (this registers the scheduler so it actually fires at 4am):
launchctl load -w ~/Library/LaunchAgents/com.user.claude-daemon-restart.plist
Verify it's registered:
launchctl list | grep claude-daemon-restart
You should see one line with the label name. The first column shows - (no PID) — that's correct, the task isn't running right now, it's waiting for 4am. The second column shows 0 (last exit status ok).
8.4 — Restart the daemon so everything takes effect
~/claude-daemon/start-daemon.sh
The wrapper's new "try_upgrade" logic will run first and check for Claude Code updates. Then the daemon launches, reads the updated CLAUDE.md with the /fresh and /ctx commands, and starts listening.
8.5 — Test all three new features from Telegram
-
/ctx— send it to your bot from your phone. The bot should reply with something likectx: 15,432 / 200,000 tokens (7.7%) [claude-opus-4-6]. This proves the script is reachable and the command-handling rule inCLAUDE.mdis working. -
/fresh— send it. The bot should reply with "Restarting for a fresh session — back in ~10 seconds..." then go silent. Wait 15 seconds, then DM the bot again with any message. It should answer — proving the auto-restart worked. Check the wrapper log to confirm:grep -i "launching\|exited" ~/Library/Logs/claude-daemon-wrapper.log | tail -6You should see the old daemon exit and a new one launch a few seconds later.
-
4am restart — this one you can only verify tomorrow morning. Run:
grep "scheduled 4am\|launching daemon" ~/Library/Logs/claude-daemon-wrapper.log | tail -10Tomorrow after 4am you should see a "launchd: scheduled 4am restart signal sent" line followed immediately by the daemon exiting and a new one launching.
With all three working, your daemon is now effectively self-maintaining.
How to use it day-to-day
Starting the daemon
After any Mac reboot, open Terminal and run:
~/claude-daemon/start-daemon.sh
That's the only command you need to memorize.
Checking if it's alive
From any other Terminal window, run:
ps aux | grep "claude.*channels" | grep -v grep
If you see a line of output, the daemon is running. If you see nothing, it's dead.
You can also tail the wrapper log to see restart events:
tail -20 ~/Library/Logs/claude-daemon-wrapper.log
Stopping the daemon
Click into the daemon window and press Ctrl+C. The wrapper catches the signal and exits cleanly.
Editing your bot's memory
Open Obsidian. Navigate to ClaudeVault/memory/. Edit any memory file. The bot will see your changes the next time it reads that file. You can also add new memory files yourself if you want to seed the bot with facts.
Checking how full the context is
From Telegram, DM your bot:
/ctx
You'll get back a usage report like ctx: 87,432 / 200,000 tokens (43.7%). Use this when the bot is starting to feel slow or when you want to decide whether to reset.
Resetting the context manually
From Telegram, DM your bot:
/fresh
The bot confirms, goes silent for about 10 seconds while the wrapper restarts it, then is back online with a completely empty context. Memory on disk is untouched — anything the bot learned persists. Use this:
- Between unrelated tasks ("I was working on X, now I want to talk about Y")
- At the end of a long debugging session
- Whenever
/ctxtells you the context is over 75% - When the bot starts referencing things from earlier chats that shouldn't be relevant
The daemon also resets itself automatically at 4am every day, so you don't strictly need to remember — but manual /fresh is useful between sessions of active work.
What survives restarts
- Everything written to a memory file (persistent)
- Bot token and access policy (persistent)
What does NOT survive restarts
- The current in-context conversation (gone — but memory has the important facts)
- Whatever the bot was "thinking about" mid-response (gone)
This is why the CLAUDE.md file tells the bot to be aggressive about writing memory. Anything important should be on disk.
Troubleshooting
"bun not found" after installing Bun
Close Terminal completely (Cmd+Q). Open it again. If it still doesn't work:
cat ~/.zshrc | grep -i bun
You should see two lines mentioning BUN_INSTALL and PATH. If you don't, re-run the echo commands from Part 1.5, then close and reopen Terminal.
"brew install" fails with "directories are not writable"
Your Homebrew installation has broken ownership. Run this (it will ask for your password):
sudo chown -R $(whoami) /opt/homebrew
Then try the failed command again.
The bot doesn't reply to my messages
First, check that the daemon is actually running. In a new Terminal window:
ps aux | grep "claude.*channels" | grep -v grep
If you see no output, the daemon died. Start it again with ~/claude-daemon/start-daemon.sh.
If the daemon is running but still not replying:
ps aux | grep bun | grep -v grep
If there's no bun process, the Telegram plugin died. Stop the daemon (Ctrl+C in its window) and restart it with the wrapper — it should spawn a fresh plugin.
If both claude and bun are running but messages still don't reach your phone, verify your bot token is correct:
cat ~/.claude/channels/telegram/.env
You should see TELEGRAM_BOT_TOKEN= followed by the token you got from BotFather. If the token is wrong or missing, run Claude Code (just type claude) and use /telegram:configure YOUR_TOKEN to fix it.
The bot replies with "Pairing required" every time, even after I paired
You skipped Part 5.4. Run the daemon, then in its window type:
/telegram:access policy allowlist
Then the bot will stop sending pairing prompts.
Claude asks permission for every message from Telegram
You removed the --dangerously-skip-permissions flag from the wrapper script, or you're running the daemon without the wrapper. If you want permissionless mode, make sure start-daemon.sh has that flag in the caffeinate -i claude ... line. If you want permissions (safer but annoying), tap Allow on each prompt from your phone.
The daemon dies every morning
It's being killed by idle sleep even with the wrapper. Check that caffeinate -i is in the wrapper's command line:
grep caffeinate ~/claude-daemon/start-daemon.sh
If it's there and the daemon still dies, your Mac may be forcing sleep despite caffeinate (rare but possible on newer macOS versions). As a workaround, open System Settings → Battery → Options → set "Prevent automatic sleeping when the display is off" to On. This is a more aggressive fix but guaranteed to work.
/ctx returns "no transcript found"
The daemon hasn't run yet or hasn't processed any messages. Launch it, send at least one message from Telegram, then try /ctx again.
/ctx returns "no usage data yet (new session)"
You just restarted the daemon and it hasn't finished processing a message with a usage field yet. Wait a few seconds and try again.
/fresh doesn't restart the daemon
Check that you included the Special Telegram commands section in your ~/claude-daemon/CLAUDE.md. Run:
grep -c "/fresh" ~/claude-daemon/CLAUDE.md
You should see a number greater than 0. If it's 0, the commands aren't in your CLAUDE.md — add them from Part 3.2 and restart the daemon.
Also check that the pattern the pkill command uses actually matches your daemon. From any Terminal:
pgrep -fa "claude.*--channels.*telegram"
You should see your running daemon. If pgrep returns nothing but the bot is answering messages, the daemon is running under a different command line — in which case the pkill in /fresh won't find it. Adjust the pattern in CLAUDE.md to match.
The 4am restart doesn't fire
Verify the LaunchAgent is registered:
launchctl list | grep claude-daemon-restart
If it's not listed, reload it:
launchctl unload ~/Library/LaunchAgents/com.user.claude-daemon-restart.plist 2>/dev/null
launchctl load -w ~/Library/LaunchAgents/com.user.claude-daemon-restart.plist
Check tomorrow morning's log after 4am:
grep "scheduled 4am\|launching" ~/Library/Logs/claude-daemon-wrapper.log | tail -10
You should see a "scheduled 4am" line. If you see the scheduled line but no subsequent "launching daemon" line, the pkill didn't match the running daemon — probably the same pattern issue as above. Fix the pattern in both places.
Memory files aren't showing up in Obsidian
Check the symbolic link is still in place:
ls -la ~/.claude/projects/ | grep daemon
Find the daemon folder name, then:
ls -la ~/.claude/projects/FOLDERNAME/memory
You should see it listed with -> pointing to ~/Obsidian/ClaudeVault/memory. If it doesn't have the arrow, the symlink was broken (maybe by a file operation that replaced it with a real folder). Re-run the symlink command from Part 6.3, but first move any new memories out of the way:
mv ~/.claude/projects/FOLDERNAME/memory ~/Obsidian/ClaudeVault/memory-backup
ln -s ~/Obsidian/ClaudeVault/memory ~/.claude/projects/FOLDERNAME/memory
Then manually copy files from memory-backup into ~/Obsidian/ClaudeVault/memory/ if you want to keep them.
Known limitations
These are things this setup genuinely doesn't handle well:
- Mac reboots: you have to manually restart the daemon. There's a way to make it auto-start at boot (macOS launchd) but it's out of scope for this tutorial.
- Closing the laptop lid: the wrapper prevents idle sleep, not lid-close sleep. If you close your lid the daemon will probably die.
- Many active chats: because it's one Claude Code session receiving all Telegram chats, conversations can feel muddy if you're simultaneously running 5+ distinct chat topics. For 1–3 active topics it's fine.
- Long tasks while you're away: if you ask the bot to do something that takes more than a couple of minutes, and you lose internet during that time, the bot may be unable to reply even after the work finishes.
What to do next
Once you've lived with the basic setup for a week or two, things worth exploring:
- Seed your bot with memory files manually: create markdown files directly in
~/Obsidian/ClaudeVault/memory/with your background, preferences, projects, and goals. The bot will read them and treat them as known facts. - Use it from your phone for real work: ask it to draft emails, recall past decisions, summarize code, run terminal commands on your Mac remotely.
- Add more tools: Claude Code has a plugin marketplace. Anything you install in Claude Code becomes available to the Telegram bot automatically.
A note on trust
This setup trades safety for power. A permissionless bot locked to your Telegram account is fast and frictionless — but if your Telegram account is ever compromised, the attacker has effectively unlimited access to your Mac.
Mitigate by:
- Using a strong, unique password and two-factor authentication on your Telegram account
- Not sharing your bot's username publicly
- Reviewing
~/Library/Logs/claude-daemon-wrapper.logand the daemon's Terminal window periodically to see what it's been doing - Turning off the daemon (Ctrl+C) any time you don't need it running
This is not a product. It's a personal tool running on your personal machine. Be thoughtful about what you tell it and what you let it do.
Enjoy your assistant.