autohand -p "Review the latest changes, run tests, and summarize what needs attention" --output-format stream-json

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.

RuntimeUse it whenNotes
VPSYou want the simplest always-on remote terminalWorks with Hetzner, Linode, Vultr, DigitalOcean, AWS Lightsail, or any Ubuntu host
AWS EC2You need VPC access, IAM, CloudWatch, or enterprise controlsPrefer a small Ubuntu LTS instance and restrict SSH ingress
DigitalOcean DropletYou want a straightforward developer VPSAttach an SSH key during creation and use snapshots before major automation changes
DockerYou want repeatable local or remote Autohand Code runnersMount the target repository and pass credentials as environment variables or secrets
Cloudflare TunnelYou want private remote access without opening inbound portsRun Autohand Code on a VPS or container, then expose SSH or an HTTP trigger through cloudflared
Cloudflare WorkerYou need an edge webhook that validates requests and dispatches jobsWorkers can call APIs or a runner endpoint; they are not a replacement for a full Linux CLI environment
Cloudflare ContainersYou want a Worker-controlled containerized runtimeUse 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 --yes only for trusted repos and narrow tasks.
  • Logs: Prefer --output-format stream-json for 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 /undo or git revert can recover from unwanted changes.

Next steps