Bootstrap a VPS to run Claude Code with the Telegram plugin and the KAppMaker CLI skill. Once set up, you can drive your KAppMaker workflows (create apps, generate logos, configure stores, build & publish Android releases) from Telegram on your phone.
- Quick start — one-command bootstrap of a fresh VPS
- Securing the VPS — do this first: Tailscale, UFW, SSH lockdown
- What gets installed — the toolchain the script sets up
- Post-install — non-root user, login to Claude, plugins, Telegram
- GitHub authentication — dedicated bot account & SSH key
- Web previews — public URLs for Wasm/JS builds
- Using it from Telegram — example commands & memory
- Self-improving dev loop — opt-in autonomous improvement loop
- Working with Claude Code effectively — high-value habits & best practices
- Recommended Claude Code skills — bundled + optional skills worth adding
- Limitations · Architecture · Troubleshooting
Run the bootstrap as a non-root sudo user — not root. The script installs per-user (a multi-GB Android SDK, ~/.bashrc env, skills, the loop template all go into the running user's home), and the bot needs --dangerously-skip-permissions, which Claude refuses as root. The script does not create a user for you, so on a fresh VPS where you land as root, create one first:
sudo adduser devuser && sudo usermod -aG sudo devuser
su - devuserThen run the bootstrap as that user (run it once, as this user — running it as root first just makes a wasted second copy):
curl -fsSL https://raw.githubusercontent.com/KAppMaker/KAppMakerDeveloperBot/main/setup-vps.sh | bashOr, if you'd rather review first:
wget https://raw.githubusercontent.com/KAppMaker/KAppMakerDeveloperBot/main/setup-vps.sh
less setup-vps.sh
bash setup-vps.shThe script is idempotent — re-running it (as the same user) skips anything already installed. SSH-key login, UFW, Tailscale, and passwordless sudo come next in Securing the VPS.
A VPS is a computer wired directly to the entire internet — ~8 billion people can knock on its door and try to get in. Treat it that way. You want it reachable only by you (and, if you host a public site, only by Cloudflare in front of it). This matters even more here because you may run Claude with --dangerously-skip-permissions for hands-off Telegram/loop use — so the box itself must be locked down to just you.
The model (battle-tested by folks running real apps on their own infra — see @levelsio's VPS-lockdown tweet):
- SSH only over Tailscale — put the server on a private mesh network and make that the only way in. No public SSH surface to brute-force.
- Default-deny firewall (UFW) — block all inbound, then open only what you truly need.
- Cloudflare in front of any public web — if (and only if) you serve a website, allow inbound
443from Cloudflare IPs only, never from the open internet. (The bundledpreviewhelper uses outbound Cloudflare quick tunnels, so it needs no inbound web rule.)
| ✅ | Hardening | Why |
|---|---|---|
| ☐ | SSH key-only auth (PasswordAuthentication no) |
Kills password brute-force |
| ☐ | Root login off (PermitRootLogin no) + a non-root sudo user (see Post-install) |
No direct root attack surface |
| ☐ | UFW on, default-deny inbound | Closed by default, open by exception |
| ☐ | SSH locked to Tailscale | Public :22 is never exposed |
| ☐ | Docker ports bound to 127.0.0.1 |
Docker bypasses UFW via iptables — bind explicitly |
| ☐ | Unattended security upgrades | Auto-patch known CVEs |
| ☐ | fail2ban | Bans repeat offenders |
| ☐ | Tested backups | A backup you've never restored is a wish, not a backup |
-
Install Tailscale and join your tailnet (do this before touching the firewall, so you don't lock yourself out):
curl -fsSL https://tailscale.com/install.sh | sh sudo tailscale upConfirm you can SSH in over the Tailscale IP (
100.x.y.z) from your laptop. -
Lock the firewall to Tailscale-only SSH:
sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw allow in on tailscale0 to any port 22 proto tcp # SSH only over Tailscale sudo ufw enable
⚠️ Keep your provider's web console / rescue session open until you've confirmed Tailscale SSH works — otherwise a bad rule can lock you out. Only after that, remove any publicallow 22rule. -
(Only if hosting a public website) allow
443from Cloudflare IPs only:for ip in $(curl -s https://www.cloudflare.com/ips-v4); do sudo ufw allow from "$ip" to any port 443 proto tcp; done for ip in $(curl -s https://www.cloudflare.com/ips-v6); do sudo ufw allow from "$ip" to any port 443 proto tcp; done
Point your domain through Cloudflare (orange-cloud / proxied) so Cloudflare stands in front and absorbs attacks. Never open
443/80to0.0.0.0/0.
There's a community Claude Code skill that does all of the above interactively — SSH lockdown, UFW, Tailscale, fail2ban, unattended-upgrades, Docker port binding, and backup guidance — in a few minutes:
Hardening skill: https://gist.github.com/burakeregar/5b8a7bca382ae43342db30f3c04788fc
Save it to
~/.claude/commands/vps-setup.mdon the VPS, then ask Claude (e.g. "run the full VPS setup" / "harden ssh") and it walks you through the rest. Review what it changes before applying — you're handing it root.
| Component | Version | Purpose |
|---|---|---|
git, curl, tmux, unzip, build-essential |
latest | Base tooling |
python3, pip, venv |
system | Used by some KAppMaker tools |
| Temurin JDK | 17 | Required for Android Gradle Plugin / KMP |
Android SDK cmdline-tools + platforms;android-34 + build-tools;34.0.0 |
latest | Build & sign APK/AAB on the VPS |
| Gradle | 9.4.1 | Standalone Gradle (project wrappers override this) |
| Node.js | 22 | Runtime for Claude Code |
| Bun | latest | Required by the Telegram plugin |
| Claude Code | latest | @anthropic-ai/claude-code global npm |
| KAppMaker CLI | latest | kappmaker global npm — used by the kappmaker plugin |
GitHub CLI (gh) |
latest | Push generated app repos to GitHub |
cloudflared |
latest | Cloudflare quick tunnels for web (Wasm/JS) preview URLs |
preview / preview-stop |
bundled | Helper scripts in ~/bin that wrap cloudflared for one-command preview links |
kapp-loop-install + loop template |
bundled | Per-app self-improving dev loop scaffold (opt-in, off by default) — see Self-improving dev loop |
kapp-service-install |
bundled | Opt-in installer for the always-on Claude+Telegram systemd service (auto-restart + start on boot) |
caveman + ui-ux-pro-max skills |
bundled | Global Claude Code skills: terse output (token thrift) + UI/UX design intelligence (Compose/SwiftUI) — see Recommended Claude Code skills |
Environment variables (JAVA_HOME, ANDROID_SDK_ROOT, ANDROID_HOME, BUN_INSTALL, PATH) are persisted to ~/.bashrc in a marked block.
A ~/projects/ directory is created with two workspace files:
CLAUDE.md— workspace-wide rules: project switching ("switch to fittracker"), project lifecycle, kappmaker-first workflow, build previews, asset attachment, safety confirmations, and Telegram output style.MEMORY.md— user-controlled persistent memory. Empty by default; you populate it via Telegram with messages like "remember: all new repos should be private" / "forget X" / "what do you remember". Claude reads it before every meaningful task and respects it (memory entries override CLAUDE.md defaults when they conflict).
Each app you create lives in its own subdirectory and can have its own CLAUDE.md for project-specific rules.
The script prints these steps when it finishes; they can't be automated.
Note: the bootstrap script runs
kappmaker config initinteractively at the end. If for any reason it was skipped (e.g. no TTY), run it manually before doing anything else:kappmaker config init. Docs: https://cli.kappmaker.com/.
Don't run the bot as root: it's a security risk, and Claude Code refuses --dangerously-skip-permissions when running as root — and you'll want that flag for hands-off Telegram / loop operation (see step 6). Create a normal user with sudo and do everything below as that user:
sudo adduser devuser && sudo usermod -aG sudo devuser
su - devuserEach user has its own env, so create this user before running the bootstrap (see Quick start) and run the script once, as this user — that's the clean path. If you already ran it as root, just re-run it as devuser (idempotent); the root copy is harmless but redundant — you can reclaim the disk with sudo rm -rf /root/android-sdk /root/.bun /root/projects once devuser is set up.
The login password prompt is brute-forceable — switch this user to SSH key auth. From your laptop, push your public key:
ssh-copy-id devuser@<server-ip>
# no ssh-copy-id? →
# cat ~/.ssh/id_ed25519.pub | ssh devuser@<server-ip> 'mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys'Verify you can ssh devuser@<server-ip> without a password. Only then, turn off password + root login globally:
sudo sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sudo sshd -t && sudo systemctl restart ssh # sshd -t must pass first
⚠️ Keep your current SSH session and your provider's web console open until a fresh key-based login works — a badsshd_configcan lock you out.
For unattended operation you may not want sudo to prompt for a password. Add a validated drop-in (never edit /etc/sudoers directly):
echo "devuser ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/devuser
sudo chmod 440 /etc/sudoers.d/devuser
sudo visudo -c # must print "parsed OK"
⚠️ Tradeoff: passwordless sudo +--dangerously-skip-permissionsmeans the agent effectively is root. That's only acceptable because the box is locked to you (SSH key-only + Tailscale + root login off). If you'd rather keep a safety line, skip this — day-to-day kappmaker/gradle/claude work needs nosudoat all; only the one-time setup does.
-
Reload shell
source ~/.bashrc
-
Log into Claude with your Pro/Max subscription
cd ~/projects claude
Open the printed URL in your laptop browser, paste the auth code back. Always start Claude from
~/projectsso the workspace CLAUDE.md is loaded. -
Install plugins (inside Claude)
KAppMaker skill — natural-language access to the
kappmakerCLI (guide):/plugin marketplace add KAppMaker/KAppMaker-CLI /plugin install kappmaker@KAppMaker-CLIAlternatively, outside Claude:
npx skills add KAppMaker/KAppMaker-CLI --skill kappmakerTelegram channel plugin (official README):
/plugin install telegram@claude-plugins-official /reload-plugins -
Configure Telegram — pass your BotFather token inline
/telegram:configure 123456789:AAHfiqksKZ8...This writes
TELEGRAM_BOT_TOKEN=...to~/.claude/channels/telegram/.env. -
Pair your Telegram account. DM your bot on Telegram — it replies with a 6-character pairing code. Back in the Claude session:
/telegram:access pair <code>Then lock it down so only you can reach the bot:
/telegram:access policy allowlist -
Run inside tmux with the Telegram channel active so Claude listens for your bot messages and survives SSH disconnect
tmux new -s claude cd ~/projects && claude --channels plugin:telegram@claude-plugins-official
Detach:
Ctrl+BthenD· Reattach:tmux attach -t claudeImportant: plain
claude(without--channels) starts a normal interactive session and does not listen on Telegram. The--channelsflag is what opens the listener.Hands-off mode (no permission prompts). For unattended Telegram use — and for the self-improving dev loop to run without stopping to ask — add
--dangerously-skip-permissionsso Claude runs tools without prompting:cd ~/projects && claude --channels plugin:telegram@claude-plugins-official --dangerously-skip-permissions
This flag only works as a non-root user (see the note above). It removes the per-action approval prompts, so only use it on a VPS you control. The loop scaffold's
no-touchdeny-list (secrets, keystores,**/build/**, CI workflows) is still a guardrail, but treat skip-permissions as full trust in the agent.Or run it always-on (recommended over plain tmux). A tmux session you start by hand only survives SSH disconnect — it does not restart Claude if the process is killed (e.g. an out-of-memory kill during a build) or if the box reboots. To make the bot truly always-on:
kapp-service-install
By default this runs Claude inside an attachable tmux session managed by systemd — so you keep the live view you're used to and get auto-restart + start-on-boot:
tmux attach -t claude # watch it work live; detach with Ctrl+B then DIt auto-restarts within ~15s if Claude dies and starts on boot. Prefer no tmux at all?
kapp-service-install --headlessruns Claude directly under systemd (strongest supervision; watch withjournalctl -fu claude-telegram). Either way, don't also runclaudeby hand in your own tmux — two sessions polling the same bot token conflict. (Cloud-provisioned boxes get the headless service automatically viaprovision/bootstrap.sh.) -
Log into GitHub CLI for app repo pushes
gh auth login
Use the dedicated bot account from the GitHub authentication section below — don't use your personal account on the VPS.
A VPS can be compromised. If your personal GitHub credentials live on it, an attacker gets your private repos, can force-push to main, etc. Use a dedicated machine user account instead — fully isolated, easy to revoke.
-
Create a separate GitHub account for the bot (free). Use a
+alias on your email so it lands in your inbox:you+kappmakerbot@gmail.com→ register at https://github.com/signup. Enable 2FA on it (separate authenticator entry from your personal one). -
Add it to your
KAppMakerorg with the Member role (browser):- Org → People → Invite member
- Repos it needs to push to → Manage access → grant write
-
Generate an SSH key on the VPS for this account:
ssh-keygen -t ed25519 -C "kappmaker-bot-vps" -f ~/.ssh/kappmaker_bot cat ~/.ssh/kappmaker_bot.pub
Copy the printed public key.
-
Add the public key to the bot account (browser, logged in as the bot): https://github.com/settings/keys → New SSH key. Optional hardening: if your VPS has a static IP, prefix the key with
from="1.2.3.4"so it only works from that IP. -
Tell SSH to use this key for github.com (on the VPS):
cat >> ~/.ssh/config <<'EOF' Host github.com HostName github.com User git IdentityFile ~/.ssh/kappmaker_bot IdentitiesOnly yes EOF chmod 600 ~/.ssh/config
-
Set the git identity so commits are attributed to the bot, not
root@vps:git config --global user.name "KAppMaker Bot" git config --global user.email "you+kappmakerbot@gmail.com"
-
Verify:
ssh -T git@github.com # → "Hi kappmaker-bot! You've successfully authenticated..." -
Now log into
gh(step 7 above) — choose SSH and point at the same key file.ghwill use it for HTTPS API calls and for git push.
| Scenario | With personal creds | With bot account |
|---|---|---|
| VPS gets root-level compromise | Attacker gets all your private repos, can force-push, delete branches | Attacker only gets repos in the KAppMaker org that bot has write to |
| Bot key leaks | Have to rotate personal key (affects all your machines) | Revoke one key on one account, done |
| You stop using the VPS | Need to remember to revoke the key | Just delete the bot account or remove from org |
When kappmaker builds the web target, the output is just static files — but you're on your phone, so the script bundles a preview helper that gives you a public URL via Cloudflare Tunnel.
# After: ./gradlew :webApp:jsBrowserDistribution
preview ~/projects/<app>/MobileApp/webApp/build/dist/js/productionExecutable
# → prints e.g. https://random-words-here.trycloudflare.comOpen the URL on your phone and you're previewing your KMP web build. When you're done:
preview-stop # stop the default-port preview
preview-stop --all # stop everythingClaude knows this workflow — just ask via Telegram: "build webapp for fittracker and send me the preview link" and it'll run the build, start the tunnel, and reply with the URL.
Tunnel notes:
- URL changes every time the tunnel restarts (it's a free Cloudflare quick tunnel — no account needed)
- Tunnel only lives while
cloudflaredis running on the VPS - For a permanent URL, set up a named Cloudflare Tunnel against your own domain — out of scope here, but the same
cloudflaredbinary supports it
Once paired, message your bot. The kappmaker skill auto-loads when your prompt matches its triggers. Examples:
Create a new app called FitTracker for fitness loggingGenerate a logo for FitTrackerBuild the Android releasePublish to Play Store internal testingBump version to 1.2.0
Memory commands — teach Claude your preferences once, they stick across sessions:
Remember: all new GitHub repos should be privateRemember: use MIT license by defaultWhat do you remember?— Claude shows the contents of~/projects/MEMORY.mdForget the MIT license preference
An opt-in autonomous loop that improves an app one small, verified change at a time. It plans the work, implements the top item, spins up specialist sub-agents to critique the change, applies the worthwhile suggestions, runs a real Gradle gate, and only then checks the item off — repeating until the plan is done or you say stop. Its default goal is conversion (free→paid subscriptions + credit-pack purchases), reviewed ethically (it refuses dark patterns).
It is not installed in apps automatically and never runs until you trigger it with a plain message — the same whether you're in the terminal or on Telegram. There are no slash commands.
This is our take on the Ralph technique — the same engine Anthropic's
ralph-wiggumplugin uses: a Stop hook that re-feeds the turn until the work is done. On top of that primitive we add a real Gradle verification gate (it never checks a box on a red build), aPLAN.mdchecklist as the completion signal (instead of ralph's exact-string--completion-promise), parallel specialist reviews, ethics guardrails, and plain-language triggers that work over Telegram (instead of/ralph-loop, which doesn't). So the "no slash commands" above is a deliberate divergence from ralph, not a gap.
cd ~/projects/<app>
kapp-loop-installThis drops the scaffold into the app: workflow guide in AiGuidelines/loop/, specialist sub-agents
in .claude/agents/, helper scripts in scripts/, a gated Stop hook in .claude/settings.json,
and a run-output dir .loop/. It also appends a short rules block to the app's CLAUDE.md. Your
existing config is never clobbered (if .claude/settings.json already exists it writes
settings.loop.json for you to merge).
On the VPS the template is pre-deployed to
~/projects/.loop-template/bysetup-vps.sh, sokapp-loop-installjust works. Nothing changes about a normal session until you start the loop.
Just describe the goal and tell it to keep going. Examples (terminal or Telegram):
- "improve the onboarding conversion and keep going until it's done"
- "start the self-improve loop on the paywall"
- "run the dev loop — focus on the credit-pack purchase flow"
- "work on first-run activation autonomously until the plan is complete"
On start it takes a git checkpoint, seeds PLAN.md from the goal, raises the loop flag, and begins
the first item.
- "stop" · "pause the loop" · "that's enough for now"
The loop also stops automatically when the plan is complete, when a build/test fails (it never checks a box on a red build), or when it hits the iteration cap (25 by default).
PLAN.md— the living checklist (✅ done / ☐ remaining), grouped by milestone..loop/decisions.md— why each specialist suggestion was accepted or rejected..loop/reviews/— each specialist's structured critique per iteration..loop/reports/run-<timestamp>.md— a final report when the run finishes. If you started the loop from Telegram, it also sends you a concise summary message.
Each reviews against the app's own AiGuidelines/ and recommends only — code is written by the
orchestrator alone:
| Sub-agent | Focus |
|---|---|
orchestrator |
Owns PLAN.md, delegates, decides what to apply, enforces the gate |
onboarding-specialist |
First-run activation, time-to-value, signup/permission friction |
paywall-conversion-specialist |
Subscription + credit-pack conversion, trial framing, pricing (ethical) |
ui-ux-reviewer |
Compose UI: tap targets, accessibility, dark mode, snapshot impact |
qa-engineer |
Kotlin/coroutine correctness, edge cases, missing tests, build safety |
You: switch to fittracker
Bot: Switched to fittracker.
You: install the self-improve loop here
Bot: Done — loop installed, off by default.
You: improve the onboarding conversion and keep going until it's done
Bot: ⏳ Loop started. Seeded PLAN.md (3 milestones, 11 items). Working item 1…
…(iterates: implement → review → verify → check off)…
Bot: done — completed 11/11 items. Report: .loop/reports/run-2026-05-29T1430.md
Highlights: chose OnBoarding variation 2, moved paywall after first value moment,
fixed 2 a11y issues. All gates green.
You: stop
Bot: Loop stopped. Committed work is preserved.
The full workflow rules live in each app at AiGuidelines/loop/SELF_IMPROVE_LOOP.md.
A few high-value habits that make the bot (and the loop) produce better work. These are distilled from shanraisshan/claude-code-best-practice — see it for the full list.
- Plan before you build. For anything non-trivial, ask it to plan first (research → plan → execute → review) instead of jumping straight to edits.
- Keep
CLAUDE.mdlean (aim under ~200 lines). Push detail into theAiGuidelines/files — the loop and specialists already read those. - Lean on specialist sub-agents. Delegating review/research to focused agents keeps the main context clean and the answers sharper (the loop does this automatically).
- Verification is law, not vibes. Trust the build/test gate over "looks done" — the loop never checks a box on a red build, and you should hold normal sessions to the same bar.
- Mind context hygiene. Start a fresh session for an unrelated task rather than letting one thread sprawl.
The mobile-monetization gap (onboarding, paywalls, app-store work) is already covered by this repo's self-improving dev loop specialists and the kappmaker skill. The third-party skills worth adding are about UI/UX craft, token thrift, Kotlin code intelligence, and general dev workflow.
These two are installed globally (~/.claude/skills/) so they're available in every session. Skip them with KAPP_SKIP_SKILLS=1.
- caveman — forces terse, stripped-down output (~65% fewer output tokens). A great fit here: you read replies on a phone via Telegram and run on a Claude subscription. Toggle with
/caveman [lite|full|ultra], back with "normal mode". - ui-ux-pro-max — design intelligence with Jetpack Compose / SwiftUI support (UI styles, palettes, font pairings, UX guidelines, anti-pattern avoidance). Auto-activates on UI/design requests; amplifies the onboarding/paywall work.
- Kotlin code intelligence —
/plugin install kotlin-lsp@claude-plugins-official(the official marketplace is built-in), plus thekotlin-language-serverbinary onPATH. Gives Claude real type errors + jump-to-definition on the KMP/Compose code. Caveat: the language server is memory-hungry — mind it on a small box (see the OOM note). - General workflow skills from mattpocock/skills —
npx skills add mattpocock/skills, then pick:- prototype — throwaway prototypes to validate an onboarding/paywall idea before building it.
- handoff — condense a session into a transfer doc (pairs well with the restart/session-recovery behavior).
- git-guardrails — pre-execution hooks that block dangerous git ops — useful on an autonomous
--dangerously-skip-permissionsbox.
Trust + cost: skills run with full trust (arbitrary code) on a skip-permissions box, and each installed skill adds to the context window every turn. Install deliberately, from sources you trust, and pin versions where you can — don't bulk-install.
- iOS builds are not possible on a Linux VPS —
.ipabuilds need macOS/Xcode. App Store Connect metadata setup via kappmaker still works fine; only the actual iOS compile step is unavailable. - Subscription vs. API: Running Claude Code on a VPS is meant for your own interactive use via Telegram, not as a multi-user service or scripted automation pipeline. If you need always-on, multi-user, or scheduled automation, use an Anthropic API key instead.
┌─────────────┐ ┌──────────────────────────┐
│ You (📱) │ ──────► │ Telegram Bot (BotFather)│
└─────────────┘ └────────────┬─────────────┘
│ long-poll
▼
┌──────────────────────────┐
│ VPS (this repo's script)│
│ ┌────────────────────┐ │
│ │ Claude Code │ │
│ │ ├─ telegram plugin│ │
│ │ └─ kappmaker skill│ │
│ └────────────────────┘ │
│ + JDK 17, Android SDK, │
│ Gradle, Node, gh │
└──────────────────────────┘
- Adoptium repo fails on your distro — your Debian/Ubuntu codename may not be in their apt repo yet. Fall back to
sudo apt-get install -y openjdk-17-jdkand adjustJAVA_HOMEto/usr/lib/jvm/java-17-openjdk-${ARCH}. - Android cmdline-tools URL 404s — Google rotates the version number. Get the current link from https://developer.android.com/studio#command-line-tools-only and update
setup-vps.sh. - Telegram bot doesn't respond — check that bot privacy is set to Disable via
@BotFather→/setprivacy, otherwise the bot only sees commands. - Claude can't see plugins after install — restart the
claudesession. - My tmux/Claude session "randomly" dies during a build — it's almost always the Linux OOM killer, not a tmux limit. Android/Gradle builds are memory-hungry (the app ships
-Xmx4Gfor both the Gradle and Kotlin daemons), and with no swap the kernel kills the biggest process — sometimes Claude or the tmux server.setup-vps.shnow adds a swapfile and writes a VPS-sized~/.gradle/gradle.propertiesto prevent this; re-runsetup-vps.shon an existing box (it's idempotent) to apply. Confirm a past kill withsudo dmesg -T | grep -i "killed process". Also avoid running a build in tmux and the always-on bot at once — that doubles the memory pressure.