Claude Code in Production: VPS, Hooks, Timers, and the Stack Beyond Your Laptop
The Short Version
- The constraint: Claude Code runs on your machine. Close the lid, everything stops. No overnight automation, no weekend monitoring, no 5 AM reports.
- The fix: A $15/month VPS gives Claude Code a permanent address. OAuth authentication, Syncthing for vault sync, systemd timers for scheduling.
- The multiplier: Hooks inject context before every interaction. Timers fire tasks on schedules you define. Telegram puts the whole system in your pocket.
- The guardrail: Autonomous AI touching production data demands safety gates. Dry-run defaults, global write blocks, 7 hard rules.
The Laptop Problem
You've built a CLAUDE.md file. Your AI knows your business, your clients, your voice. During the workday, it's exceptional. You issue commands, it executes with full context, and the output lands clean.
Then you close your laptop.
A lead submits a form at 2 AM. Nobody triages it until 9. A CRM audit should run every morning before you sit down, but it waits for you to open a terminal and type the command. Your Obsidian vault's semantic index drifts stale because reindexing only happens when you remember to trigger it. Weekend monitoring doesn't exist because the machine that runs the intelligence is sitting in a bag.
This is the gap between a powerful AI assistant and an AI operator. The assistant responds when prompted. The operator runs independent of your physical presence. The knowledge architecture you've built — the context files, the domain routing, the memory layer — all of it becomes a daytime-only resource if it depends on an open laptop.
The fix involves four layers: a server that stays on, hooks that inject intelligence, timers that trigger work, and a messaging interface that connects it all to your phone.
The VPS Solution
A virtual private server is a machine in a data center that runs 24/7. For Claude Code production deployment, a Hetzner CAX21 (ARM64, 4 vCPU, 8GB RAM) costs roughly $8-15/month. That's sufficient for Claude Code, Syncthing, a SQLite database, and several concurrent automation processes.
The setup sequence:
1. Provision the server. Hetzner's cloud console provisions an ARM64 Ubuntu instance in under two minutes. SSH in, update packages, create a non-root user.
2. Install Claude Code. Standard npm install. The critical step is authentication — you don't want to babysit an interactive login on a headless server.
# Install Claude Code globally
npm install -g @anthropic-ai/claude-code
# Authenticate via OAuth (generates a long-lived token)
claude login --method oauth
# Store the token for automated sessions
echo "CLAUDE_CODE_OAUTH_TOKEN=your-token-here" >> ~/.env.automation
The OAuth token persists across sessions and survives reboots. Store it in a dotfile that your automation scripts source. The token's expiry is roughly one year — calendar a renewal reminder.
3. Sync your vault. Syncthing creates an encrypted, peer-to-peer sync between your laptop's Obsidian vault and the VPS copy. No cloud provider in the middle. Changes propagate in seconds. Your VPS always has the current state of every context file, every domain document, every piece of operational memory.
# Install Syncthing on VPS
apt install syncthing
# Enable as user service
systemctl --user enable syncthing
systemctl --user start syncthing
# Configure via web UI at localhost:8384
# Add your laptop as a peer, share your vault folder
4. Clone your project directory. Your .claude/ configuration, your hooks, your scripts — all of it mirrors to the server. Claude Code on the VPS reads the same CLAUDE.md, the same hooks configuration, the same operational context as your laptop instance.
The result: two identical Claude Code environments. One travels with you. One never sleeps.
Hooks: Automatic Context Injection
Hooks are the mechanism that separates a chatbot from an operator. They're scripts that execute at specific points in Claude Code's lifecycle — before a session starts, before a tool runs, after a response generates. They intercept, enrich, and constrain.
Three hooks transform the baseline experience:
The Domain Router
Every prompt carries signals about what domain it belongs to. A message mentioning "FUB" or "smart list" routes to real estate CRM context. "Topical map" or "retainer" routes to SEO operations. "Budget" or "rabbitry" routes to personal operations. The domain router detects these keywords and loads the relevant _context.md file before Claude Code processes the prompt.
// .claude/hooks/domain-router.js
// Hook type: PreToolUse (fires before Claude reads any file)
const DOMAIN_MAP = {
JAG: {
triggers: ["fub", "follow up boss", "mojo", "phone duty",
"smart list", "gtz", "nurture", "hot prospect"],
context: "02 - JAG/_context.md"
},
SWS: {
triggers: ["seo", "topical map", "retainer", "cryo",
"dispatch", "deliver client"],
context: "03 - SWS/_context.md"
},
BUILD: {
triggers: ["course", "catalog", "module", "skill audit",
"observer", "hook"],
context: "01 - BUILD/_context.md"
}
};
function route(prompt) {
const lower = prompt.toLowerCase();
for (const [domain, config] of Object.entries(DOMAIN_MAP)) {
if (config.triggers.some(t => lower.includes(t))) {
return { domain, file: config.context };
}
}
return null;
}
Without this hook, you'd prepend "load my JAG context" to every real estate prompt. With it, context arrives silently. The AI already knows which domain it's operating in before it generates its first token.
The Semantic Memory Hook
Your vault contains thousands of files. No context window holds them all simultaneously. The semantic memory hook runs a vector search against your vault's QMD index before every tool use, surfacing the 3-5 most relevant documents for the current task. Claude Code reads those files as additional grounding, not because you told it to, but because the hook determined they were relevant.
This is the difference between an AI that knows what you told it today and an AI that recalls what you wrote six months ago about the same client.
The Voice Calibration Hook
When Claude Code drafts content — emails, proposals, articles — it defaults to its own register. The voice calibration hook intercepts content generation tasks and injects your writing samples, your style directives, your vocabulary preferences. The output reads like you wrote it because the hook loaded examples of how you actually write.
Hooks compose. The domain router fires, loads JAG context. The semantic memory hook fires, surfaces the relevant client file from six weeks ago. The voice calibration hook fires, loads your email style. All three execute in sequence before Claude Code generates a single word. The human experience: you typed a prompt. The machine experience: three intelligence layers activated, eight files loaded, context assembled from three different retrieval methods.
Timers: Scheduled Automation
Hooks respond to interaction. Timers create interaction. They're the mechanism for work that should happen whether or not you're present.
On macOS, launchd manages scheduled tasks. On the VPS (Linux), systemd timers serve the same function with more granular control. A production deployment typically runs both — local timers for tasks that need your machine's resources, VPS timers for tasks that run headless.
A systemd timer consists of two files: a .timer that defines the schedule and a .service that defines the work.
# /etc/systemd/system/morning-report.timer
[Unit]
Description=Generate morning operations report
[Timer]
OnCalendar=Mon..Fri *-*-* 05:00:00
Persistent=true
[Install]
WantedBy=timers.target
# /etc/systemd/system/morning-report.service
[Unit]
Description=Morning report via Claude Code
[Service]
Type=oneshot
User=operator
EnvironmentFile=/home/operator/.env.automation
WorkingDirectory=/home/operator/vault
ExecStart=/usr/local/bin/claude -p "Generate the morning operations report. Check FUB for overnight leads, audit the CRM pipeline, summarize any Telegram messages received after 10 PM. Output to 02 - JAG/Reports/morning-%Y.%m.%d.md"
TimeoutStartSec=300
Enable and start:
systemctl daemon-reload
systemctl enable morning-report.timer
systemctl start morning-report.timer
# Verify it's scheduled
systemctl list-timers | grep morning
A production stack might run 15-25 timers across the two machines. Some examples from a real deployment:
| Timer | Schedule | What It Does |
|---|---|---|
| Morning Report | 5:00 AM weekdays | CRM audit, overnight lead triage, pipeline summary |
| QMD Reindex | Every hour | Re-embeds changed vault files for semantic search |
| FUB Compliance | 10:00 AM weekdays | Checks agent response times, flags overdue leads |
| Vault Backup | 2:00 AM daily | Encrypted snapshot to offsite storage |
| Stale Context Sweep | Sunday 8:00 PM | Identifies _context.md files not updated in 14+ days |
| Transcript Capture | 6:00 AM daily | Pulls new podcast transcripts, extracts sources and data |
| Session Ledger Prune | 1st of month | Archives sessions older than 90 days to cold storage |
Each timer is a text file. Version-controlled, auditable, reproducible. If the VPS dies, you provision a new one, clone the repo, run systemctl daemon-reload, and every scheduled task resumes. No GUI dashboards, no vendor lock-in, no "where did I configure that automation" archaeology.
Telegram: Your AI in Your Pocket
A VPS running timers generates output. You need to see that output without SSH-ing into a server. Telegram bridges the gap — it's a messaging app with a bot API robust enough to serve as a production notification and interaction layer.
The architecture: a Telegram bot runs on the VPS, connected to Claude Code via a Python wrapper. You send a voice message or text from your phone. The bot transcribes it (Telegram handles voice-to-text natively for Premium users), passes it to Claude Code with full vault context, and returns the response in the same chat.
What this looks like in practice:
You (voice message, 7:15 AM, driving to work): "What leads came in overnight and which ones need callbacks before 9?"
Bot: 3 new leads since 10 PM. Sarah Chen (ID: 45892) — $450K budget, Cary preferred, submitted via Zillow at 11:43 PM. Mark Torres (ID: 45893) — investor inquiry, no budget listed, web form at 2:17 AM. Diana Okafor (ID: 45894) — pre-approved, $380K, Raleigh downtown, Realtor.com at 6:02 AM. Chen and Okafor are hot — both have lender letters. Torres can wait for the 10 AM block. I've queued callback reminders for Chen and Okafor in FUB.
Session continuity matters. The bot maintains a 2-hour resume window — if you send another message within two hours, it continues the same Claude Code session with the same context. After two hours of silence, the next message starts a fresh session with a clean context load. This prevents stale context from compounding over days while preserving conversational flow within a working block.
Quiet hours prevent the bot from pushing non-urgent notifications between 10 PM and 6 AM. Timer outputs during quiet hours queue silently and deliver as a batch at 6:01 AM. Urgent alerts (lead marked hot, system error, authentication failure) bypass quiet hours.
A multi-bot architecture scales this further. Separate bots for separate domains — a JAG bot for real estate operations, an SWS bot for SEO client work, a VVR bot for personal operations. Each bot carries its own token, its own context scope, its own notification rules. You don't get real estate lead alerts in the same thread as grocery budget updates.
Safety Gates
Everything described so far amounts to giving an AI autonomous access to production systems. Your CRM. Your client data. Your vault. Your scheduled operations. Without constraints, this is a liability, not an asset.
Seven rules govern autonomous operations:
- Dry-run by default. Any operation that writes to an external system (CRM update, email send, file deletion) executes in preview mode first. The output describes what would change, with exact field values and record IDs. Actual execution requires either explicit confirmation or a pre-approved automation rule.
- Global write blocks on sensitive systems. The CRM deals pipeline, for example, is blocked globally. No scoped lifts, no batch execution. The AI generates a dry-run report showing what it would do. A human reviews and executes. The block exists because a single malformed API call can reassign a $500K deal.
- Audit trail for every automated action. Every timer run, every hook execution, every Telegram interaction logs to a session ledger — a SQLite database with full-text search. 37,000+ messages indexed. When something goes wrong, you reconstruct the exact sequence.
- Confirmation gates for bulk operations. Any action touching 3 or more files requires stating the plan and waiting for approval. No silent bulk edits. The AI describes what it intends to modify, you confirm, then it executes.
- No inference on ambiguous data. If a contact name appears in multiple contexts, the AI asks for disambiguation rather than guessing. If a deal stage seems wrong, it flags rather than corrects. The rule: when uncertain, report; don't resolve.
- Token-scoped permissions. The OAuth token on the VPS has defined scope. It can read files, run searches, execute approved scripts. It cannot push to Git remotes, modify system configurations, or access credentials outside its environment file.
- Corrections propagate permanently. When an error is identified and corrected, the fix applies everywhere — every file, every reference, every cached value. No partial corrections. No "I'll fix it next time." The correction is the final state.
The philosophy: automation speed with human judgment on irreversible actions. The AI handles the 90% of operations that are read-only or safely reversible. The remaining 10% — the writes that matter — route through a human checkpoint. The system gets faster over time as you approve patterns that become standing rules, but the default is always caution.
The Stack, Assembled
Laid out end to end, the production deployment looks like this:
Layer 1: Knowledge. Your Obsidian vault — context files, domain documents, CRM records, session history. Syncthing keeps it synchronized between your laptop and VPS. QMD indexes it for semantic retrieval. This is the memory.
Layer 2: Intelligence. Claude Code, running on both machines. CLAUDE.md provides identity and rules. Hooks inject domain context, semantic memory, and voice calibration before every interaction. This is the reasoning.
Layer 3: Automation. Systemd timers on the VPS, launchd on your Mac. Scheduled tasks that generate reports, run audits, reindex data, capture transcripts. The intelligence operates on a clock, not just on demand. This is the initiative.
Layer 4: Interface. Telegram bots for mobile access. Voice in, grounded response out. Session continuity, quiet hours, domain-scoped channels. Your laptop can be closed, your phone still connects you to the full stack. This is the reach.
Layer 5: Safety. Dry-run defaults, global write blocks, audit logging, bulk confirmation gates, disambiguation rules, scoped permissions, permanent corrections. Every automated action is bounded. This is the trust.
Each layer depends on the one below it. Without knowledge, the intelligence hallucinates. Without intelligence, automation is just cron jobs. Without automation, you're still the bottleneck. Without the interface, you lose access when you leave your desk. Without safety, the whole thing is a loaded weapon.
The total cost: $15/month for the VPS, $20/month for Claude API usage on scheduled tasks (varies with volume), $0 for Syncthing, $0 for systemd, $5/month for Telegram Premium. Roughly $40/month for an AI operator that works while you sleep, drives, and takes weekends off.
Frequently Asked Questions
How much does it cost to run Claude Code on a VPS?
A Hetzner CAX21 (ARM64, 4 vCPU, 8GB RAM) runs about $8-15/month. Claude Code API usage is the larger cost — expect $50-200/month depending on how many automated tasks you schedule. The VPS itself is cheap. The intelligence running on it scales with usage.
Is it safe to give an AI autonomous access to production systems?
Not without guardrails. The pattern that works: dry-run by default for any destructive operation. Every write to a CRM, every file deletion, every API call that changes state — runs in preview mode first, reports what it would do, and waits for confirmation or a pre-approved rule. You build the safety into the hooks, not the hopes.
Can I use Claude Code hooks without a VPS?
Yes. Hooks run locally on your machine whenever Claude Code is active. The VPS only matters when you want automation to fire while your laptop is closed — scheduled reports, overnight monitoring, webhook responses. If you only need smarter context loading during interactive sessions, hooks alone get you most of the value.
Move Claude Code off your laptop.
Phase 0 gets your foundation right — the context file, the vault, the hooks. The agent layer adds the VPS, the timers, and the Telegram bridge. Your AI stops being a tool you open and becomes infrastructure that runs.
Start with Phase 0 Setup