clawops

MCP serverCloud & infra

Lets your agent deploy OpenClaw to AWS, GCP, Azure or a local VM.

Unavailable. This server has no hosted endpoint yet, so ahel can't serve it.

Add to setup to save this item as a reference. ahel cannot run it, and signing in will not install it.

About this server

Deploy self-hosted OpenClaw to AWS, GCP, Azure or a local VM, a CLI and an MCP server in one.

Getting started

  1. Save this item in Your setup as a reference.
  2. Read the source or reference documentation for its setup requirements. Saving it here does not connect it to your AI.
  3. Check this page for availability before trying to install it through ahel.

From the project's README

As published by dfridkin/clawops in README.md.

MCP-native infrastructure ops for OpenClaw, with read-only mode, destructive-action confirmation, and audit logs built in.

clawops is a CLI and MCP server for deploying and operating self-hosted OpenClaw instances. Provision on AWS, GCP, Azure, or any Linux VM, then manage day-to-day operations from the terminal, or let Claude Code and Cursor drive them through typed MCP tools with explicit safety controls.


What's new in 2.0.1

A patch release, and a large one: in 2.0.0 no cloud deploy succeeded by any path. Every item below is a fix or an addition in 2.0.1. The reasoning behind each one is in its commit message, and the decisions that came out of them are in docs/decisions/.

Deploying to a cloud

  • clawops plan → clawops apply provisions a cloud stack and deploys OpenClaw onto it.
  • clawops up deploys to AWS, GCP and Azure, running the same path as plan → apply.
  • clawops installs the Pulumi CLI it needs into ~/.clawops/.pulumi-cli, or uses a compatible one already on $PATH (ADR 0010).
  • clawops creates and stores the passphrase its state backend requires (ADR 0011).
  • clawops plan takes --ssh-cidr, --gateway-cidr and --publish-gateway, and apply passes them to the cloud firewall. auto resolves this machine's address.
  • clawops plan stops, and names the cause, when it cannot open the state backend.
  • --instance-type takes a clawops alias (micro–gpu) or a machine type your cloud names itself, and the plan records the concrete type.
  • Deploys pin the account they were planned against: gcp:project on GCP, azure-native:subscriptionId on Azure.

Checking the account before you spend

  • clawops doctor --provider <cloud> checks one cloud's credentials and account setup, with or without a stack. --instance-type points the size check at the size you are deploying.
  • AWS. The account the credentials resolve to, the state bucket, and whether the instance type is offered in the region.
  • GCP. The project, the APIs a deploy needs, and the state bucket.
  • Azure. The subscription, the resource providers, the VM size, and the azblob credentials Pulumi authenticates with.
  • clawops setup runs the same checks and offers to fix what it safely can, enabling an API, creating a state bucket with versioning on and public access blocked, naming the change before making it.
  • A check clawops could not perform reports as a warning naming the error, rather than as a pass or a failure.
  • Azure accepts your az login; a service principal is no longer required.

Naming, config and setup

  • clawops names the state backend after the account it is deploying into, instead of asking you for a name or writing a placeholder (ADR 0012).
  • A name you type instead is checked against the rules of the cloud that has to accept it.
  • clawops init keeps the stacks already in your config.
  • clawops init generates an SSH key that clawops can read. If you ran init before this release, clawops doctor will tell you whether yours is usable.
  • gcloud config set project is honoured.
  • The setup wizard writes model configuration that OpenClaw accepts, and installs the plugin your chosen provider needs.
  • Amazon Bedrock works: the right transport, and an inference profile resolved against your deployment region and recorded in the plan. Needs bedrock:ListInferenceProfiles.

While a deploy is running

  • apply waits for SSH, then waits for the gateway to answer, before reporting success.
  • apply reports progress as it goes instead of going quiet for minutes.
  • A deploy that times out prints what the host was doing, from its bootstrap log.
  • A host still installing Docker is treated as still booting rather than as a failed deploy.

Day-two commands

  • clawops logs reads from the gateway on AWS.
  • doctor --stack, ssh, logs, gateway, config and agents work against a freshly deployed stack.
  • clawops doctor validates cloud credentials.
  • clawops tells a refused Docker socket from a missing container, and says which it found.
  • clawops destroy forgets the instance's host key, so redeploying onto an address the cloud has recycled no longer fails verification.

Documentation

  • The GCP guide names the credential source clawops actually reads, and describes 2.0 firewall behaviour.
  • The smoke-test plan covers 2.0, and pnpm test:cloud aws|gcp|azure runs it against a real deployment and destroys it afterwards.

What's new in 2.0

clawops 2.x targets OpenClaw >= 2026.9.2. The 1.x line continues for OpenClaw <= 2026.7.1-2 under the legacy dist-tag until 2027-03-31:

npm install -g @clawops/cli            # 2.x
npm install -g @clawops/cli@legacy     # 1.x maintenance

Pin the tag in CI. latest moves to 2.x, so an unpinned pipeline will change lines. CHANGELOG.md carries the full history; this section covers what changed about how clawops behaves.

Your deployment keeps its state

OpenClaw 2.0 stores sessions, transcripts and credentials in SQLite. clawops mounted no state at all, so every restart destroyed them, and a restart is what gateway restart, gateway update and config set all do.

One host directory (/var/lib/clawops/openclaw) is now bind-mounted at OpenClaw's own default location, holding the config, the database and any provider plugins. Existing deployments migrate on the next up/apply.

clawops up / clawops apply

flowchart TD
    A["clawops plan"] --> B{"config valid<br/>against OpenClaw schema?"}
    B -- no --> B1["refuse: plan is still<br/>a file you can edit"]
    B -- yes --> C["clawops apply"]
    C --> D{"OpenClaw version<br/>in supported range?"}
    D -- no --> D1["refuse: names<br/>@clawops/cli@legacy"]
    D -- yes --> E["provision host"]
    E --> F["state dir, owned 1000:1000<br/>migrate any pre-2.0 config"]
    F --> G["write config<br/>validated before writing"]
    G --> H["install provider plugins<br/>while egress exists"]
    H --> I["start gateway"]
    I --> J{"/startupz says started?"}
    J -- no --> J1["fail with the reason"]
    J -- yes --> K{"configured providers<br/>all loaded?"}
    K -- no --> K1["warn: healthy gateway,<br/>missing model backend"]
    K -- yes --> L["done"]

Three of those steps are new, and each exists because the old flow could report success while something was wrong: the config was never validated before being written, provider plugins were left to be fetched at boot (or silently missing on a deny-all host), and "started" was inferred from docker run exiting 0.

clawops gateway update

Previously: pull, run, report success. docker run exiting 0 means the container was created, and the container it replaced is already gone.

flowchart TD
    A["clawops gateway update X"] --> B{"X in supported range?"}
    B -- no --> B1["refuse before pulling"]
    B -- yes --> C["docker pull X"]
    C --> D["snapshot state database"]
    D -- cannot snapshot --> D1["refuse: no rollback point"]
    D --> E{"target release understands<br/>this schema?"}
    E -- no --> E1["refuse: downgrade across<br/>a schema boundary"]
    E -- yes --> F["swap container"]
    F --> G{"/startupz says started?"}
    G -- yes --> H["done"]
    G -- no --> I["one-shot doctor --fix<br/>in a throwaway container"]
    I --> J["re-run, re-gate"]
    J -- started --> K["done: reported as repaired"]
    J -- still not --> L["roll back to previous image"]
    L -- started --> M["rolled back, reason reported"]
    L -- still not --> N["failed: snapshot path named"]

The snapshot is not only a rollback point: database preflight refuses a live database because the schema version sits in the WAL until checkpointed, so the consolidated snapshot is what makes the compatibility check possible at all.

clawops gateway restart

A restart changes neither the deployed version nor who can reach the gateway. Both are read back from the running container rather than guessed:

flowchart LR
    A["gateway restart"] --> B["read current image"]
    B -- no container --> B1["refuse: nothing to reuse.<br/>latest and stable point at 2.0"]
    B --> C["read current publish scope"]
    C --> D["recreate with the same<br/>version and reachability"]
    D --> E{"/startupz says started?"}
    E -- no --> E1["fail with the reason"]
    E -- yes --> F["done"]

Migrating an existing 1.x deployment

flowchart TD
    A["clawops migrate"] --> B{"1.x container running?"}
    B -- no --> B1["nothing to rescue: state was<br/>already lost to an earlier restart"]
    B -- yes --> C["verified backup, inside the running container"]
    C -- "backup fails" --> C1["refused: nothing touched"]
    C --> D["extract state from the RUNNING container"]
    D --> E["chown 1000:1000"]
    E --> F["stop and remove 1.x"]
    F --> G["synthesise a valid 2.0 config"]
    G --> H["start 2.0 with the state directory"]
    H --> I{"/startupz started?"}
    I -- "no: schema still migrating" --> J["restart once"]
    J --> K{"started?"}
    K -- no --> K1["failed: points at the backup"]
    K --> L["report"]
    I -- yes --> L
    L --> M["what carried over,<br/>device identity, config to review"]

Two things about that shape are not obvious, and both came from running a real migration:

State is extracted from the running container. All 1.x state lived inside it, clawops mounted none, so stopping first destroys what the migration came to save.

The config is synthesised, not carried forward. 1.x never had one that applied; the file clawops mounted was read by nothing. Your old settings are reported as intent to review, never applied blindly. Their channel blocks would not validate against 2.0 anyway.

The gateway also needs two starts: the first performs the state-schema migration and reports it as pending. migrate waits for the second rather than declaring success early.

If you ran gateway restart, gateway update or config set on a clawops before 2.0, your state is already gone, nothing was mounted to survive the container replacement. migrate says so plainly rather than pretending to rescue it.

clawops backup restore works again, and never in place

v1.7.5 made restore fail with an explanation, because the OpenClaw it supported had no restore subcommand to call. 2.0 does, and clawops delegates to it:

flowchart TD
    A["clawops backup restore --file X"] --> B["upload archive to the host"]
    B --> C["openclaw backup restore --target &lt;staging&gt;"]
    C -- "target not empty" --> C1["refused by OpenClaw"]
    C --> D["archive verified, expanded<br/>into a fresh directory"]
    D --> E["warnings printed verbatim<br/>time travel, channel relink,<br/>approvals, plugins"]
    E --> F["nothing activated"]
    F --> G["you stop the gateway, swap the<br/>state dir, restart, re-apply"]

clawops does not extract archives itself and does not restore in place. The final step is manual on purpose, and re-applying matters: the archive does not carry plugin node_modules, so a restored deployment starts without its model providers, looking healthy while doing it.

The archive is a credential. It carries the state database, mcp_oauth_stores, secret_store_entries, worker_environment_credentials, device_auth_tokens, unencrypted. clawops now writes it 0600 locally; it previously used the default 0644.

Model providers that need a plugin are installed for you

OpenClaw 2.0 made model providers install-gated plugins. Twenty-four ship in the image, anthropic, openai, google, ollama, openrouter among them, but not all of them. Configuring one that is not bundled, without installing it, produces a gateway that starts, reports healthy, and has no model backend.

clawops installs what your config needs, pinned to an exact version, during apply:

Resolving clawhub:@openclaw/deepseek-provider@2026.9.2…
Downloading plugin @openclaw/deepseek-provider@2026.9.2 from ClawHub…
Installed plugin: deepseek

This adds an outbound dependency the 1.x line did not have: clawhub.ai. It is needed while apply is running, not at boot. Deliberately, so a failure reaches the person running the command rather than a locked-down host at 3am. Blocked, it looks like this:

fetch failed | getaddrinfo EAI_AGAIN clawhub.ai | EAI_AGAIN

clawops checks the installed provider IDs afterwards and will not call the deploy finished while a configured provider is missing. Required outbound access lists every destination and when it is needed.

Chat channels are installed for you too

Every channel in OpenClaw 2.0 is an install-gated plugin. clawops apply installs the ones your config names, during the deploy while egress exists, and then asks the gateway whether they are really installed:

[clawops] warning: the gateway is running, but these configured channels are not installed:
discord. They will never connect.

It has to ask. openclaw channels add. The obvious command, returns success even when the plugin install fails, so clawops uses openclaw plugins install and verifies against channels list --all --json.

Channel plugins are pinned to the supported runtime. The current latest does not install on it: plugin "discord" requires plugin API >=2026.9.3, but this OpenClaw runtime exposes 2026.9.2. The same drift that forced version pins on model providers.

Telegram needs nothing installed: it ships in the image.

Bad config is caught before it is written

Config is validated against OpenClaw's own schema, captured from the image, not hand-written, before anything is sent to the host, and again before a write replaces a working file. clawops plan refuses a plan whose config the gateway would reject, while the plan is still a file you can edit.

A rejected config is kept at <path>.rejected.<timestamp> and the live one is left alone, so a validation failure never costs you what you were trying to write.

One rule is clawops's own: gateway.mode is optional in the schema and mandatory in practice. A config without it passes openclaw config validate and then exits 78.

Containers are hardened

The gateway runs with --cap-drop=ALL, --security-opt no-new-privileges, --init and --pids-limit 512. State is owned numerically by 1000:1000, matching the container's user rather than a host account that may not have that uid.

The version pin is enforced everywhere it can change

doctor, plan, up and apply refuse an OpenClaw release outside the supported range, and gateway restart reuses the version already deployed rather than resolving a moving tag. A restart changes neither the version nor who can reach it.

The gateway is no longer exposed to your network

The container publishes on 127.0.0.1:18789 instead of 0.0.0.0:18789. Reach it with clawops tunnel or a reverse proxy on the host.

Previously the wizard set allowedGatewayCidrs from the CIDR you gave for SSH, so a plaintext HTTP dashboard. Token in the URL. Was opened to your whole shell-access network as a side effect of one unrelated answer. To bind all interfaces deliberately, set network.publishGateway: "all".

You must act if a client or reverse proxy on another machine reaches the gateway directly, or external monitoring hits /health. A proxy on the host is unaffected; one in a container on the host needs --network host.

Health checks can actually fail

The gateway serves its Control UI on a catch-all route, so any unmatched path answers 200 with HTML:

/healthz                        200  application/json   {"ok":true,"status":"live"}
/health-typo                    200  text/html          <!doctype html>…

clawops probed with curl -fsS … >/dev/null, which succeeds on a typo. It proved something was listening on the port, not that the gateway was healthy. Probes now read the response body, and the restart gate uses /startupz rather than liveness, after a restart the process listens long before startup finishes.

clawops mcp wire actually wires something now

It has never worked, not on 2.0, not on any 1.x release. It wrote gateway.mcpClients, which is not a key OpenClaw has: checked against the config schemas of 2026.4.5, 2026.7.1-2 and 2026.9.2. The real key is top-level mcp.servers. And the entry it wrote was command: "clawops" over stdio, which spawns inside the gateway container, where clawops is not installed and nothing installs it.

On 1.x nothing validated the write, so clawops stored a key nothing read, restarted your gateway, and reported: "The gateway's AI can now run clawops commands." It could not.

flowchart TD
    A["clawops mcp wire"] --> B["openclaw mcp add --transport streamable-http"]
    B --> C{"gateway connects<br/>to the URL?"}
    C -- no --> C1["probe fails, nothing saved,<br/>clawops prints the reason"]
    C -- yes --> D["saved to mcp.servers.clawops"]
    D --> E["openclaw mcp reload"]

It delegates to openclaw mcp add now, which probes the server before saving, so "wired" means the gateway connected, not that a file was written.

You have to run the server yourself. clawops is not installed on the gateway host:

clawops mcp serve --http 18790 --bind 0.0.0.0 --token "$(openssl rand -hex 16)"
clawops mcp wire --stack prod --token <same token>

Installing clawops on the gateway host is a deliberate follow-up, not part of 2.0: it puts deployment credentials on the deployed box, and the gateway's AI is reachable from every channel it is connected to. See docs/security/threat-model.md T11.

clawops mcp serve --http serves more than one client, and asks who you are

Two bugs, found by testing against a real gateway rather than a mock.

It built one transport for the whole process, so the first client to connect claimed it and every later one. A second editor, a reconnect, the gateway's own probe, was answered "Server already initialized". HTTP mode is the multi-client mode.

It had no authentication, while exposing every tool including clawops_destroy. It now takes a bearer token, compares it in constant time, and refuses to bind anywhere but loopback without one.

The firewall follows the deployment

flowchart TD
    A["clawops plan"] --> B{"publishGateway?"}
    B -- "loopback (default)" --> C{"allowedGatewayCidrs empty?"}
    C -- no --> C1["refuse: those rules would admit<br/>traffic to a closed port"]
    C -- yes --> D["SSH rules only"]
    B -- all --> E["SSH rules + gateway rules<br/>on spec.network.gatewayPort"]
    D --> F["clawops harden"]
    E --> F
    F --> G["read the container's port bindings"]
    G --> H{"published to the network?"}
    H -- no --> H1["ufw: SSH only"]
    H -- yes --> H2["ufw: SSH + the published port"]

Three security controls were doing the opposite of what they say.

clawops harden opened the gateway port on every deployment. The ufw module ran ufw allow 18789/tcp unconditionally. Since the gateway publishes on 127.0.0.1, that opened a port nothing was listening on. A hardening step widening the firewall past what the deployment exposes. It now reads the running container's port bindings and adds the rule only when the gateway is really published, on whatever port it is published on.

The AWS security-group audit exempted the two ports it exists to check. Ports 22 and 18789 were on an "expected" list, so a group opening SSH or the gateway to 0.0.0.0/0 came back as "No unexpected open ingress rules found". It also never read IPv6 rules, so ::/0 was invisible.

The setup wizard defaulted SSH access to 0.0.0.0/0. Pressing Enter opened SSH to the whole internet, on the path most first-time users take. It offers your own IP as a /32 now, and when that cannot be detected it offers no default and requires an answer.

clawops plan could not express any of it, and apply never passed any of it to Pulumi. Both are fixed in 2.0.1. See the list at the top of this section.

The gateway port comes from the plan

"network": {
  "allowedSshCidrs": ["203.0.113.4/32"],
  "allowedGatewayCidrs": [],
  "publishGateway": "loopback",
  "gatewayPort": 9443
}

One value now reaches the security-group rules, the container publish flag, the default gateway.port and the gateway URL. It was a constant redeclared in eleven places, so changing it meant finding all of them, and missing one produced a container publishing one port, a gateway listening on another, and a firewall opening a third.

Local deployments use clawops up --gateway-port 9443.

clawops doctor answers whether it works, and says so in its exit code

flowchart TD
    A["clawops doctor"] --> B["local: Node, Pulumi CLI + home,<br/>config, SSH key, credentials"]
    B --> C{"--stack given?"}
    C -- no --> Z["report"]
    C -- yes --> D["container state"]
    D --> E["deployed OpenClaw version"]
    E --> F["probe /startupz<br/>and read the body"]
    F --> G["published scope, disk,<br/>log rotation, hardening drift"]
    G --> Z
    Z --> Y{"any check failed?"}
    Y -- no --> Y1["exit 0"]
    Y -- yes --> Y2["exit 1"]

Three changes:

It asks the gateway. doctor used to read docker inspect's healthcheck field, which the OpenClaw image does not set, so it reported "no healthcheck configured" and moved on. A running container means the process started, not that it serves. It now probes /startupz and reads the body.

It exits 1 when something failed. Only an old Node.js used to do that; an unreadable SSH key or an unsupported gateway exited 0, so a CI step running clawops doctor read a broken deployment as success. Warnings still exit 0, a fresh machine with no stacks is unconfigured, not broken.

It is an MCP tool. clawops_doctor returns the same report as structured data, so an agent that hits a failure can find out why. It reports only; it never runs openclaw doctor --fix. --json gives the CLI the same report.

clawops agents list stops inventing an empty list

The command ended in || echo '[]', so a stopped container, a gateway still starting, or a Docker permission error all produced "No agents running.", a wrong answer rather than an error. It now fails, and says which.

Day-two commands work on AWS

gateway restart, logs, monitor, backup, agents, config set and doctor's container checks were all broken on AWS: clawops connects as ubuntu, but provisioning only put clawops in the docker group, so every Docker command failed with permission denied. GCP and Azure connect as clawops, so only AWS was affected.

Removed

clawops agents restart and the clawops_agents_restart MCP tool. OpenClaw 2.0 has no per-agent restart, only gateway restart and daemon restart, both of which interrupt every agent on the host. Use clawops gateway restart, or stay on @clawops/cli@legacy.

clawops agents list and clawops agents logs are unaffected.


Who this is for

Shortened here. Read the whole README on GitHub.

Signals

GitHub stars
6
Last commit
Sep 2026
Weekly_downloads
164 weekly_downloads
Advanced
Delivery
clawops MCP server → your ahel connector (mcp.ahel.ai) → your AI.
Item type
mcp-server
Key
io-github-dfridkin-clawops
Source
github.com/dfridkin/clawops