Headless Mode
Run Autohand Code on a VPS, Docker, and Cloud Hosts
Install Autohand Code on an always-on Linux host so you can run headless automation from SSH, cron, webhooks, Docker, AWS, DigitalOcean, or Cloudflare.
What this covers
- Provision a small Linux host for always-on Autohand Code headless runs
- Install Node.js, Git, Autohand Code, and provider credentials safely
- Run one-off, scheduled, and webhook-triggered Autohand Code jobs
- Expose a remote runner with SSH, Docker, or Cloudflare Tunnel
- Choose between VPS, AWS EC2, DigitalOcean Droplets, Cloudflare Workers, and Cloudflare Containers
Provider-specific tutorials
Use this page to choose the right runner pattern, then follow the dedicated provider guide for exact setup steps.
Choose a runtime
Use a real Linux host when Autohand Code needs a repository checkout, Git, package managers, build tools, or a long-running shell. Use Cloudflare Workers as a secure HTTP trigger or gateway, not as the process that runs the CLI directly.
| Runtime | Use it when | Notes |
|---|---|---|
| VPS | You want the simplest always-on remote terminal | Works with Hetzner, Linode, Vultr, DigitalOcean, AWS Lightsail, or any Ubuntu host |
| AWS EC2 | You need VPC access, IAM, CloudWatch, or enterprise controls | Prefer a small Ubuntu LTS instance and restrict SSH ingress |
| DigitalOcean Droplet | You want a straightforward developer VPS | Attach an SSH key during creation and use snapshots before major automation changes |
| Docker | You want repeatable local or remote Autohand Code runners | Mount the target repository and pass credentials as environment variables or secrets |
| Cloudflare Tunnel | You want private remote access without opening inbound ports | Run Autohand Code on a VPS or container, then expose SSH or an HTTP trigger through cloudflared |
| Cloudflare Worker | You need an edge webhook that validates requests and dispatches jobs | Workers can call APIs or a runner endpoint; they are not a replacement for a full Linux CLI environment |
| Cloudflare Containers | You want a Worker-controlled containerized runtime | Use when you need a Linux-like filesystem or existing container image controlled from Worker code |
Prerequisites
- A Linux host running Ubuntu 22.04 LTS or newer
- SSH key access to the host; avoid password SSH
- Node.js 20 or newer, Git, and a package manager for your project
- An Autohand Code-supported model provider key, such as OpenRouter, OpenAI, Anthropic-compatible gateways, AWS Bedrock, GCP Vertex AI, Ollama, MLX, or llama.cpp
- A repository URL and a dedicated system user for automation
Security note: Do not run unattended automation as root. Create a dedicated user, keep secrets out of shell history, and prefer read-only deploy keys until you intentionally enable write access.
Step 1: Provision a VPS
Create a small Ubuntu host with your SSH public key attached. A basic shared CPU instance is enough for most headless code review, docs, lint, and issue-triage jobs. Scale up when your project builds require more CPU or memory.
# Local machine: generate a dedicated key for this runner
ssh-keygen -t ed25519 -C "autohand-runner" -f ~/.ssh/autohand-runner
# Connect after the provider gives you an IP address
ssh -i ~/.ssh/autohand-runner [email protected]
For AWS EC2, create an Ubuntu AMI instance, attach a security group that allows SSH only from trusted IPs, and use Systems Manager Session Manager if your organization forbids public SSH. For DigitalOcean, create a Droplet with your SSH key selected during provisioning.
Step 2: Install Autohand Code on the host
Install the base runtime and Autohand Code CLI as the automation user.
sudo apt-get update
sudo apt-get install -y ca-certificates curl git build-essential
# Install Node.js from your preferred trusted source, then verify:
node --version
npm --version
# Install Autohand Code
npm install -g autohand-cli
autohand --version
If your team uses Homebrew on Linux, you can install Autohand Code with Homebrew instead. Keep the install method consistent across hosts so upgrades are predictable.
Step 3: Configure credentials
Use environment variables or a config file owned by the runner user. Keep the file readable only by that user.
mkdir -p ~/.autohand
chmod 700 ~/.autohand
cat > ~/.autohand/env <<'EOF'
export OPENROUTER_API_KEY="sk-or-v1-..."
export AUTOHAND_MODEL="anthropic/claude-sonnet-4"
export AUTOHAND_OUTPUT_FORMAT="stream-json"
EOF
chmod 600 ~/.autohand/env
source ~/.autohand/env
If you store provider configuration in ~/.autohand/config.json, keep that file out of repository checkouts and backups that are shared broadly.
Step 4: Clone the project
Use a deploy key or machine user token. Prefer least privilege: read-only for review jobs, write access only for jobs that push branches or commits.
mkdir -p ~/work
cd ~/work
git clone [email protected]:your-org/your-repo.git
cd your-repo
# Optional: let Autohand Code learn project conventions
autohand -p "Read this repo and create or update AGENTS.md with build, test, and style guidance" --restricted
Step 5: Run Autohand Code headlessly
Start with read-only analysis, then enable edits after the runner is configured correctly.
# Read-only review
autohand -p "Review this repository for failing tests, security risks, and obvious maintenance issues" --restricted
# Stream progress for remote logs
autohand -p "Run the test suite and fix straightforward failures" --output-format stream-json
# Allow approved automation in a trusted runner
autohand -p "Update docs for the latest CLI changes, run tests, and commit the result" --yes --auto-commit
Use --restricted for review-only jobs, --dry-run for previews, and --yes only on runners where repository write access and shell access are intentionally scoped.
Step 6: Schedule jobs
For simple recurring automation, use cron or systemd timers. Write logs to a directory owned by the runner user.
mkdir -p ~/logs
crontab -e
# Every weekday at 08:00 UTC: pull, review, and write JSON logs
0 8 * * 1-5 . $HOME/.autohand/env && cd $HOME/work/your-repo && git pull --ff-only && autohand -p "Review changes from the last 24 hours and summarize issues" --restricted --output-format json >> $HOME/logs/daily-review.jsonl 2>&1
Run Autohand Code in Docker
Use Docker when you want a repeatable runner image or you need to keep host dependencies isolated. Mount the repository into the container and pass provider credentials at runtime.
FROM node:22-bookworm
RUN apt-get update \
&& apt-get install -y --no-install-recommends git openssh-client ca-certificates build-essential \
&& rm -rf /var/lib/apt/lists/*
RUN npm install -g autohand-cli
WORKDIR /workspace
ENTRYPOINT ["autohand"]
docker build -t autohand-runner .
docker run --rm -it \
-e OPENROUTER_API_KEY \
-e AUTOHAND_MODEL="anthropic/claude-sonnet-4" \
-v "$PWD:/workspace" \
autohand-runner \
-p "Review this repository and suggest the highest-risk fixes" --restricted
For a remote Docker host, clone the repository on the host, run the container there, and use SSH or Cloudflare Tunnel for access. Avoid baking API keys into the image.
Add a webhook runner
A minimal HTTP trigger lets GitHub Actions, Slack, Linear, or a Cloudflare Worker dispatch jobs to the VPS without giving every service SSH access.
// server.mjs
import { createServer } from "node:http";
import { spawn } from "node:child_process";
const token = process.env.AUTOHAND_RUNNER_TOKEN;
const repoDir = process.env.AUTOHAND_REPO_DIR || "/home/autohand/work/your-repo";
createServer((req, res) => {
if (req.method !== "POST" || req.headers.authorization !== `Bearer ${token}`) {
res.writeHead(401);
res.end("unauthorized");
return;
}
const job = spawn("autohand", [
"-p",
"Review the latest repository state and summarize action items",
"--restricted",
"--output-format",
"stream-json"
], { cwd: repoDir, env: process.env });
res.writeHead(202, { "Content-Type": "text/plain" });
job.stdout.on("data", chunk => process.stdout.write(chunk));
job.stderr.on("data", chunk => process.stderr.write(chunk));
job.on("exit", code => console.log(`autohand exited ${code}`));
res.end("queued\n");
}).listen(8787, "127.0.0.1");
AUTOHAND_RUNNER_TOKEN="$(openssl rand -hex 32)" node server.mjs
Keep the listener bound to 127.0.0.1 unless it is behind a reverse proxy, VPN, or Cloudflare Tunnel with authentication.
Expose remote access with Cloudflare Tunnel
Cloudflare Tunnel is a good fit when the runner should not expose inbound ports. Install cloudflared on the VPS and publish either SSH or the webhook runner through an outbound tunnel.
# On the VPS
cloudflared tunnel login
cloudflared tunnel create autohand-runner
# Route a private webhook hostname to the local runner
cloudflared tunnel route dns autohand-runner autohand-runner.example.com
# ~/.cloudflared/config.yml
tunnel: autohand-runner
credentials-file: /home/autohand/.cloudflared/autohand-runner.json
ingress:
- hostname: autohand-runner.example.com
service: http://127.0.0.1:8787
- service: http_status:404
cloudflared tunnel run autohand-runner
Put Cloudflare Access in front of the hostname for human-triggered workflows. For machine-triggered webhooks, validate HMAC signatures or bearer tokens in the runner before starting Autohand Code.
Use a Cloudflare Worker as a trigger
A Worker can validate an incoming webhook, apply rate limits, hide the runner origin, and dispatch a request to your VPS or Cloudflare Container. It should not be treated as a general Linux shell for the Autohand Code CLI.
export default {
async fetch(request, env) {
if (request.method !== "POST") {
return new Response("method not allowed", { status: 405 });
}
const auth = request.headers.get("authorization");
if (auth !== `Bearer ${env.RUNNER_TRIGGER_TOKEN}`) {
return new Response("unauthorized", { status: 401 });
}
const response = await fetch(env.RUNNER_URL, {
method: "POST",
headers: {
authorization: `Bearer ${env.RUNNER_BACKEND_TOKEN}`,
"content-type": "application/json"
},
body: await request.text()
});
return new Response(await response.text(), { status: response.status });
}
};
wrangler secret put RUNNER_TRIGGER_TOKEN
wrangler secret put RUNNER_BACKEND_TOKEN
wrangler secret put RUNNER_URL
wrangler deploy
Use Cloudflare Containers when you need a container runtime
If the runner must execute inside Cloudflare instead of a VPS, use Cloudflare Containers rather than a plain Worker. Containers are controlled from Worker code and are intended for workloads that need a full filesystem, a specific runtime, or an existing container image.
import { Container, getContainer } from "@cloudflare/containers";
export class AutohandCodeContainer extends Container {
defaultPort = 8787;
sleepAfter = "10m";
}
export default {
async fetch(request, env) {
const id = request.headers.get("x-repo") || "default";
const instance = getContainer(env.AUTOHAND_CONTAINER, id);
return instance.fetch(request);
}
};
name = "autohand-container-runner"
main = "src/index.js"
compatibility_date = "2026-06-18"
[[containers]]
class_name = "AutohandCodeContainer"
image = "./Dockerfile"
max_instances = 5
[[durable_objects.bindings]]
class_name = "AutohandCodeContainer"
name = "AUTOHAND_CONTAINER"
[[migrations]]
new_sqlite_classes = [ "AutohandCodeContainer" ]
tag = "v1"
Start with the VPS or Docker path first. Move to Cloudflare Containers when you specifically need Worker-controlled routing, on-demand containers, or Cloudflare-native deployment.
Operations checklist
- Identity: Use a dedicated Unix user, Git deploy key, and model provider key for the runner.
- Network: Restrict SSH by IP, VPN, or Cloudflare Tunnel. Do not expose raw runner ports to the internet.
- Permissions: Default scheduled jobs to
--restricted. Enable--yesonly for trusted repos and narrow tasks. - Logs: Prefer
--output-format stream-jsonfor long jobs and ship logs to CloudWatch, journald, or your normal log pipeline. - Updates: Pin the CLI version for production runners, test upgrades in a disposable clone, then roll forward.
- Recovery: Keep snapshots, use git branches for write jobs, and make sure
/undoor git revert can recover from unwanted changes.