Documentation menu
On this page

Install wardend

curl -fsSL https://wardenclaw.dev/install.sh | sudo sh

One command, one question. The script finds the OpenClaw gateway service of the user who ran sudo (~/.config/systemd/user/openclaw-gateway.service), shows what it is about to do and, after a y, wraps that gateway in place: the same user, the same home, the same command and environment, now started by wardend as root’s child under the exec gate. Every launch outside an approved subtree is a card on your phone, and until a phone is paired nothing the agent launches will run, so the install ends with the pairing. Your chat with the agent does not change; with the app in observe mode it signs every card while you watch what the agent asks for.

What changes on the machine, so that the model cannot turn the guard off: the old user service is disabled and wardend (a root system service) starts the same command; the user leaves the sudo and docker groups, and root stays with a human account (an existing sudoer, else <user>-admin created with the same ssh keys and sudo without a password: log in as that account from now on); the agent’s sudo and docker become requests through rootexec: every request is a card on your phone (the read-only docker and systemctl verbs are rules the app may sign by itself). It starts wardend, prints the pairing QR and returns when the phone is approved. What it does, step by step: Hardened install.

The same command run again over a wrapped gateway wraps it again: the unit is re-rendered from the new release’s template (the previous one is kept as wardend.service.wardend-bak and the changed lines are printed, so you see what the new template does before it is written), the user leaves the privileged groups again if he got them back, the old user service stays disabled, and wardend restarts (with the gateway) only if the unit or the binary changed. That is the upgrade of a wrapping install, and the way back after a rollback to the user service. To update the binaries only and leave the unit as it is, add --no-unit.

A dry run that changes nothing and needs no root:

curl -fsSL https://wardenclaw.dev/install.sh | sudo sh -s -- --dry-run

No OpenClaw service found, or another harness? The flags describe it; --yes asks nothing:

curl -fsSL https://wardenclaw.dev/install.sh | sudo sh -s -- --agent-user agent --rw-paths /srv/projects \
  --harness-cmd "/usr/bin/node /opt/openclaw/dist/index.js gateway --port 18789" --yes

--uninstall removes wardend and keeps the journal (Uninstall), --version vX.Y.Z pins a release, --single-user is the trial variant below, --help lists the rest.

Before you pipe into sudo

wardend is a security tool, so don’t take the one-liner on faith. The script refuses any archive that doesn’t match the release’s checksums, but the script itself reaches you over TLS from this domain. The file here is a copy of install.sh from the latest release, so its checksum is in that release’s checksums.txt. To check it, download it from here and the checksums from GitHub, compare them, read the script, run it. Each step runs only if the one before it succeeded:

U=https://github.com/xarvel/WardenClaw/releases/latest/download
curl --proto '=https' --tlsv1.2 -fsSLO https://wardenclaw.dev/install.sh &&
  curl --proto '=https' --tlsv1.2 -fsSLO $U/checksums.txt &&
  sha256sum --ignore-missing -c checksums.txt &&
  less install.sh &&
  sudo sh install.sh

Releases are not signed, so this check tells you that the script is the one listed in the release’s checksums.txt and lets you read it before it runs; it does not tell you who built the release. For a few minutes after a new release the site may still serve the previous install.sh, and sha256sum reports FAILED: try again a bit later. At any other time a mismatch means the file here is not the one from the release: don’t run it, and report it privately.

Both files come from GitHub, so the check above catches a tampered copy of the script on this site or on the way to you, not a takeover of the repository or of the GitHub account: that is the trust boundary. An upgrade doesn’t need this site at all (below).

Build from source

Go 1.26, no cgo. Then run the same root-side script by hand in Step 2:

git clone https://github.com/xarvel/WardenClaw
cd wardenclaw/daemon
go build -o wardend .

Hardened or single-user

There are two ways to run wardend. They differ in one thing that decides everything: whether the agent harness runs as the same Linux user as wardend.

Hardened (recommended) Single-user (dev / quick try)
wardend runs as root, system service your user
harness runs as a separate unprivileged user, child of wardend your user
binary, config, key, journal, unit owned by root owned by your user
can the model turn wardend off? no, the kernel refuses by UID yes, without a single execve
use it for any real agent with a shell trying wardend out for an evening

The reason is simple. wardend gates execve/execveat. Signals, ptrace, /proc/<pid>/mem and file writes are decided by the kernel from the UID. If the model runs as the user that owns wardend, it can kill the supervisor or rewrite its config without running any program. The full analysis is in Why the model can’t turn wardend off.

Requirements

The installer checks all of this and stops with a clear message.

uname -r                               # 5.19 or newer
ls /proc/sys/kernel/yama 2>/dev/null   # present = Yama available

What you end up with

systemd
 └─ wardend  (root)            /usr/local/bin/wardend, system unit, --require-hardened
     └─ harness  (agent)       seccomp filter installed, then uid/gid dropped by wardend
         └─ tools the agent runs (agent), every execve gated
Path Owner, mode What
/usr/local/bin/wardend root, 0755 binary
/etc/wardend/ root, 0700 config.json (0600), your policy.json
/var/lib/wardend/ root, 0700 supervisor.key, journal.jsonl, wardend.sock
/etc/systemd/system/wardend.service root, 0644 system unit
/usr/local/share/wardend/ root, 0755 deploy/, redteam/, uninstall.sh of the installed release
/var/lib/agent/ agent the harness’s home and data

What makes it hold:

The sandbox options apply to the whole unit, including the harness. That is on purpose, and it has two consequences you’ll meet below: the harness can write only to its home and the paths you list, and nothing it needs may live under /home or /root.

Step 1. Install the harness system-wide

The unit hides /home and /root from the harness (ProtectHome), so the harness program itself must live elsewhere, for example under /opt/<harness> or /usr/local, owned by root. If it lives in your home (nvm, ~/.local), reinstall it there: the agent had better not be able to rewrite its own harness anyway. The installer refuses a harness command with paths under /home or /root.

Create the shared working directories you want to give the agent (for example /srv/projects) now: every path in --rw-paths must exist.

Step 2. Run the installer

curl -fsSL https://wardenclaw.dev/install.sh | sudo sh

In order, the script:

  1. checks Linux, the CPU architecture, the kernel (5.19+), systemd and root;
  2. downloads checksums.txt and the archive for your architecture from the GitHub release, checks the archive’s SHA-256 against the list, and checks that the archive’s version is the one you asked for and not older than the installed one. Any mismatch stops it before anything is written;
  3. asks for the agent user (default agent), the harness command and extra writable directories, then shows a summary and asks to confirm;
  4. copies deploy/, redteam/ and uninstall.sh of the release to /usr/local/share/wardend/ and runs deploy/hardened-install.sh from it: creates the user if needed (useradd --system --home-dir /var/lib/agent --shell /usr/sbin/nologin), stops if it has privileged groups or sudo rights, installs the binary, creates /etc/wardend and /var/lib/wardend (root, 0700), writes config.json (0600, "require_hardened": true, "child_user"), renders the unit, sets ptrace_scope, writes the polkit rule that keeps the agent from starting its own systemd –user manager and runs daemon-reload;
  5. when it wraps an existing gateway, starts wardend and pairs the phone: it prints the QR of wardend pair start and returns only after the phone is approved (skipped on an upgrade whose config already names a trusted device; without a terminal it prints sudo wardend pair start for you to run from your own terminal). With flags it prints the next steps instead: start wardend, then pair.

The flag install does not move data, stop your old service or start wardend. Run it again at any time: an existing config is left as is, and with an existing flag install and no --harness-cmd it only upgrades the binary (a systemctl restart wardend applies it); a changed unit template is reported and re-rendered only with --harness-cmd again. A re-run over a wrapping install re-renders the unit itself (above).

Upgrades. Run the copy the install left behind, not a new download:

sudo /usr/local/share/wardend/install.sh

It fetches the latest release from GitHub and checks it the way a first install does. It doesn’t depend on this site: whoever changed install.sh here would not touch the installed copy, and a piped script checks only itself. A release older than the installed one is refused; going back on purpose takes --version vX.Y.Z --allow-downgrade.

The upgrade takes effect on systemctl restart wardend, and that restarts the agent: wardend runs the harness as its child, so every session of the agent ends, and requests still waiting for a decision are denied (the command gets EPERM). Upgrade at a quiet moment, and check the config before the restart:

sudo wardend status | jq .pending   # 0: nothing waits for a decision
sudo wardend config-check --config /etc/wardend/config.json && sudo systemctl restart wardend

Upgrade wardend and the app together. The app checks the protocol version of the server: after a protocol change it says which part to update and signs no decisions until it is updated. wardenctl checks the same and exits with code 4 (External approvers).

Flag Default Meaning
--harness-cmd asked harness command, absolute paths, nothing under /home or /root
--agent-user agent (asked) harness user, created if missing
--agent-home existing home or /var/lib/<user> must exist; under /home the unit gets ProtectHome=tmpfs and binds only that directory in
--rw-paths empty (asked) extra writable directories, space-separated, must exist
--ptrace-scope 2 skip to leave the sysctl alone
--root-exec on for the wrapping install, off with flags the rootexec socket: the agent may ask wardend to run one command as root (wardend rootexec -- …, or plain sudo … and docker … through shims on the harness PATH), judged by the policy’s root_exec list (every request is a card; the built-in read-only docker and systemctl rules are ones the app may sign by rule); --no-root-exec turns it off for the wrapping install; daemon/docs/rootexec.md
--admin-user an existing sudoer, else <user>-admin the human account that keeps root when the agent user leaves the sudo and docker groups (wrapping install)
--with-wardenctl off also install the terminal approver (keep it on another machine if you can)
--no-unit off on a re-run over an installed wrapping install: update the binaries only, keep the systemd unit as it is (by default the unit is re-rendered from the new template, the previous one is kept as .wardend-bak)
--dry-run off print every step, change nothing, no root needed
--yes off no questions; a missing --harness-cmd is an error
--allow-downgrade off install a release older than the installed one

The script stops with an error if the kernel is older than 5.19, the harness or its home is under /home, a writable path doesn’t exist, or the agent user is in a privileged group or has sudo rights.

From source, the same root-side script takes environment variables (HARNESS_CMD, AGENT_USER, AGENT_HOME, RW_PATHS, PTRACE_SCOPE, ROOT_EXEC) and the path to your build. Dry run first:

DRY_RUN=1 AGENT_USER=agent RW_PATHS="/srv/projects" \
  HARNESS_CMD="/usr/bin/node /opt/openclaw/dist/index.js gateway --port 18789" \
  deploy/hardened-install.sh ./wardend

If the output looks right, run the same with sudo and without DRY_RUN=1, as a separate step:

sudo AGENT_USER=agent RW_PATHS="/srv/projects" \
  HARNESS_CMD="/usr/bin/node /opt/openclaw/dist/index.js gateway --port 18789" \
  deploy/hardened-install.sh ./wardend

To check the user by hand:

id agent               # only its own group
sudo -l -U agent       # "not allowed to run sudo"

Step 3. Move the harness data (manual)

This step is not automated: every harness keeps its state differently. The goal is to give the agent user exactly what the harness needs and nothing that belongs to you.

  1. Stop the harness and disable its current service so it can’t come back around wardend (for a user unit: systemctl --user disable --now <unit>; also remove a single-user wardend drop-in if you had one, like this).

  2. Copy the harness state into the agent’s home and give it to the agent. With OpenClaw as an example:

    # once: a second copy would overwrite what the agent's harness has written since
    sudo test -e /var/lib/agent/.openclaw || sudo rsync -a ~/.openclaw/ /var/lib/agent/.openclaw/
    sudo chown -R agent:agent /var/lib/agent
    sudo grep -rn --include='*.json' "$HOME" /var/lib/agent/.openclaw/   # fix absolute paths to your old home

    API keys and tokens go with it, mode 0600. The harness needs them, so the agent can read them anyway; what changes is that it no longer reads your keys.

  3. Shared working directories. Grant access to exactly the directories the agent works in, without handing over ownership. Either a group:

    sudo groupadd agentwork
    sudo usermod -aG agentwork agent
    sudo usermod -aG agentwork "$USER"          # if you edit the same files
    sudo chgrp -R agentwork /srv/projects
    sudo chmod -R g+rwX /srv/projects
    sudo find /srv/projects -type d -exec chmod g+s {} +

    or ACLs:

    sudo setfacl -R -m u:agent:rwX -m d:u:agent:rwX /srv/projects

    Don’t share a repository you also work in as yourself. Anything the agent can write there can later run as you: .git/hooks, .git/config, .envrc, a Makefile, npm or pip scripts, editor tasks. That is the escape step 4 forbids. Give the agent its own clone, take its work through a bare repository that you own (the agent pushes, you fetch), and review the changes before you run anything from them. Shared directories are for data, not for code you execute.

    These directories must be in --rw-paths (they become ReadWritePaths). Added one later? Run the installer again with the full --harness-cmd and --rw-paths to re-render the unit.

  4. Never give the agent write access to anything that runs as you or as root: your home, dotfiles (~/.bashrc, ~/.profile), ~/.local/bin, directories on your PATH, ~/.config/systemd/user, crontabs, git hooks of repositories you work in as yourself. A write there is code that runs as you next time you log in, and you have sudo. That is the same escape as the single-user install, just one step longer.

The agent’s own home may be under /home (--agent-home /home/<user>, or an existing account’s home): the installer then renders ProtectHome=tmpfs plus BindPaths= for that one directory, and every other home stays hidden. Any other data that must stay under /home: move it (for example to /srv, or under the agent’s home), or edit the unit by hand to add it to BindPaths=.

Step 4. Start and pair

sudo systemctl enable --now wardend
systemctl status wardend                              # Main PID is wardend
ps -o user,pid,args --ppid "$(systemctl show -p MainPID --value wardend)"   # harness as agent
sudo journalctl -u wardend -n 50 | grep selfcheck     # no FATAL, no warnings
sudo wardend status

Until a phone is paired nothing the agent launches will run: every launch is a card, and a card nobody signs expires into EPERM after the TTL (wardend status warns about it). The wrapping install pairs the phone itself at the end; after a flag install, pair from your own terminal, never from a chat with the agent. The socket belongs to root, so the client commands run with sudo:

sudo wardend pair start

It prints the QR code, waits for the phone, shows the device’s name and key fingerprint and asks Approve this device? [y/N]. Compare the fingerprint with the phone screen and answer y. From a script, or to approve later: sudo wardend pair list, then sudo wardend pair approve <id>.

The app reaches wardend through the relay, exactly as in the single-user setup: wardend dials out, no port or tunnel is needed. Where to get the app: Get the app; pairing, a check and troubleshooting: Connect the phone.

Step 5. Watch with the app in observe, then choose its mode

The gate always asks; what changes is who signs. Put the app in observe for the first days: it signs every card automatically, and the feed and the journal show what the agent launches and which risk hints come with it (remote execution, leaving the gate, package installs, publishing, secrets, destructive commands). Let the agent do its normal work, then let wardend count what the app signed. wardend replay runs the journal through the same classifier and prints aggregates only, no argv:

sudo wardend replay --journal /var/lib/wardend/journal.jsonl --config /etc/wardend/config.json

Read cards (how many a day you would answer by hand), peaks (how many arrive within 2 minutes, the ticket TTL, and how many at night), categories (why) and top rules (cards) (which hints fire most). To see the commands themselves:

sudo jq -r 'select(.kind=="exec" and (.data.class=="root" or .data.class=="delegating")) | "\(.data.category)  \(.data.argv|join(" "))"' \
  /var/lib/wardend/journal.jsonl | sort | uniq -c | sort -rn | head -50

If a card comes up for routine you trust (a scheduled job, a script of the harness), name it by exact pattern in service_allow of a policy file in /etc/wardend, so it stays root-owned: such a launch is a service card the app signs by rule, in every mode; a launch with a risk hint never passes as service. Your own hint rules go to tripwire in the same file (Policy):

# only if there is no policy yet: never overwrite the rules you have added
sudo test -e /etc/wardend/policy.json ||
  sudo sh -c 'umask 077; /usr/local/bin/wardend policy-defaults > /etc/wardend/policy.json'
sudoedit /etc/wardend/policy.json

Set "policy": "/etc/wardend/policy.json" in /etc/wardend/config.json and restart:

sudoedit /etc/wardend/config.json
# a config wardend can't read would stop it, and the agent with it: check before the restart
sudo wardend config-check --config /etc/wardend/config.json && sudo systemctl restart wardend

wardend config-check reads the config and the policy it names the way wardend does at start, and starts nothing. It reports JSON errors with line and column, unknown config keys (an error: wardend would not start, so a typo can’t silently leave the default; in the policy file an unknown key is a warning), values wardend would refuse and a config without a trusted device. Exit code 0 means wardend will start with these files.

Once the stream looks like something you can answer, switch the app: delegate lets its on-device judge sign by its rules and brings the rest to you; manual brings every card to you. Nothing changes on the server for that, and no restart is needed. The phone must stay paired and online: a card nobody signs expires into EPERM.

Step 6. Verify: redteam as the agent

redteam/hardened-check.sh runs as the agent user and tries every cross-process and file vector against the installed wardend. It is harmless: signal 0 instead of SIGKILL, open-for-append without writing, reads without printing.

sudo -u agent bash -s < /usr/local/share/wardend/redteam/hardened-check.sh
=== Signals, ptrace, supervisor memory ===
  PASS: kill -0 812: Operation not permitted
  PASS: /proc/812/mem: denied (kernel ptrace check)
  PASS: systemctl kill wardend: Interactive authentication required.
=== wardend files ===
  PASS: /usr/local/bin/wardend: write denied
  PASS: /etc/wardend/config.json: directory closed
  PASS: /var/lib/wardend/supervisor.key: directory closed
  ...
=== Own systemd --user manager (runs commands outside the gate) ===
  PASS: linger is off for agent
  PASS: no systemd --user manager for agent
  PASS: loginctl enable-linger: Could not enable linger: Access denied
total: PASS=30 FAIL=0 SKIP=0

Every attack must be refused: FAIL=0. Any FAIL names the exact point where the install is not hardened. For extra confidence, ask the agent itself to run the script from inside the harness: the result must be the same.

Rollback

A seccomp filter cannot be removed from a running tree, so every rollback restarts the harness. There are two:

sudo /usr/local/share/wardend/uninstall.sh

What stays and why, --purge, running the harness again without wardend, the agent user and paired devices: Uninstall.

Troubleshooting

Single-user install (dev / quick try)

Only for trying wardend out. A model running with the same user’s rights can turn wardend off: kill it, trace it, rewrite its config or unit, and restart the harness without the gate, all without a single execve. In this mode wardend is defense in depth against unwanted exec, not a guard the model cannot remove.

What stays open, compared with the hardened install:

Vector Single-user Hardened
kill / ptrace / process_vm_writev the supervisor open closed by the kernel (different UID)
write the binary, config, key, journal open closed (root-owned, 0700 directories)
write /proc/<supervisor>/mem open closed by the kernel
rewrite the service unit, restart without the gate open (user unit) closed (root-owned system unit)
read the supervisor key and forge its signatures open closed
OOM-kill the supervisor open reduced (OOMScoreAdjust)

Setup takes a minute and needs no root:

curl -fsSL https://wardenclaw.dev/install.sh | sh -s -- --single-user

or from source:

go build -o wardend . && install -D -m 0755 wardend ~/.local/bin/wardend
mkdir -p ~/.wardend && chmod 700 ~/.wardend
wardend run -- <harness command>
wardend pair start           # from another terminal: until a phone is paired nothing the harness launches will run

The installer pairs the phone itself at the end (wardend pair start, no sudo here). To wrap a systemd user service with it, see Running the OpenClaw gateway under wardend in the CLI reference. Don’t set ptrace_scope=2 here: a non-root wardend would lose access to /proc/<pid>/mem of its own children. When you’re done trying, remove it (~/.local/share/wardend/uninstall.sh --single-user, or sh uninstall.sh --single-user in daemon/ of the source tree) and do the hardened install.

Comparison

Vector Single-user Hardened
Run a command around the gate (execve/execveat) closed closed
Nested seccomp notify filter answering “yes” closed closed
32-bit / compat exec closed closed
memfd_create + execveat, fexecve, ld.so directly closed closed
TOCTOU: swap argv or the file after CONTINUE closed closed
Approve its own card over the socket or the relay closed closed
Forge or replay a ticket (the device key lives on the phone) closed closed
Read the supervisor key, forge journal and response signatures open closed
Flood the card queue closed closed
kill / ptrace / process_vm_writev the supervisor open closed by the kernel
Write the binary / config / key / journal open closed
Write /proc/<supervisor>/mem open closed by the kernel
Rewrite the unit and restart without the gate open closed
OOM-kill the supervisor open reduced

What neither variant covers (work started outside wardend’s process tree, what an approved root does with interpreters, files and network) is listed in Why the model can’t turn wardend off.