Step 0 · every project page starts here
Prompt Zero
From a machine with nothing installed — not even git — to a hardened server with Claude Code ready to work on it.
It takes two prompts: this page's, once — then one per app. That is the honest version of the promise. Anyone selling you "one prompt from nothing" is counting the setup as free.
Who this is for
You pay for software you'd rather run yourself. You can copy, paste, and read what comes back. You have never administered a server, and you do not want a career in it.
You will not need to know what a reverse proxy is by the end of this page. You will have one running, patched, and holding a certificate it renews without you. Set aside an evening for the first time through — most of it is waiting on a provider and reading two prices.
The honest bill
Before anything else, because it is the part that decides whether you keep going. Three lines, no bundle, no trial that turns into a subscription.
| What | Cost | Why it is approximate |
|---|---|---|
| Anthropic account, paid plan | from about $20/mo | Claude Code needs a paid plan or API billing. Currently from about $20/mo — check anthropic.com/pricing for today’s number. This is the one line that is not per app: the same plan installs all of them. Pricing page ↗ |
| VPS, Hetzner CX22-class | ~€4–6/mo | 2 vCPU, 4 GB RAM, 40 GB disk, billed in euros, plus a small charge for the IPv4 address. Destroy the server and the meter stops the same hour. Pricing page ↗ |
| A domain | ~$10/yr | One domain covers every app you ever run: notes.example.com, photos.example.com, invoices.example.com. It is the only annual commitment here. |
Vendor list prices, each in the currency that vendor bills in. They drift, so the pricing pages are linked rather than copied. No affiliate links anywhere on this site — the recommendations owe nobody anything.
Call it about $25 a month while you are learning, and notice where it goes: the agent is the biggest line, and it is not per app. Cancel the server and that charge stops the same hour. The domain is the only thing you are committed to for a year.
Set up your machine
Three branches, one per operating system. Open yours, ignore the others. All three end in the same place: a terminal you can find again, claude available in it, and an SSH key that exists.
One thing this page does not do: reprint the Claude Code install command. It changes, and this site's own rule bans piping an unpinned script from a web page into a shell. The current command lives on the Claude Code setup docs, which is also the only place it stays correct. Read it before you run it — that is the habit this whole site is trying to build.
macOS — Apple Silicon and Intel
- Open Terminal: press Command and Space, type
Terminal, press Return. To find it again later it lives in Applications, then Utilities. Drag it to your Dock now; you will want it tomorrow. - Install Claude Code by following the setup docs. Then run
claude --version. It must print a version number. Runclaudeonce and log in to your Anthropic account in the browser window it opens. - Make an SSH key:
ssh-keygen -t ed25519 -C "prompt-zero". Accept the default file location. Set a passphrase — this key is about to be the only credential to a server. If the command warns that~/.ssh/id_ed25519already exists, press Control-C and keep the key you have. - Load it into the agent so you type that passphrase once per boot:
ssh-add --apple-use-keychain ~/.ssh/id_ed25519. - Print the public half, which is the part you give away:
cat ~/.ssh/id_ed25519.pub. It is one line startingssh-ed25519. Copy it.
Apple Silicon and Intel take identical steps. The only difference you may trip over is Homebrew's location if you install it later, /opt/homebrew on Apple Silicon against /usr/local on Intel, which matters only when you follow instructions written for the other machine. Your laptop's architecture has nothing to do with the server's: the VPS is x86 either way, and the agent, not your CPU, is what talks to it.
Branch complete · claude --version prints a version · ~/.ssh/id_ed25519.pub exists
Windows — the WSL2 reality, stated plainly
Claude Code wants a Linux shell, and on Windows that means WSL2. This is the longest branch of the three and the one with reboots in it. Budget an hour, and read the trap at the end before you start: it is the one that costs people an afternoon.
- Open Windows Terminal or PowerShell as Administrator (right-click, Run as administrator) and run
wsl --install. Reboot when it asks. That is reboot one. - If it fails with a virtualization error, hardware virtualization is off in your firmware. You have to reboot into BIOS or UEFI to turn it on: the key is usually F2, F10, or Delete, pressed during the vendor logo, and the setting is called Intel VT-x, AMD-V, or SVM Mode depending on who made the board. Enable it, save, and let Windows come back up. That is reboot two, and a trip into a screen that looks nothing like Windows. It is normal, and it is the single most common reason this branch stalls.
- After the reboot, run
wsl --install -d Ubuntuif a distribution was not installed already, thenwsl -l -v. Ubuntu must be listed with VERSION 2. If it says 1, runwsl --set-version Ubuntu 2. - Open the Ubuntu shell (Start menu, type Ubuntu) and set your Linux username and password when it asks. Everything from here happens inside that shell, not in PowerShell.
- Install Claude Code inside Ubuntu by following the setup docs, then run
claude --version. Runclaudeonce and log in. - Make the key inside Ubuntu:
ssh-keygen -t ed25519 -C "prompt-zero", default location, with a passphrase. Theneval "$(ssh-agent -s)"andssh-add ~/.ssh/id_ed25519. WSL starts no agent for you, so this pair of commands belongs in your session each time, or in your~/.bashrc. - Print the public half:
cat ~/.ssh/id_ed25519.puband copy the line.
The trap. Windows has two separate filesystems and each has its own .ssh directory. A key made in PowerShell lands in C:\Users\you\.ssh\. A key made in Ubuntu lands in the Linux home directory, reachable from Windows as \\wsl.localhost\Ubuntu\home\you\.ssh\. They are not the same place and neither one can see the other's key. If you generate a key in PowerShell and then run the agent in WSL, ssh vps fails with a permission error that says nothing about the real cause. Decide once, at the start: everything happens inside WSL. Keys, ~/.ssh/config, the agent, the prompts. If you already made a key in PowerShell, make a second one in Ubuntu and use that; copying keys between the two filesystems drops the file permissions SSH insists on.
Branch complete · inside the Ubuntu shell: claude --version prints a version · ~/.ssh/id_ed25519.pub exists
Linux — the short one
- Open your terminal. Install Claude Code by following the setup docs, then run
claude --versionandclaudeonce to log in. ssh-keygen -t ed25519 -C "prompt-zero", default location, with a passphrase.- If your desktop is not already running an agent:
eval "$(ssh-agent -s)"thenssh-add ~/.ssh/id_ed25519. cat ~/.ssh/id_ed25519.puband copy the line.
Branch complete · claude --version prints a version · ~/.ssh/id_ed25519.pub exists
Get a VPS
A VPS is a computer you rent by the hour, in a building with better power and network than yours. You will destroy your first one, probably twice, and that is the correct use of it.
Hetzner — the default
Cheapest credible option for this, by a margin wide enough to matter at hobby scale. Take a CX22-class server with Ubuntu 24.04, in the location nearest to you. 4 GB of RAM clears the floor for most of this catalog. Do not take a 1 GB box to save two euros: it survives the install and dies during the first real import, and the failure looks random when it was decided at checkout.
Add your SSH key on the creation screen, in the SSH keys section, by pasting the ssh-ed25519 … line you copied. This is the whole reason there is no password in this guide. If you skip it, the provider emails you a root password and you are already in a worse position than this page assumes.
Two things about Hetzner that people meet the hard way, so meet them here instead:
- Identity checks. New accounts are sometimes asked to verify who they are, with a photo ID or a small card transaction, before the first server will boot. It is not a scam and it is not a rejection. It can take hours, occasionally into the next day. Start the signup before the evening you plan to build something.
- Outbound mail is blocked. Ports 25, 465 and 587 are closed on new accounts until you ask support to open them, which means anything that sends email — password resets, invitations, alerts — silently does not. Request the unblock on day one, in a support ticket, and treat the lead time as days. The rule is documented in Hetzner's cloud-server FAQ. Every prompt on this site is written so nothing depends on mail on day one.
DigitalOcean — the runner-up
Pick this one if the Hetzner signup stalls or asks for more than you want to give. The signup is smoother, the documentation is friendlier, and it costs a bit more for the same machine. Take a Basic droplet with Ubuntu 24.04 and at least 2 GB of RAM — the entry-level 1 GB droplet is under the floor for most of the catalog, and 1 GB of RAM is where the OOM killer lives. Same rule about the SSH key: add it on the creation page, not afterwards. Outbound mail is restricted on new accounts here too.
Both are named because they were the two that survived a signup-friction and price check, not because either pays us. There is nothing to disclose, which is the point of having nothing to disclose.
Make ssh vps work
Every prompt on this site says ssh vps and means it literally. On your own machine — in WSL if you are on Windows — create or open ~/.ssh/config and add this block, with the server's real IP address from the provider's panel in place of the example:
Host vps HostName 203.0.113.10 User root IdentityFile ~/.ssh/id_ed25519 IdentitiesOnly yes
Then chmod 600 ~/.ssh/config, and run ssh vps. It asks once whether you trust the host's fingerprint; type yes. You are now root on a server you have not hardened, which is exactly the state the next section is about.
203.0.113.10 is a documentation address reserved by RFC 5737. Nothing answers there. User root is correct for now and changes to your own account in block 5 of the prompt.
The permission truth
When Claude Code asks to run ssh vps and you approve it, you are giving the agent full control of that server. Not a subset. It can install, delete, reconfigure, reboot, and read every file on the box, including anything you later put there. That is the deal, and no allow-list in the tool makes it granular.
The boundary that actually matters is the one either side of it:
- It never controls your laptop. The agent runs on your machine and asks before it does anything there. Your files, your keys, your other accounts are on the far side of a prompt you answer every time.
- The server is disposable. You created it in ninety seconds. If it ends up in a state you do not understand, destroy it and run this page again. Nothing on the box after Prompt Zero is yours yet, which makes this the cheapest possible moment to get it wrong.
Two consequences worth taking seriously. Point Prompt Zero at a fresh, empty server, never at a box that already runs something you would miss. And once your data does live there, the answer stops being "destroy it" and starts being "restore it" — which is why every install prompt on this site takes a backup before it finishes, and why that will never be a paid feature.
One more thing, stated here rather than buried: Prompt Zero puts your login account in the docker group, and on this server that is equivalent to root. Anyone who can run docker can mount the host filesystem into a container. It is the accepted trade for not typing sudo in front of every container command, and it is why the box gets exactly one human account.
Prompt Zero itself
Open Claude Code in any directory, paste the whole thing, and answer the questions it asks. It stops once, on purpose, before the step that could lock you out. Nothing in it downloads an instruction: every package comes from a signed apt repository, and every block ends in something checkable rather than "make sure it works".
You are Claude Code on the user's machine. The user has a fresh Ubuntu 24.04 server from a cloud
provider, the SSH public key from this machine was added when the server was created, and
`ssh vps` currently logs in as root.
Harden that server end to end: one non-root login user with sudo, key-only SSH, a default-deny
firewall with 22, 80 and 443 open, automatic security updates, Docker Engine with the Compose
plugin, and Caddy running under systemd. Verify every block before starting the next one, and
finish by printing a checklist of what changed.
Run every command on the server over `ssh vps` unless a step says otherwise. Blocks 1 to 5 run as
root. From block 6 on, `ssh vps` logs in as <USER> and anything touching /etc carries `sudo`.
Never print, echo, or repeat in your summary the contents of any key, keyfile, or credential on
either machine.
## 1. Preflight
If <USER> is still literal, ask the user once what to call their login account, then stop until
they answer. Lowercase letters and digits, no spaces, not `admin`, not `root`.
```bash
grep PRETTY_NAME /etc/os-release
uname -m
id -un
ls -A /srv
```
Assert: the release is Ubuntu 24.04, the architecture is x86_64 or aarch64, and `id -un` prints
`root`. If the login is not root, ask the user which account the provider gave them and stop.
If `ls -A /srv` prints anything, or the user says this box already runs something they care
about, stop and say so. This prompt assumes a server whose total loss would cost nothing.
## 2. Update the system
```bash
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get -y upgrade
test -f /var/run/reboot-required && echo REBOOT-NEEDED || echo no-reboot-needed
```
If the last line printed REBOOT-NEEDED, reboot now with `reboot`. That ends the connection. Wait
about 30 seconds, then open a new one and check:
```bash
ssh vps 'uptime -p'
```
Assert: it answers, and reports minutes rather than days. Rebooting here costs 30 seconds.
Rebooting after nine services are installed costs an argument about which one failed to come back.
## 3. Create the login user
```bash
adduser --disabled-password --gecos "" <USER>
usermod -aG sudo <USER>
install -d -m 700 -o <USER> -g <USER> /home/<USER>/.ssh
install -m 600 -o <USER> -g <USER> /root/.ssh/authorized_keys /home/<USER>/.ssh/authorized_keys
```
Give that account sudo without a password prompt, and validate the file before anything depends
on it:
```bash
echo "<USER> ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/90-<USER>
chmod 440 /etc/sudoers.d/90-<USER>
visudo -c -f /etc/sudoers.d/90-<USER>
id <USER>
wc -l < /home/<USER>/.ssh/authorized_keys
```
Assert: `visudo -c` prints `parsed OK`, `id` lists the `sudo` group, and the key file has at
least one line. A malformed file under /etc/sudoers.d breaks sudo for every account on the box,
which is why it is checked in the same block that writes it.
The account has no password, on purpose. SSH becomes key-only two blocks from now, so a password
would be an unused secret sitting in a password manager. Tell the user in one sentence: their SSH
key is the only credential to this server, sudo will not ask them for anything, and the key
belongs on their laptop with a passphrase and nowhere else.
## 4. Firewall
Open 22 in the same block that enables the firewall, before enabling it. `ufw enable` under a
default-deny policy with no SSH rule ends the session and does not give it back.
```bash
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable
ufw status verbose
```
22 is the only way in. 80 is where the certificate challenge arrives, and where Caddy answers with
a redirect to HTTPS. 443 is where every app the user installs will answer.
Assert: the output contains `Status: active`, `deny (incoming)`, `allow (outgoing)`, and allow
rules for 22, 80 and 443 and nothing else. Application and database ports never appear in this
list for the life of the server: they bind to 127.0.0.1 in compose, and Caddy reaches them there.
## 5. STOP before the door closes
The next block turns off password authentication and root login. If the new account cannot get in
yet, that block locks the user out of their own server. This is the most common way a first VPS
dies, and the order of these two steps is the entire difference.
STOP: tell the user to do these three things in a second terminal window, and wait. Do not
continue until they confirm all three.
1. Open ~/.ssh/config and change `User root` to `User <USER>`.
2. Run `ssh vps`. It must log in, and `whoami` must print <USER>.
3. In that same session run `sudo -n true; echo $?`. It must print `0`.
If any of the three fails, fix it here, in this block. Do not proceed on the basis that it will
probably work.
Then state the recovery path once, so the stakes are clear rather than frightening: if a later
step does lock them out, the provider's web console still reaches the machine, and rebuilding this
server from scratch takes about a minute, because nothing on it is theirs yet.
## 6. Key-only SSH
```bash
sudo tee /etc/ssh/sshd_config.d/99-hardening.conf >/dev/null <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
PubkeyAuthentication yes
X11Forwarding no
MaxAuthTries 3
EOF
sudo chmod 644 /etc/ssh/sshd_config.d/99-hardening.conf
sudo sshd -t
sudo systemctl restart ssh
sudo sshd -T | grep -E '^(passwordauthentication|permitrootlogin|kbdinteractiveauthentication) '
```
Assert: `sshd -t` prints nothing and exits 0, and the last command prints
`passwordauthentication no`, `permitrootlogin no` and `kbdinteractiveauthentication no`. Running
`sshd -t` before the restart is the whole safety net here: a typo in that file plus a restart is a
locked door with no error message.
Open a new connection and prove the door still opens:
```bash
ssh -o BatchMode=yes vps 'whoami'
```
Assert: it prints <USER>. Sessions that are already open survive the restart, so a fresh
connection is the only honest test.
SSH stays on port 22. Moving it reduces log noise and nothing else, and it breaks every tool that
assumes the default.
## 7. Automatic security updates
```bash
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y unattended-upgrades
sudo dpkg-reconfigure -f noninteractive unattended-upgrades
cat /etc/apt/apt.conf.d/20auto-upgrades
sudo unattended-upgrade --dry-run -v | tail -3
```
Assert: 20auto-upgrades contains `APT::Periodic::Update-Package-Lists "1";` and
`APT::Periodic::Unattended-Upgrade "1";`, and the dry run exits 0. If either line is missing or
set to 0, write the file and re-read it:
```bash
sudo tee /etc/apt/apt.conf.d/20auto-upgrades >/dev/null <<'EOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
EOF
cat /etc/apt/apt.conf.d/20auto-upgrades
```
Automatic reboots stay off. Security patches install themselves; a kernel update that needs a
reboot waits for the user. An unattended 3am reboot during a database write is a worse outage than
a kernel that is a week old. Tell the user to run `cat /var/run/reboot-required` when they log in,
and to reboot when it says something.
## 8. Docker Engine and the Compose plugin
From Docker's apt repository, with its signing key on disk. Not a script piped into a shell: apt
gives a signed repository, one upgrade path, and packages the distribution can account for.
```bash
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker <USER>
```
```bash
sudo systemctl is-enabled docker
sudo systemctl is-active docker
sudo docker version --format 'server {{.Server.Version}}'
sudo docker compose version
```
Assert: `enabled`, `active`, and both versions print. Record both version strings for block 12.
Group membership only applies to new sessions, so confirm it the way the user will meet it:
```bash
ssh vps 'docker compose version'
```
Assert: a version prints, with no `sudo` and no permission error. If it reports permission denied
on /var/run/docker.sock, that session predates the group change; open another one.
Tell the user plainly: membership in the `docker` group is equivalent to root on this server,
because anyone who can run `docker` can mount the host filesystem into a container. That is the
accepted trade for not typing sudo in front of every container command, and it is the reason this
server has exactly one human account on it.
## 9. Caddy
From Caddy's own apt repository, so `apt-get upgrade` keeps it patched with everything else.
```bash
sudo apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list >/dev/null
sudo apt-get update
sudo apt-get install -y caddy
```
```bash
caddy version
sudo systemctl is-enabled caddy
sudo systemctl is-active caddy
curl -sSI http://127.0.0.1 | head -1
```
Assert: a version prints, the service is `enabled` and `active`, and the last line contains
`200`, which is Caddy's stock page answering on port 80. Record the version for block 12.
Two facts to state to the user, not to schedule: Caddy requests a certificate the first time a
hostname reaches it and renews on its own, so there is no cron job to add; and port 80 stays open
because that is where the certificate challenge arrives.
Leave /etc/caddy/Caddyfile as installed. Every install prompt on this site appends its own site
block and reloads.
## 10. What usually goes wrong
Three failures account for most of them. The lockout in block 6, which is why block 5 is a hard
stop. `docker` refusing to run without sudo in the same session that added the group, which is
not a broken install and needs nothing but a new connection. And a provider that has not finished
verifying the account: the server appears in the panel, `ssh vps` times out, and nothing in this
prompt is wrong. Check the provider's console before debugging SSH.
## 11. Out of scope
- Do not install fail2ban, an intrusion detector, or any other add-on. Key-only SSH behind a
default-deny firewall is the baseline every install prompt on this site assumes, and a second
moving part here is one nobody verified.
- Do not move SSH to a non-standard port.
- Do not create a DNS record, a Caddy site block, or a TLS certificate. The first install prompt
does that, with a real hostname.
- Do not install an application, a database, or a language runtime. This prompt produces a
hardened empty server and stops.
- Do not open another firewall port.
## 12. The closing checklist
Print exactly this, filled in from what the run measured, and nothing after it:
```
login user <USER> · in the sudo group · sudo without a password · no password set
root ssh login disabled
password ssh disabled
firewall default deny inbound, allow outbound · open: 22, 80, 443
auto updates unattended-upgrades enabled · automatic reboot off
docker server <version> · compose <version> · <USER> in the docker group
caddy <version> · systemd enabled and active · stock page on :80
reboot pending yes or no, from /var/run/reboot-required
your way in ssh vps
```
Then tell the user two things and stop. Their SSH key is the only credential to this server. And
the next prompt they paste is an install prompt from a project page, which assumes every line of
that checklist is true.This prompt carries no clean-machine stamp yet. The harness verifies per-app installs on disposable servers and writes the stamp from the run; when Prompt Zero has its own run, the date lands here. Until then the honest label is: written against the style guide, not yet machine-verified. The rules behind that distinction are on /methodology.
Where you end up
When the checklist prints, the server looks like this. Every per-app prompt on this site opens by assuming it, which is the entire reason this page exists.
os Ubuntu 24.04, fully patched
login one non-root user, in the sudo group, no password set
ssh key-only · root login off · password login off · port 22
firewall ufw, default deny inbound, allow outbound · open: 22, 80, 443
updates unattended-upgrades on · automatic reboot off, on purpose
docker Docker Engine + Compose plugin, from Docker's apt repository
your user is in the docker group, which is root-equivalent here
caddy systemd service, enabled and active · stock Caddyfile
automatic TLS, issued on the first request for a hostname
layout /srv is empty. Each app gets /srv/<app>/ and writes nothing outside it.
your way in ssh vpsWhat you do not have yet: a DNS record, a certificate, or a single application. Those arrive with the first install prompt, which needs one thing from you that no agent can do — a hostname pointed at that IP address.
Next: pick something off The Rack and paste one more prompt. If Prompt Zero broke for you, the prompt-failure report is the most useful thing this project receives, and if it did something dangerous rather than something broken, report it privately.