Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

Claude on a VPS — Telegram bridge + overnight autonomous research

A blueprint, not a turn-key product. It captures the architecture and the hard-won design decisions behind running Claude Code headless on a cheap VPS, driven two ways:

  1. Interactive — you text a Telegram bot, it runs claude on the server and texts the answer back.
  2. Autonomous — a nightly cron hands Claude a research brief and delivers a finished report to Telegram by morning.

Why a blueprint and not code-you-clone? The valuable part here isn't a few hundred lines of Python — it's the failure modes you only discover in production (orphaned chromium processes, OAuth token timing, sync that silently dies). This repo hands you the architecture and the gotchas. You hand it to your own Claude and have it build the implementation against your stack, your secrets, your VPS. The reference snippets in reference/ are annotated skeletons with placeholders, not a working deploy.


The shape of it

                          ┌──────────────────────────────┐
   Telegram  ──text──▶    │   Bot process (systemd)       │
                          │   aiogram long-polling        │
                          └──────────────┬───────────────┘
                                         │ spawn, capture stdout
                                         ▼
                          ┌──────────────────────────────┐
   cron (nightly) ──▶     │   claude -p  (headless CLI)   │
                          │   --dangerously-skip-perms    │
                          │   runs in workspace/          │
                          └──────────────┬───────────────┘
                                         │ result
                                         ▼
                          Telegram  ◀──text / document──

   workspace/  ◀── git pull (cron, every 30 min) ── GitHub (your repo)

Two entry points (Telegram message, cron tick), one engine (claude -p headless), one shared workspace that stays fresh via git pull.


Capability 1 — interactive Telegram bridge

A long-running bot (systemd service, not Docker — you want a stable long-poll, not a restart-happy container) receives a message, runs claude -p "<message>" in the workspace directory, and returns stdout.

The non-obvious decisions:

  • Run Claude in its own process group (start_new_session=True) and on timeout kill the whole group (killpg), not just the claude process. Claude spawns bash → node → chromium for things like screenshots; a plain proc.kill() leaves those grandchildren orphaned (re-parented to PID 1), still holding resources, and the bot reads as "busy" until the next timeout. This single bug will waste a day if you don't know it up front. See reference/claude-runner.py.
  • Hard wall-clock timeout on every run. Headless agents can wedge. A 300s cap with a clean kill-tree is the floor.
  • Allowlist by Telegram user ID in env — never open the bot to the world. It runs a shell on your server.
  • Inject standing instructions via --append-system-prompt instead of trusting the model to remember conventions across stateless -p calls (see the screenshot rule below).

The screenshot trap (worth its own section)

If your agent ever renders HTML→PNG on the VPS, do not let it write ad-hoc puppeteer with waitUntil: 'networkidle0'. On a headless server, any HTML that references an external font or CDN that never loads makes networkidle0 hang forever. Three layers fix it:

  1. A single screenshot helper (shot <input> <output>) using puppeteer-core + system Chromium, with sane timeouts (waitUntil: 'load', ~30s nav + ~45s hard cap) — on timeout it screenshots whatever rendered instead of hanging.
  2. Force the agent to use it via --append-system-prompt ("NEVER write ad-hoc puppeteer, call shot").
  3. A watchdog cron (every few minutes) that SIGKILLs any chromium/render or orphaned claude-wrapper older than N minutes — the backstop for everything the first two miss.

Capability 2 — overnight autonomous research ("night shift")

You queue a research brief during the day; cron runs it at night; a finished, cited report lands in Telegram by morning. The interesting part is cost control via model tiering.

The insight: the expensive lever in agentic research isn't which model reasons — it's which model reads the raw context (search results, scraped pages). Raw context is huge; conclusions are small. So split into three phases:

Phase Model Reads Writes
Gather cheap (e.g. Sonnet) runs all search/scrape raw structured notes
Synthesize expensive (e.g. Opus) only the notes analysis, conclusions
Write cheap (e.g. Sonnet) notes + synthesis final formatted report

The expensive model never touches raw scrapes — it reads distilled notes, so it's cheap in absolute terms while still doing the hard reasoning. See reference/night-run.sh.

Other decisions that matter:

  • Resume by artifact. Each phase writes a file (notes.md, synth.md, report.md). If a phase dies, the next cron slot sees the artifact already exists and continues from there — finished phases are never re-run.
  • Multiple cron slots as retry windows. A primary slot plus a couple of later ones; a failed run is retried in the next slot with a prompt that says continue the partial report, don't restart. Bounded by a max-attempts counter.
  • Time the slots to your subscription's token refresh. If you're on a subscription with a rolling quota, schedule the safety-net slot just after the refresh boundary (e.g. a few minutes past, not exactly on it, to avoid colliding with the previous slot's lock).
  • flock so slots never overlap. One run at a time, period.
  • Delivery is push-only. The VPS has a read-only deploy key (it pulls the workspace, never pushes). The report is delivered as a Telegram document, not committed back.

Keeping the workspace fresh: git pull, not rsync

The agent works against a workspace/ that mirrors your repo. Sync it with a cron git pull --ff-only every ~30 min, using a read-only deploy key.

Learn from the scar: an earlier rsync-from-laptop approach silently stopped working for five weeks (Network is unreachable inside the scheduler's restricted context — no error surfaced). Pull-based sync on the server is observable and self-contained. Add a staleness alert: if workspace/ hasn't updated in >2h, ping yourself on Telegram (with anti-spam, e.g. at most once per 6h). A sync you don't monitor is a sync that's already broken.


Build it yourself (hand this to your Claude)

This repo is meant to be read by an agent. A starting prompt:

Read the README and reference/ in this repo. I want to stand up the same architecture on my own VPS. My stack: <provider/OS>. My Telegram bot token and allowed user ID are in <where>. My Claude auth is <API key / OAuth>. Build me: (1) the Telegram→claude -p bridge as a systemd service with process-group kill-tree on timeout, (2) the screenshot helper + watchdog, (3) the 3-phase night-shift run.sh with artifact resume and a cron schedule. Use placeholders for anything secret and tell me exactly what to fill in.

Then iterate with it against your real server.


Prerequisites you'll need to supply

  • A VPS (2 vCPU / 4 GB is plenty) with Claude Code CLI installed and authenticated (API key or subscription OAuth token).
  • A Telegram bot token (from @BotFather) and your numeric Telegram user ID.
  • A GitHub repo for the workspace + a read-only deploy key on the VPS.
  • flock, cron, and (for screenshots) system Chromium + puppeteer-core.

What this repo deliberately does NOT contain

No tokens, no IPs, no domains, no usernames, no working .env. The reference code is illustrative skeleton with placeholders. Wiring it to real infrastructure is the part you (and your agent) do.

License

MIT — see LICENSE.

About

Blueprint: run Claude Code headless on a VPS — Telegram bridge + overnight autonomous research. Architecture + hard-won gotchas, build-it-yourself with your own agent.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages