← Back to Blog

Run Claude Code and Codex 24/7 on a VPS (Laptop Off, Phone in Hand)

Published · by RS Computers

Claude Code Codex CLI AI agents

To run Claude Code and Codex 24/7 on a VPS, install both command-line agents on a Linux server under a normal user, keep interactive sessions inside tmux so they survive a dropped SSH connection, and let a systemd timer start unattended jobs with claude -p or codex exec. The models run in Anthropic's and OpenAI's clouds. The server holds your code, runs the tests and keeps working while your laptop is closed, and you read the results on your phone over SSH, through Claude's Remote Control or as a Telegram message. We built this box on Debian 13 on 12 October 2026 and show each command with the output we got; the few parts we could not test are marked as such.

Key facts, checked on 12 October 2026:

What runs on the server, and what doesn't

A coding agent reads your project, edits files and runs commands, guided by an AI model. Claude Code (Anthropic) and Codex CLI (OpenAI) are both terminal programs, and the heavy part, the model, answers over HTTPS from the vendor's data centre. So the agents need little; your builds and tests decide the server size.

PartWhere it runsWhat it needs from the server
The model (Claude or GPT)Anthropic's or OpenAI's cloudOutbound HTTPS only
The CLI (claude, codex)Your VPSAbout 190 to 320 MB of RAM each while open, 0.25 to 1 GB of disk
Your code, tests, builds, DockerYour VPSMost of the CPU, RAM and disk
tmux, the systemd timer, gitYour VPSA few MB

For a trading bot built by the agent, see our Claude Code crypto bot guide; to give it your own tools and data, see running an MCP server on a VPS. This guide is about the box itself.

Accounts and the fair-use line

Claude's Remote Control (step 8) works only with a subscription login, so our setup uses the subscription for interactive work and an API key only for the timer job.

Step 1: a server and a user that is not root

Log in to a fresh Debian 13 VPS as root, install the tools and create a user called agent with no sudo rights. Every command Claude or Codex runs, runs as this user, so it can't touch the system or the firewall.

# as root
apt update
apt install -y curl git tmux nftables bubblewrap socat nano
useradd -m -s /bin/bash agent
id agent

The last line should print uid=1000(agent) gid=1000(agent) groups=1000(agent). bubblewrap (0.12.0 on Debian 13) is the sandbox tool both agents use on Linux; Claude Code also needs socat for its network filter. Now let your root SSH key log in as agent:

# as root
install -d -m 700 -o agent -g agent /home/agent/.ssh
install -m 600 -o agent -g agent /root/.ssh/authorized_keys /home/agent/.ssh/authorized_keys

Step 2: SSH keys only, and a firewall

A server that works while you sleep should not accept passwords:

# as root
cat > /etc/ssh/sshd_config.d/10-agent-box.conf <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
EOF
sshd -t && systemctl restart ssh
sshd -T | grep -E '^(passwordauthentication|kbdinteractiveauthentication|permitrootlogin) '

Expected: permitrootlogin without-password, passwordauthentication no, kbdinteractiveauthentication no. A password-only login then ends with Permission denied (publickey), as in our test. Keep your current SSH window open until a new key login works.

The agents only make outgoing connections, so the firewall lets in SSH and nothing else:

# as root
cat > /etc/nftables.conf <<'EOF'
#!/usr/sbin/nft -f
flush ruleset

table inet agentbox {
  chain input {
    type filter hook input priority 0; policy drop;
    ct state established,related accept
    ct state invalid drop
    iif "lo" accept
    meta l4proto { icmp, ipv6-icmp } accept
    udp dport 68 accept          # DHCP replies
    iifname "tailscale0" accept  # only used once Tailscale is installed
    tcp dport 22 accept          # SSH
  }
}
EOF
nft -c -f /etc/nftables.conf
systemd-run --unit=fw-undo --on-active=5m /usr/sbin/nft delete table inet agentbox
nft -f /etc/nftables.conf

The systemd-run line is a safety net that deletes the rules after 5 minutes if they lock you out. If a new SSH session works, cancel it and load the rules at boot:

# as root
systemctl stop fw-undo.timer
systemctl enable nftables

In our test, port 22 answered from outside, a test service on port 8080 timed out, and the rules returned after a restart. Our Debian 13 hardening guide explains the pattern. To close public SSH entirely, put the server and your phone on Tailscale, log in over the private network and delete the tcp dport 22 line; see our Tailscale VPS guide or, to self-host the control server, Headscale vs NetBird.

Step 3: install Claude Code and Codex CLI

Log in with ssh agent@YOUR_SERVER_IP and run both official installers. Neither needs root or Node.js:

# as the agent user
curl -fsSL https://claude.ai/install.sh | bash
curl -fsSL https://chatgpt.com/codex/install.sh | sh
exit

They end with Claude Code successfully installed! and Codex CLI 0.162.1 installed successfully. Both commands land in ~/.local/bin, which Debian adds to your PATH at the next login, hence the exit. Connect again:

# as the agent user, after logging in again
claude --version
codex --version
claude doctor

We got 2.1.296 (Claude Code), codex-cli 0.162.1 and No installation issues found. Now sign in:

# as the agent user
claude auth login
codex login --device-auth

claude auth login prints a sign-in link and waits at Paste code here if prompted >: open the link on your laptop or phone, approve, paste the code back. codex login --device-auth shows https://auth.openai.com/codex/device and a code valid for 15 minutes; OpenAI says to switch on device code sign-in in your ChatGPT security settings first. Our lab has no accounts, so we stopped at both prompts. Claude Code keeps the login in ~/.claude/.credentials.json with mode 600 (authentication docs); Codex keeps it in ~/.codex/auth.json, which OpenAI says to treat like a password.

Measured in our labClaude Code 2.1.296Codex CLI 0.162.1
Install time10.5 s7.4 s
Disk after install246 MB432 MB
Disk after first interactive start246 MB970 MB (a second copy for its app-server)
RAM, interactive screen openabout 190 MBabout 320 MB with its app-server
Peak RAM, one headless runabout 250 MBabout 180 MB

RAM here is PSS, a process's share of memory including its program file, taken at the start screen; long sessions use more. free -m showed 286 MB for both together, because Linux counts most of those files as cache. Codex also left its app-server running after we quit, holding about 225 MB. codex app-server daemon stop ended it, apart from a 70 MB helper that stayed until the next restart.

Step 4: sessions that survive a closed laptop

tmux is a terminal that keeps running on the server after you disconnect. Start one per agent:

# as the agent user
mkdir -p ~/projects && cd ~/projects
tmux new -s claude

Inside it, go to your project and run claude. Press Ctrl+B, then D, to leave it running. Do the same with tmux new -s codex. Later, tmux ls lists the sessions and tmux attach -t claude takes you back. In our test, a job writing the time every second kept writing through 20 seconds without an SSH connection, and the session was still there when we reconnected.

Step 5: fence the agents in

An agent can do what its Linux user can do and read what that user can read. Our most useful finding: Codex's default sandbox blocked every write and network request, as documented, yet a command inside it could still read the server's private Git key. Reads are open by default in both tools, so tell them what is off limits.

First, a private folder for keys. read -rs takes a key without showing it or saving it in your shell history:

# as the agent user
mkdir -p ~/.config/agent && chmod 700 ~/.config/agent
umask 077
read -rs KEY          # paste the Anthropic API key, press Enter
printf '%s\n' "$KEY" > ~/.config/agent/anthropic.key; unset KEY
read -rs KEY          # paste the OpenAI API key, press Enter
printf '%s\n' "$KEY" > ~/.config/agent/openai.key; unset KEY
stat -c '%a %n' ~/.config/agent ~/.config/agent/*

stat should show 700 for the folder and 600 for each file. Skip any key you don't use.

Claude Code

These rules in ~/.claude/settings.json stop Claude's file tools from reading the key folders, switch off the mode that skips all permission checks, turn on the bubblewrap sandbox for shell commands, and make Claude Code refuse to start without it:

# as the agent user
cat > ~/.claude/settings.json <<'EOF'
{
  "permissions": {
    "deny": [
      "Read(~/.ssh/**)",
      "Read(~/.config/agent/**)",
      "Read(~/.codex/auth.json)"
    ],
    "disableBypassPermissionsMode": "disable"
  },
  "sandbox": {
    "enabled": true,
    "failIfUnavailable": true,
    "allowUnsandboxedCommands": false,
    "credentials": {
      "files": [
        { "path": "~/.ssh", "mode": "deny" },
        { "path": "~/.config/agent", "mode": "deny" },
        { "path": "~/.codex/auth.json", "mode": "deny" }
      ]
    }
  }
}
EOF
claude doctor

claude doctor names a broken settings file; ours passed. We could not watch these rules stop a real Claude session without an account, so the rest comes from the sandboxing docs. Permission rules cover Claude's own Read and Edit tools, while the sandbox covers shell commands and their child processes, which may write only in the project and a temporary folder. The allowed network list starts empty, and any other host waits for your approval. Nobody approves anything at 3 a.m., so add the hosts your tests need, such as a package registry, under sandbox.network.allowedDomains.

The timer job gets its API key through apiKeyHelper, a setting that runs a command to fetch the key. A separate file keeps the key out of your interactive sessions:

# as the agent user
cat > ~/.config/agent/claude-job.json <<'EOF'
{
  "apiKeyHelper": "cat ~/.config/agent/anthropic.key"
}
EOF

With a fake key, claude -p used the helper and failed with Invalid API key · Fix external API key, but only after 255 seconds of retries. That is why the timer in step 7 has a time limit.

Codex

Codex takes a permissions profile in ~/.codex/config.toml. This one extends the built-in workspace profile (write in the project, no network) and denies reads of the key folders. Use full paths:

# as the agent user
cat > ~/.codex/config.toml <<'EOF'
default_permissions = "agentbox"

[permissions.agentbox]
description = "Write in the project, never read keys"
extends = ":workspace"

[permissions.agentbox.filesystem]
"/home/agent/.ssh" = "deny"
"/home/agent/.config/agent" = "deny"
"/home/agent/.codex/auth.json" = "deny"
EOF
codex doctor | grep denied-read

Expect denied-read rules 3; in a project, codex exec then shows sandbox: workspace-write. You will test the fence at the end of step 6. Before allowing network for package installs, read OpenAI's approvals and security page.

Step 6: Git with a deploy key

A deploy key is an SSH key that opens one repository. On GitHub it is read-only unless you tick "Allow write access", it can't be reused on a second repository, and it never expires (GitHub docs). Create one per project:

# as the agent user
ssh-keygen -t ed25519 -N "" -C "agent-box deploy key" -f ~/.ssh/deploy_demo
cat >> ~/.ssh/config <<'EOF'
Host github-demo
  HostName github.com
  User git
  IdentityFile ~/.ssh/deploy_demo
  IdentitiesOnly yes
EOF
chmod 600 ~/.ssh/config
ssh-keyscan -t ed25519 github.com > /tmp/gh.key
ssh-keygen -lf /tmp/gh.key
cat /tmp/gh.key >> ~/.ssh/known_hosts
ssh -T github-demo

ssh-keygen -lf must print SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU, GitHub's published Ed25519 fingerprint (GitHub fingerprints). The last line says Permission denied (publickey) until you paste ~/.ssh/deploy_demo.pub into the repository under Settings, Deploy keys. Then clone through the alias, for example git clone github-demo:YOURNAME/demo.git ~/projects/demo, and set git config --global user.name and user.email. Our lab used a local Git server with the same alias method in place of GitHub. For your own Git server, see our Forgejo guide.

The rule that keeps this safe: the agent never pushes to main. Each run goes to its own branch, such as agent/2026-10-12_1002, and you merge after reading the diff. On GitHub, protect main with a rule that requires a pull request.

Check the Codex fence

codex sandbox runs a command in the agent's sandbox without calling the model:

# as the agent user
cd ~/projects/demo
codex sandbox -- touch inside.txt && rm inside.txt && echo "project is writable"
codex sandbox -- head -1 /home/agent/.ssh/deploy_demo

The second command must end in Permission denied. Our results:

Inside the Codex sandboxDefaultWith the profile
Write in the projectBlockedAllowed
Write in the home folderBlockedBlocked
Read the private deploy keyAllowedPermission denied
Reach a websiteBlockedBlocked

The fence also held under the systemd limits of the timer in step 7.

Step 7: the night shift, a systemd timer

A systemd timer is Debian's scheduled task. The script and units belong to root and run as agent, so the agent can't change its own schedule, which it could with a unit in its home folder. The script fetches the newest main, makes a branch, runs the agent, commits, pushes the branch and messages you:

# as root
cat > /usr/local/bin/agent-job <<'EOF'
#!/bin/bash
# One unattended agent run as the "agent" user. Started by agent-job.timer.
set -uo pipefail

REPO="$HOME/projects/demo"        # the project the agent works on
TASK="$HOME/tasks/nightly.md"     # what you want done, in plain words
OUT="$HOME/agent-runs"            # one report per run
STAMP="$(date +%Y-%m-%d_%H%M)"
BRANCH="agent/$STAMP"
mkdir -p "$OUT"
cd "$REPO" || exit 1

notify() {  # short message to your phone, skipped when there is no token file
  local f="$HOME/.config/agent/telegram.env"
  [ -r "$f" ] || return 0
  . "$f"
  curl -sS -m 15 -o /dev/null -w "telegram: HTTP %{http_code}\n" \
    "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \
    -d chat_id="$TG_CHAT" --data-urlencode text="$1"
}

# 1. start from the newest main, on a fresh branch
if ! git fetch --quiet origin || ! git switch --quiet -C "$BRANCH" origin/main; then
  notify "agent-job $STAMP: git fetch failed"; exit 1
fi

# 2. the agent itself: keep ONE of the three blocks
# --- placeholder (what our lab ran) ---
echo "placeholder run $(date -Is)" >> agent-heartbeat.txt
echo "Placeholder: the model call runs here." > "$OUT/$STAMP.md"
STATUS=0
# --- Claude Code ---
# claude -p "$(cat "$TASK")" --settings "$HOME/.config/agent/claude-job.json" \
#   --permission-mode acceptEdits --max-turns 40 > "$OUT/$STAMP.md"
# STATUS=$?
# --- Codex ---
# CODEX_API_KEY="$(cat "$HOME/.config/agent/openai.key")" \
#   codex exec -o "$OUT/$STAMP.md" "$(cat "$TASK")" > /dev/null
# STATUS=$?

# 3. commit to the agent branch only; main is for humans
git add -A
if git diff --cached --quiet; then
  RESULT="no changes"
elif git commit --quiet -m "agent run $STAMP" && git push --quiet origin "$BRANCH"; then
  RESULT="pushed $BRANCH"
else
  RESULT="commit or push failed"
fi
echo "exit $STATUS, $RESULT"
notify "agent-job $STAMP: exit $STATUS, $RESULT"
exit "$STATUS"
EOF
chmod 755 /usr/local/bin/agent-job
bash -n /usr/local/bin/agent-job && echo "script syntax OK"

Our lab ran the placeholder, because a model call needs a paid account. To switch, delete the three placeholder lines and uncomment the Claude Code or Codex block. Those two blocks follow Anthropic's CLI reference and OpenAI's non-interactive docs. We could only run them with missing or fake keys, and both stopped with a clear error: Not logged in · Please run /login from Claude Code, 401 Unauthorized from Codex after about 14 seconds. Treat your first real night as a test and read its report. Without --settings, the Claude block uses your subscription login instead of the API key, and --max-turns 40 stops a run that goes in circles. With the step 5 sandbox on, Claude runs shell commands such as your tests inside it without asking. Codex in exec mode showed approval: never and the step 5 profile.

Write the task in plain words, as you would brief a colleague:

# as the agent user
mkdir -p ~/tasks
cat > ~/tasks/nightly.md <<'EOF'
Run the test suite. If a test fails, find the cause and fix it with the smallest change.
Do not touch files outside this repository. Do not add dependencies.
End with a short report: what failed, what you changed, what you could not fix.
EOF

Now the service and timer. The limits stop a runaway job at 2 hours, 2 GB of RAM and one and a half CPU cores:

# as root
cat > /etc/systemd/system/agent-job.service <<'EOF'
[Unit]
Description=Unattended coding agent run
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
User=agent
Group=agent
Environment=PATH=/home/agent/.local/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=/usr/local/bin/agent-job
TimeoutStartSec=2h
MemoryMax=2G
CPUQuota=150%
Nice=10
NoNewPrivileges=yes
EOF
cat > /etc/systemd/system/agent-job.timer <<'EOF'
[Unit]
Description=Run the coding agent every night

[Timer]
OnCalendar=*-*-* 03:00:00
RandomizedDelaySec=15m
Persistent=true

[Install]
WantedBy=timers.target
EOF
systemd-analyze verify /etc/systemd/system/agent-job.service /etc/systemd/system/agent-job.timer
systemctl daemon-reload
systemctl start agent-job.service
journalctl -u agent-job.service -n 4

The manual run logged exit 0, pushed agent/2026-10-12_0959. Per the systemd.timer manual, Persistent=true catches up a run missed while the server was off, within the same random delay. To watch the timer fire, give it a two-minute schedule for a while:

# as root, for testing only
mkdir -p /etc/systemd/system/agent-job.timer.d
cat > /etc/systemd/system/agent-job.timer.d/test.conf <<'EOF'
[Timer]
OnCalendar=
OnCalendar=*:0/2
RandomizedDelaySec=0
EOF
systemctl daemon-reload
systemctl enable --now agent-job.timer
systemctl list-timers agent-job.timer

Ours fired at 10:00:06 and 10:02:13; each run pushed its own branch and main stayed untouched. The journal also showed telegram: HTTP 401, because our token was a placeholder. Then remove the test schedule:

# as root
rm -r /etc/systemd/system/agent-job.timer.d
systemctl daemon-reload
systemctl list-timers agent-job.timer

The next run showed as Tue 2026-10-13 03:12:56 UTC, 03:00 plus the random delay. After a full restart, the timer, firewall and key login all came back. To run on every push instead of every night, a CI runner fits better; see our staging server and CI runner guide.

Step 8: reading results from your phone

SSH from a phone app

Any SSH app with Ed25519 keys works, such as Termius, Blink Shell or Termux. Create a key in the app and add its public half as a new line in /home/agent/.ssh/authorized_keys. Then tmux attach -t claude opens the live session, ls ~/agent-runs lists the night's reports, and git log --oneline --all shows the new branches.

A Telegram message when a run ends

Create a bot with @BotFather and find your chat ID, as our Telegram bot guide shows. Store both where the job looks:

# as the agent user
umask 077
cat > ~/.config/agent/telegram.env <<'EOF'
TG_TOKEN="123456789:your-bot-token"
TG_CHAT="your-chat-id"
EOF

A wrong token gets {"ok":false,"error_code":401,"description":"Unauthorized"} from Telegram and telegram: HTTP 401 in the journal. We could not test a real bot.

Remote Control and Codex

With a Pro, Max, Team or Enterprise login, Claude Code's Remote Control links a server session to the Claude app. We could not try it without a subscription. Per Anthropic's docs, you start claude remote-control inside tmux, answer y to a one-time question and press the spacebar to show a QR code for your phone. It makes outbound HTTPS requests only, so the firewall stays shut. API keys are not supported, on Team and Enterprise an owner must switch the feature on first, and in server mode it exits after about 10 minutes without network.

For Codex, OpenAI's remote connections page says phone setup starts in the ChatGPT desktop app on a Mac or Windows host, although that app can open projects on an SSH host such as this VPS. Without that desktop app, SSH plus Telegram is the dependable route for Codex.

Which plan for a 24/7 agent box

Anthropic asks for 4 GB of RAM, and the agents' own use is small next to a test suite or a Docker build. VPS plans have shared vCPUs; VDS plans have dedicated vCPUs, which keep long builds at a steady speed.

PlanSpecsFits
VPS Mini4 shared vCPU, 4 GB RAM, 80 GB NVMe, 1 Gb/sOne or two agents in tmux and a nightly job on a small or medium repository
VDS Small4 dedicated vCPU, 8 GB RAM, 240 GB NVMe, 10 Gb/sBoth agents all day, real test suites, Docker builds
VDS Medium8 dedicated vCPU, 16 GB RAM, 480 GB NVMe, 10 Gb/sSeveral agents in parallel on several repositories, browser tests
VDS Large16 dedicated vCPU, 32 GB RAM, 960 GB NVMe, 10 Gb/sOne box for a small team, with a Linux user per person

Nano and Micro (1 and 2 GB) sit below Anthropic's minimum. All plans run on KVM with NVMe, unmetered traffic and their own IPv4 and IPv6, and you can upgrade later from the client area with a short reboot. Servers are in Amsterdam, Dublin and Prishtina; availability by city is on the plans page. For agents that live in chat apps, our OpenClaw self-hosting guide applies the same fences to another tool.

Frequently asked questions

Can Claude Code keep working when I close my laptop?

Yes, if it runs on a server. Start Claude Code inside tmux on a VPS and detach with Ctrl+B, D; it keeps running after SSH disconnects, and tmux attach brings it back from any device. For scheduled work without a session, run claude -p from a systemd timer.

How much RAM do Claude Code and Codex need on a VPS?

Anthropic lists 4 GB for Claude Code. In our Debian 13 lab, Claude Code 2.1.296 used about 190 MB with its screen open and Codex CLI 0.162.1 about 320 MB including its app-server. Your builds and tests usually need far more.

Can I use my Claude or ChatGPT subscription on a server?

Yes. Sign in on the server through the vendor's page with claude auth login or codex login --device-auth. Anthropic says Pro and Max limits assume ordinary individual use, so heavy unattended jobs fit better on an API key.

What is the difference between claude -p and codex exec?

Both run an agent once without the interactive screen and exit, which suits timers. claude -p accepts limits such as --max-turns; codex exec never asks for approval and starts read-only unless you allow more.

How do I check on a coding agent from my phone?

Attach to its tmux session from an SSH app, or let the job send a Telegram message when it ends. Claude Code also has Remote Control for the Claude app with a subscription login; Codex's phone pairing currently needs a desktop app host.

Is it safe to let an AI agent run unattended on a server?

Reasonably, with fences: a user without sudo, only SSH open, sandboxes that block reading your keys, and one Git branch per run that a human reviews. In our test, Codex's default sandbox could read a private key until we added a deny rule.

Your agent box, set up tonight

A normal user, SSH keys, an nftables firewall, two installers, tmux for live sessions and one systemd timer for the night shift: that is the whole box, and it ran in 286 MB of RAM in our lab before any project work. Add the sandbox rules before the first real run and keep main for humans. Pick a server on the plans page, or message us on Telegram for help choosing a plan or a city.

← All articles

Chat on Telegram